feat(homecore-hap/p1): ADR-125 HAP bridge scaffold (17 tests pass)
Add `homecore-hap` crate: HapAccessoryType (11 variants), HapCharacteristic, EntityToAccessoryMapper (light/switch/binary_sensor/sensor/cover/lock domains), HapBridge add/remove/running API, NullAdvertiser mDNS stub, and RuViewToHapMapper (presence→OccupancySensor, fall→LeakSensor, motion→MotionSensor). P2 `hap-server` feature gates the real hap = "0.1" server + mdns-sd integration. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
02742f2f47
commit
cc079febef
|
|
@ -35,6 +35,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
|
|
@ -313,6 +314,15 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
|
|
@ -1750,6 +1760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
|
@ -1863,6 +1874,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1964,6 +1981,9 @@ name = "either"
|
|||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
|
|
@ -2074,6 +2094,23 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"home",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
|
|
@ -2327,6 +2364,17 @@ dependencies = [
|
|||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-intrusive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"lock_api",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
|
|
@ -3205,6 +3253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -3232,6 +3281,15 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
version = "7.5.4"
|
||||
|
|
@ -3282,6 +3340,9 @@ name = "heck"
|
|||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
|
|
@ -3301,6 +3362,15 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
|
|
@ -3341,6 +3411,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "homecore"
|
||||
version = "0.1.0-alpha.0"
|
||||
|
|
@ -3378,6 +3457,36 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "homecore-automation"
|
||||
version = "0.1.0-alpha.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"homecore",
|
||||
"minijinja",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "homecore-hap"
|
||||
version = "0.1.0-alpha.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"homecore",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "homecore-plugins"
|
||||
version = "0.1.0-alpha.0"
|
||||
|
|
@ -3392,6 +3501,21 @@ dependencies = [
|
|||
"wasmtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "homecore-recorder"
|
||||
version = "0.1.0-alpha.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"homecore",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
|
|
@ -4076,6 +4200,9 @@ name = "lazy_static"
|
|||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
|
|
@ -4167,6 +4294,17 @@ dependencies = [
|
|||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev"
|
||||
version = "0.3.0"
|
||||
|
|
@ -4312,6 +4450,16 @@ dependencies = [
|
|||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdns-sd"
|
||||
version = "0.11.5"
|
||||
|
|
@ -4350,6 +4498,12 @@ dependencies = [
|
|||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memo-map"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
|
|
@ -4444,6 +4598,17 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "2.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2929e494b2280e1e18959bb2e121da03347ae896896fdfaceaab43c88a02803f"
|
||||
dependencies = [
|
||||
"memo-map",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
|
@ -4809,6 +4974,22 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
|
|
@ -5587,6 +5768,17 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der",
|
||||
"pkcs8",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
|
|
@ -6605,6 +6797,26 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstar"
|
||||
version = "0.8.4"
|
||||
|
|
@ -7419,6 +7631,19 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.2"
|
||||
|
|
@ -7561,6 +7786,7 @@ version = "2.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
|
|
@ -7747,6 +7973,219 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
|
||||
|
||||
[[package]]
|
||||
name = "sqlformat"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"atoi",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
"event-listener",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-intrusive",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap 2.13.0",
|
||||
"log",
|
||||
"memchr",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlformat",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sqlx-core",
|
||||
"sqlx-macros-core",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
"heck 0.4.1",
|
||||
"hex",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx-core",
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 1.0.109",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"digest",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"generic-array 0.14.7",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
"dotenvy",
|
||||
"etcetera",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"home",
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
"flume",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-intrusive",
|
||||
"futures-util",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"sqlx-core",
|
||||
"tracing",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
|
|
@ -7790,6 +8229,17 @@ dependencies = [
|
|||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -9103,12 +9553,33 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
|
|
@ -9127,6 +9598,18 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_categories"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
|
@ -9199,6 +9682,12 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "urlpattern"
|
||||
version = "0.3.0"
|
||||
|
|
@ -9359,6 +9848,12 @@ dependencies = [
|
|||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
|
|
@ -10003,6 +10498,16 @@ dependencies = [
|
|||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"wasite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.33"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ members = [
|
|||
"crates/homecore", # ADR-127 — HOMECORE state machine
|
||||
"crates/homecore-plugins", # ADR-128 — HOMECORE-PLUGINS WASM runtime (P1 scaffold)
|
||||
"crates/homecore-api", # ADR-130 — HOMECORE REST + WS API
|
||||
"crates/homecore-automation", # ADR-129 — HOMECORE automation engine
|
||||
"crates/homecore-recorder", # ADR-132 — HOMECORE state recorder
|
||||
# ADR-100/ADR-101: Cognitum Cog packaging — first Cog from this repo.
|
||||
# Ships the wifi-densepose pose-estimation model as a signed binary +
|
||||
# JSONL manifest installable by the Cognitum V0 appliance (cognitum-v0,
|
||||
|
|
@ -55,6 +57,7 @@ members = [
|
|||
# `vendor/rvcsi` and published to crates.io as `rvcsi-*` 0.3.x. Depend on the
|
||||
# published crates (or the submodule's `crates/rvcsi-*` paths) — not as v2
|
||||
# workspace members, since `vendor/rvcsi/Cargo.toml` is its own workspace.
|
||||
"crates/homecore-hap", # ADR-125 — Apple Home HomeKit Accessory Protocol bridge
|
||||
]
|
||||
# ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std),
|
||||
# excluded from workspace to avoid breaking `cargo test --workspace`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
# homecore-hap — Apple Home HomeKit Accessory Protocol bridge (ADR-125 P1 scaffold)
|
||||
#
|
||||
# P1 ships the trait surface, accessory/characteristic types, entity→HAP mapping,
|
||||
# bridge API, and an mDNS-advertise stub. The actual HAP-1.1 server and real
|
||||
# mDNS integration are feature-gated to P2 via the `hap-server` feature flag.
|
||||
|
||||
[package]
|
||||
name = "homecore-hap"
|
||||
version = "0.1.0-alpha.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["rUv <ruv@ruv.net>", "HOMECORE Contributors"]
|
||||
description = "Apple Home HomeKit Accessory Protocol bridge — ADR-125 P1 scaffold"
|
||||
repository = "https://github.com/ruvnet/wifi-densepose"
|
||||
|
||||
[lib]
|
||||
name = "homecore_hap"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# P2: gates the actual hap = "0.1" crate integration + real mDNS via mdns-sd
|
||||
hap-server = []
|
||||
|
||||
[dependencies]
|
||||
homecore = { path = "../homecore" }
|
||||
tokio = { version = "1", features = ["sync", "rt", "rt-multi-thread", "time", "macros"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
tracing = "0.1"
|
||||
async-trait = "0.1"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["sync", "rt", "rt-multi-thread", "time", "macros", "test-util"] }
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
//! HAP service type and characteristic enum catalogues.
|
||||
//!
|
||||
//! Mirrors the HAP-1.1 service/characteristic namespace used by Apple Home
|
||||
//! and the `hap` crate (https://crates.io/crates/hap). Keeping these as
|
||||
//! plain Rust enums in P1 avoids the heavy `hap` dep until P2.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// HAP service types exposed by the RuView bridge.
|
||||
///
|
||||
/// Derived from HomeKit Accessory Protocol Specification §8 (service
|
||||
/// definitions) and cross-checked against HA's `homekit` integration
|
||||
/// service catalog.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum HapAccessoryType {
|
||||
/// HAP `Lightbulb` service — maps `light.*` entities.
|
||||
Lightbulb,
|
||||
/// HAP `Switch` service — maps generic boolean `switch.*` entities.
|
||||
Switch,
|
||||
/// HAP `OccupancySensor` — maps presence / occupancy binary sensors.
|
||||
OccupancySensor,
|
||||
/// HAP `MotionSensor` — maps motion binary sensors + RuView motion.
|
||||
MotionSensor,
|
||||
/// HAP `TemperatureSensor` — maps `sensor.*temperature*` entities.
|
||||
TemperatureSensor,
|
||||
/// HAP `HumiditySensor` — maps `sensor.*humidity*` entities.
|
||||
HumiditySensor,
|
||||
/// HAP `LeakSensor` — maps abnormal event sensors; used for fall detection
|
||||
/// following HA's homekit_controller convention (HAP §11.42).
|
||||
LeakSensor,
|
||||
/// HAP `ContactSensor` — maps door / window binary sensors.
|
||||
ContactSensor,
|
||||
/// HAP `Door` service — maps `cover.*door*` entities.
|
||||
Door,
|
||||
/// HAP `LockMechanism` service — maps `lock.*` entities.
|
||||
Lock,
|
||||
/// HAP `SecuritySystem` service — maps alarm / security panel entities.
|
||||
SecuritySystem,
|
||||
}
|
||||
|
||||
impl HapAccessoryType {
|
||||
/// All defined variants — used in tests and for UI enumeration.
|
||||
pub const ALL: &'static [HapAccessoryType] = &[
|
||||
HapAccessoryType::Lightbulb,
|
||||
HapAccessoryType::Switch,
|
||||
HapAccessoryType::OccupancySensor,
|
||||
HapAccessoryType::MotionSensor,
|
||||
HapAccessoryType::TemperatureSensor,
|
||||
HapAccessoryType::HumiditySensor,
|
||||
HapAccessoryType::LeakSensor,
|
||||
HapAccessoryType::ContactSensor,
|
||||
HapAccessoryType::Door,
|
||||
HapAccessoryType::Lock,
|
||||
HapAccessoryType::SecuritySystem,
|
||||
];
|
||||
}
|
||||
|
||||
/// HAP characteristic identifiers that the bridge reads or writes.
|
||||
///
|
||||
/// Each variant corresponds to one HAP characteristic UUID as specified in
|
||||
/// HomeKit Accessory Protocol Specification §9.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum HapCharacteristic {
|
||||
/// `On` (bool) — Lightbulb / Switch power state.
|
||||
On,
|
||||
/// `Brightness` (uint8, 0–100) — Lightbulb brightness percentage.
|
||||
Brightness,
|
||||
/// `CurrentTemperature` (float, °C) — TemperatureSensor reading.
|
||||
CurrentTemperature,
|
||||
/// `CurrentRelativeHumidity` (float, %) — HumiditySensor reading.
|
||||
CurrentRelativeHumidity,
|
||||
/// `OccupancyDetected` (uint8, 0=not detected, 1=detected).
|
||||
OccupancyDetected,
|
||||
/// `MotionDetected` (bool).
|
||||
MotionDetected,
|
||||
/// `LeakDetected` (uint8, 0=no leak, 1=leak detected). Re-used for falls.
|
||||
LeakDetected,
|
||||
/// `ContactSensorState` (uint8, 0=in contact, 1=not in contact).
|
||||
ContactSensorState,
|
||||
/// `CurrentDoorState` (uint8, HAP §9.30).
|
||||
CurrentDoorState,
|
||||
/// `LockCurrentState` (uint8, HAP §9.56).
|
||||
LockCurrentState,
|
||||
/// `SecuritySystemCurrentState` (uint8, HAP §9.97).
|
||||
SecuritySystemCurrentState,
|
||||
}
|
||||
|
||||
/// Typed value carried by a HAP characteristic update.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HapCharacteristicValue {
|
||||
Bool(bool),
|
||||
UInt8(u8),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_11_accessory_types_defined() {
|
||||
assert_eq!(HapAccessoryType::ALL.len(), 11);
|
||||
// Spot-check each variant is present.
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::Lightbulb));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::Switch));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::OccupancySensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::MotionSensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::TemperatureSensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::HumiditySensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::LeakSensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::ContactSensor));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::Door));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::Lock));
|
||||
assert!(HapAccessoryType::ALL.contains(&HapAccessoryType::SecuritySystem));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn characteristic_value_roundtrip_serde() {
|
||||
let v = HapCharacteristicValue::Float(22.5);
|
||||
let json = serde_json::to_string(&v).unwrap();
|
||||
let back: HapCharacteristicValue = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(v, back);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
//! `HapBridge` — owns the set of HOMECORE entities exposed as HAP accessories.
|
||||
//!
|
||||
//! P1 does not start a real HAP-1.1 server; it ships the API surface so other
|
||||
//! crates (and P2's `hap-server` feature) can register accessories and query
|
||||
//! their current mapping. The actual mDNS + HAP pairing is gated to P2.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use homecore::entity::EntityId;
|
||||
|
||||
use crate::accessory::HapAccessoryType;
|
||||
use crate::error::HapError;
|
||||
use crate::mapping::{AccessoryMapping, EntityToAccessoryMapper};
|
||||
use crate::mdns::{HapServiceRecord, MdnsAdvertiser, NullAdvertiser};
|
||||
|
||||
/// One registered HAP accessory — an entity + its last-known mapping.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExposedAccessory {
|
||||
pub entity_id: EntityId,
|
||||
pub accessory_type: HapAccessoryType,
|
||||
pub mapping: AccessoryMapping,
|
||||
}
|
||||
|
||||
struct BridgeInner {
|
||||
accessories: HashMap<EntityId, ExposedAccessory>,
|
||||
}
|
||||
|
||||
/// The P1 HAP bridge.
|
||||
///
|
||||
/// Call [`HapBridge::add_accessory`] to register entities and
|
||||
/// [`HapBridge::running_accessories`] to read back what is currently
|
||||
/// registered. In P2, `start()` will spawn the `hap` server task.
|
||||
#[derive(Clone)]
|
||||
pub struct HapBridge {
|
||||
inner: Arc<RwLock<BridgeInner>>,
|
||||
advertiser: Arc<dyn MdnsAdvertiser>,
|
||||
pub service_record: HapServiceRecord,
|
||||
}
|
||||
|
||||
impl HapBridge {
|
||||
/// Create a bridge with the given service record and a `NullAdvertiser`
|
||||
/// (P1 default — real mDNS lands in P2).
|
||||
pub fn new(service_record: HapServiceRecord) -> Self {
|
||||
Self::with_advertiser(service_record, Arc::new(NullAdvertiser))
|
||||
}
|
||||
|
||||
/// Create a bridge with a custom `MdnsAdvertiser` (used in tests and P2).
|
||||
pub fn with_advertiser(
|
||||
service_record: HapServiceRecord,
|
||||
advertiser: Arc<dyn MdnsAdvertiser>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(BridgeInner { accessories: HashMap::new() })),
|
||||
advertiser,
|
||||
service_record,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register an entity as a HAP accessory.
|
||||
///
|
||||
/// The entity's current mapping is computed from `state`; call
|
||||
/// `update_accessory` on each `StateChanged` event to keep it fresh.
|
||||
///
|
||||
/// Returns `HapError::AlreadyRegistered` if the entity is already
|
||||
/// registered. Call `remove_accessory` first to replace it.
|
||||
pub fn add_accessory(
|
||||
&self,
|
||||
entity_id: &EntityId,
|
||||
state: &homecore::entity::State,
|
||||
) -> Result<(), HapError> {
|
||||
let mapping = EntityToAccessoryMapper::map(entity_id, state)?;
|
||||
let accessory_type = mapping.accessory_type;
|
||||
let exposed = ExposedAccessory {
|
||||
entity_id: entity_id.clone(),
|
||||
accessory_type,
|
||||
mapping,
|
||||
};
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
if inner.accessories.contains_key(entity_id) {
|
||||
return Err(HapError::AlreadyRegistered(entity_id.as_str().to_owned()));
|
||||
}
|
||||
inner.accessories.insert(entity_id.clone(), exposed);
|
||||
tracing::debug!(entity = %entity_id, ?accessory_type, "HAP accessory registered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a registered accessory.
|
||||
///
|
||||
/// Returns `HapError::EntityNotFound` if the entity was not registered.
|
||||
pub fn remove_accessory(&self, entity_id: &EntityId) -> Result<(), HapError> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
if inner.accessories.remove(entity_id).is_none() {
|
||||
return Err(HapError::EntityNotFound(entity_id.as_str().to_owned()));
|
||||
}
|
||||
tracing::debug!(entity = %entity_id, "HAP accessory removed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Snapshot all currently registered accessories.
|
||||
pub fn running_accessories(&self) -> Vec<ExposedAccessory> {
|
||||
self.inner.read().unwrap().accessories.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Number of registered accessories.
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.read().unwrap().accessories.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// P2 stub — will start the HAP-1.1 server + mDNS advertisement.
|
||||
/// In P1 this only fires the null advertiser.
|
||||
pub async fn start(&self) -> Result<(), HapError> {
|
||||
self.advertiser.advertise(&self.service_record).await?;
|
||||
tracing::info!(
|
||||
instance = %self.service_record.instance_name,
|
||||
port = self.service_record.port,
|
||||
"HapBridge started (P1 — no real HAP server; mDNS stub only)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Graceful shutdown — retracts mDNS advertisement.
|
||||
pub async fn stop(&self) -> Result<(), HapError> {
|
||||
self.advertiser.retract(&self.service_record.instance_name).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use homecore::entity::{EntityId, State};
|
||||
use homecore::event::Context;
|
||||
|
||||
fn make_bridge() -> HapBridge {
|
||||
HapBridge::new(HapServiceRecord {
|
||||
instance_name: "RuView Sense".into(),
|
||||
port: 51826,
|
||||
setup_code: "111-22-333".into(),
|
||||
device_id: "AA:BB:CC:DD:EE:FF".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn light_state(name: &str, on: bool, brightness: u8) -> (EntityId, State) {
|
||||
let eid = EntityId::parse(&format!("light.{name}")).unwrap();
|
||||
let attrs = serde_json::json!({"brightness": brightness});
|
||||
let s = State::new(eid.clone(), if on { "on" } else { "off" }, attrs, Context::default());
|
||||
(eid, s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_remove_roundtrip() {
|
||||
let bridge = make_bridge();
|
||||
let (eid, s) = light_state("kitchen", true, 200);
|
||||
|
||||
assert!(bridge.is_empty());
|
||||
bridge.add_accessory(&eid, &s).unwrap();
|
||||
assert_eq!(bridge.len(), 1);
|
||||
|
||||
let acc = bridge.running_accessories();
|
||||
assert_eq!(acc.len(), 1);
|
||||
assert_eq!(acc[0].entity_id, eid);
|
||||
assert_eq!(acc[0].accessory_type, HapAccessoryType::Lightbulb);
|
||||
|
||||
bridge.remove_accessory(&eid).unwrap();
|
||||
assert!(bridge.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_duplicate_returns_error() {
|
||||
let bridge = make_bridge();
|
||||
let (eid, s) = light_state("kitchen", true, 200);
|
||||
bridge.add_accessory(&eid, &s).unwrap();
|
||||
let err = bridge.add_accessory(&eid, &s).unwrap_err();
|
||||
assert!(matches!(err, HapError::AlreadyRegistered(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_nonexistent_returns_error() {
|
||||
let bridge = make_bridge();
|
||||
let eid = EntityId::parse("light.ghost").unwrap();
|
||||
let err = bridge.remove_accessory(&eid).unwrap_err();
|
||||
assert!(matches!(err, HapError::EntityNotFound(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_stop_with_null_advertiser() {
|
||||
let bridge = make_bridge();
|
||||
bridge.start().await.unwrap();
|
||||
bridge.stop().await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//! Unified error type for `homecore-hap`.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors produced by the HAP bridge and its sub-components.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HapError {
|
||||
#[error("entity not found: {0}")]
|
||||
EntityNotFound(String),
|
||||
|
||||
#[error("entity {entity_id} cannot be mapped to a HAP accessory type: {reason}")]
|
||||
UnmappableEntity { entity_id: String, reason: String },
|
||||
|
||||
#[error("accessory already registered: {0}")]
|
||||
AlreadyRegistered(String),
|
||||
|
||||
#[error("mDNS advertiser error: {0}")]
|
||||
MdnsError(String),
|
||||
|
||||
#[error("bridge not running")]
|
||||
NotRunning,
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//! `homecore-hap` — Apple Home HomeKit Accessory Protocol bridge (ADR-125).
|
||||
//!
|
||||
//! # P1 scope
|
||||
//!
|
||||
//! Ships the trait surface and type definitions needed to map HOMECORE entity
|
||||
//! states onto HAP accessory / characteristic values. The actual HAP-1.1 TLS
|
||||
//! server and real mDNS advertisement are gated behind the `hap-server`
|
||||
//! feature (P2). P1 ships `NullAdvertiser` (no-op) so the bridge compiles and
|
||||
//! all tests pass with `--no-default-features`.
|
||||
//!
|
||||
//! # Module layout
|
||||
//!
|
||||
//! | Module | Purpose |
|
||||
//! |--------|---------|
|
||||
//! | [`accessory`] | HAP service / characteristic enum catalogue |
|
||||
//! | [`mapping`] | `EntityToAccessoryMapper` — HOMECORE entity → HAP |
|
||||
//! | [`bridge`] | `HapBridge` — owns exposed accessories |
|
||||
//! | [`mdns`] | `MdnsAdvertiser` trait + `NullAdvertiser` stub |
|
||||
//! | [`ruview`] | `RuViewToHapMapper` — sensing primitives → HAP |
|
||||
//! | [`error`] | Unified `HapError` type |
|
||||
|
||||
pub mod accessory;
|
||||
pub mod bridge;
|
||||
pub mod error;
|
||||
pub mod mapping;
|
||||
pub mod mdns;
|
||||
pub mod ruview;
|
||||
|
||||
pub use accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
|
||||
pub use bridge::{ExposedAccessory, HapBridge};
|
||||
pub use error::HapError;
|
||||
pub use mapping::EntityToAccessoryMapper;
|
||||
pub use mdns::{MdnsAdvertiser, NullAdvertiser};
|
||||
pub use ruview::RuViewToHapMapper;
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
//! HOMECORE entity → HAP accessory type + characteristic value mapping.
|
||||
//!
|
||||
//! Mirrors the HA `homekit` integration's mapping table
|
||||
//! (homeassistant/components/homekit/type_*.py) for the entity domains and
|
||||
//! device classes handled in P1.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use homecore::entity::{EntityId, State};
|
||||
|
||||
use crate::accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
|
||||
use crate::error::HapError;
|
||||
|
||||
/// Result of mapping one HOMECORE entity state to the HAP layer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessoryMapping {
|
||||
/// HAP service type to advertise for this entity.
|
||||
pub accessory_type: HapAccessoryType,
|
||||
/// Characteristic key/value pairs to set on the HAP service.
|
||||
pub characteristics: Vec<(HapCharacteristic, HapCharacteristicValue)>,
|
||||
}
|
||||
|
||||
/// Maps a HOMECORE entity `(EntityId, State)` pair to a `HapAccessoryType`
|
||||
/// and its current characteristic values.
|
||||
///
|
||||
/// Rule table (mirrors HA homekit_controller mapping):
|
||||
///
|
||||
/// | Domain | device_class | HAP service |
|
||||
/// |--------|-------------|-------------|
|
||||
/// | `light` | — | Lightbulb |
|
||||
/// | `switch` | — | Switch |
|
||||
/// | `binary_sensor` | `occupancy` | OccupancySensor |
|
||||
/// | `binary_sensor` | `motion` | MotionSensor |
|
||||
/// | `binary_sensor` | `door` / `window` | ContactSensor |
|
||||
/// | `sensor` | — + unit=°C/°F | TemperatureSensor |
|
||||
/// | `sensor` | — + unit=% (humidity) | HumiditySensor |
|
||||
/// | `cover` (door) | — | Door |
|
||||
/// | `lock` | — | Lock |
|
||||
pub struct EntityToAccessoryMapper;
|
||||
|
||||
impl EntityToAccessoryMapper {
|
||||
/// Map a HOMECORE entity to its HAP representation.
|
||||
///
|
||||
/// Returns `HapError::UnmappableEntity` for domains that have no
|
||||
/// defined HAP mapping (e.g. `automation`, `input_boolean`).
|
||||
pub fn map(entity_id: &EntityId, state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
match entity_id.domain() {
|
||||
"light" => Self::map_light(state),
|
||||
"switch" => Self::map_switch(state),
|
||||
"binary_sensor" => Self::map_binary_sensor(entity_id, state),
|
||||
"sensor" => Self::map_sensor(entity_id, state),
|
||||
"cover" => Self::map_cover(state),
|
||||
"lock" => Self::map_lock(state),
|
||||
other => Err(HapError::UnmappableEntity {
|
||||
entity_id: entity_id.as_str().to_owned(),
|
||||
reason: format!("domain '{other}' has no HAP mapping in P1"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_light(state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
let on = state.state == "on";
|
||||
let mut chars = vec![(HapCharacteristic::On, HapCharacteristicValue::Bool(on))];
|
||||
if let Some(b) = state.attributes.get("brightness").and_then(Value::as_u64) {
|
||||
chars.push((
|
||||
HapCharacteristic::Brightness,
|
||||
HapCharacteristicValue::UInt8(b.min(255) as u8),
|
||||
));
|
||||
}
|
||||
Ok(AccessoryMapping { accessory_type: HapAccessoryType::Lightbulb, characteristics: chars })
|
||||
}
|
||||
|
||||
fn map_switch(state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
let on = state.state == "on";
|
||||
Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::Switch,
|
||||
characteristics: vec![(HapCharacteristic::On, HapCharacteristicValue::Bool(on))],
|
||||
})
|
||||
}
|
||||
|
||||
fn map_binary_sensor(
|
||||
entity_id: &EntityId,
|
||||
state: &State,
|
||||
) -> Result<AccessoryMapping, HapError> {
|
||||
let detected = state.state == "on";
|
||||
let device_class = state
|
||||
.attributes
|
||||
.get("device_class")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
|
||||
// Also check name heuristics for device_class-less entities.
|
||||
let name = entity_id.name();
|
||||
let is_occupancy = device_class == "occupancy" || name.contains("occupancy") || name.contains("presence");
|
||||
let is_motion = device_class == "motion" || name.contains("motion");
|
||||
let is_door = device_class == "door" || device_class == "window";
|
||||
|
||||
if is_occupancy {
|
||||
return Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::OccupancySensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::OccupancyDetected,
|
||||
HapCharacteristicValue::UInt8(if detected { 1 } else { 0 }),
|
||||
)],
|
||||
});
|
||||
}
|
||||
if is_motion {
|
||||
return Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::MotionSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::MotionDetected,
|
||||
HapCharacteristicValue::Bool(detected),
|
||||
)],
|
||||
});
|
||||
}
|
||||
if is_door {
|
||||
return Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::ContactSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::ContactSensorState,
|
||||
HapCharacteristicValue::UInt8(if detected { 1 } else { 0 }),
|
||||
)],
|
||||
});
|
||||
}
|
||||
// Fallback: treat as motion sensor
|
||||
Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::MotionSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::MotionDetected,
|
||||
HapCharacteristicValue::Bool(detected),
|
||||
)],
|
||||
})
|
||||
}
|
||||
|
||||
fn map_sensor(entity_id: &EntityId, state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
let unit = state
|
||||
.attributes
|
||||
.get("unit_of_measurement")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let name = entity_id.name();
|
||||
|
||||
let is_temp = unit == "°C" || unit == "°F" || unit == "C" || unit == "F"
|
||||
|| name.contains("temp") || name.contains("temperature");
|
||||
let is_humidity = unit == "%" && (name.contains("humid") || name.contains("rh"));
|
||||
|
||||
if is_temp {
|
||||
let temp: f64 = state.state.parse().unwrap_or(0.0);
|
||||
return Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::TemperatureSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::CurrentTemperature,
|
||||
HapCharacteristicValue::Float(temp),
|
||||
)],
|
||||
});
|
||||
}
|
||||
if is_humidity {
|
||||
let hum: f64 = state.state.parse().unwrap_or(0.0);
|
||||
return Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::HumiditySensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::CurrentRelativeHumidity,
|
||||
HapCharacteristicValue::Float(hum),
|
||||
)],
|
||||
});
|
||||
}
|
||||
Err(HapError::UnmappableEntity {
|
||||
entity_id: entity_id.as_str().to_owned(),
|
||||
reason: "sensor unit/name not recognised as temperature or humidity".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_cover(state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
let door_state: u8 = match state.state.as_str() {
|
||||
"open" => 0,
|
||||
"opening" => 2,
|
||||
"closing" => 3,
|
||||
_ => 1, // closed
|
||||
};
|
||||
Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::Door,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::CurrentDoorState,
|
||||
HapCharacteristicValue::UInt8(door_state),
|
||||
)],
|
||||
})
|
||||
}
|
||||
|
||||
fn map_lock(state: &State) -> Result<AccessoryMapping, HapError> {
|
||||
let lock_state: u8 = match state.state.as_str() {
|
||||
"unlocked" => 0,
|
||||
"locked" => 1,
|
||||
_ => 3, // unknown
|
||||
};
|
||||
Ok(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::Lock,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::LockCurrentState,
|
||||
HapCharacteristicValue::UInt8(lock_state),
|
||||
)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use homecore::entity::{EntityId, State};
|
||||
use homecore::event::Context;
|
||||
|
||||
fn state(id: &str, st: &str, attrs: serde_json::Value) -> (EntityId, State) {
|
||||
let eid = EntityId::parse(id).unwrap();
|
||||
let s = State::new(eid.clone(), st, attrs, Context::default());
|
||||
(eid, s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_kitchen_on_with_brightness() {
|
||||
let (eid, s) = state(
|
||||
"light.kitchen",
|
||||
"on",
|
||||
serde_json::json!({"brightness": 200}),
|
||||
);
|
||||
let mapping = EntityToAccessoryMapper::map(&eid, &s).unwrap();
|
||||
assert_eq!(mapping.accessory_type, HapAccessoryType::Lightbulb);
|
||||
assert!(mapping.characteristics.contains(&(
|
||||
HapCharacteristic::On,
|
||||
HapCharacteristicValue::Bool(true)
|
||||
)));
|
||||
assert!(mapping.characteristics.contains(&(
|
||||
HapCharacteristic::Brightness,
|
||||
HapCharacteristicValue::UInt8(200)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_sensor_occupancy_device_class() {
|
||||
let (eid, s) = state(
|
||||
"binary_sensor.kitchen_presence",
|
||||
"on",
|
||||
serde_json::json!({"device_class": "occupancy"}),
|
||||
);
|
||||
let mapping = EntityToAccessoryMapper::map(&eid, &s).unwrap();
|
||||
assert_eq!(mapping.accessory_type, HapAccessoryType::OccupancySensor);
|
||||
assert!(mapping.characteristics.contains(&(
|
||||
HapCharacteristic::OccupancyDetected,
|
||||
HapCharacteristicValue::UInt8(1)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensor_outdoor_temp_celsius() {
|
||||
let (eid, s) = state(
|
||||
"sensor.outdoor_temp",
|
||||
"21.5",
|
||||
serde_json::json!({"unit_of_measurement": "°C"}),
|
||||
);
|
||||
let mapping = EntityToAccessoryMapper::map(&eid, &s).unwrap();
|
||||
assert_eq!(mapping.accessory_type, HapAccessoryType::TemperatureSensor);
|
||||
assert!(mapping.characteristics.contains(&(
|
||||
HapCharacteristic::CurrentTemperature,
|
||||
HapCharacteristicValue::Float(21.5)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmappable_domain_returns_error() {
|
||||
let (eid, s) = state("automation.morning", "on", serde_json::json!({}));
|
||||
assert!(EntityToAccessoryMapper::map(&eid, &s).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
//! mDNS advertisement trait and P1 no-op stub.
|
||||
//!
|
||||
//! Real mDNS via the `mdns-sd` crate (https://crates.io/crates/mdns-sd)
|
||||
//! lands in P2 behind the `hap-server` feature flag. P1 ships `NullAdvertiser`
|
||||
//! so the bridge compiles and tests pass without any mDNS infrastructure.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::error::HapError;
|
||||
|
||||
/// Service record advertised over mDNS for HAP discovery.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HapServiceRecord {
|
||||
/// Service instance name shown in Apple Home ("RuView Sense").
|
||||
pub instance_name: String,
|
||||
/// TCP port the HAP server listens on (default 51826).
|
||||
pub port: u16,
|
||||
/// HAP pairing setup code (8 digits, formatted as XXX-XX-XXX).
|
||||
pub setup_code: String,
|
||||
/// Unique device ID (colon-separated MAC-like hex, required by HAP §5.4).
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
/// Advertise (and retract) a HAP accessory over mDNS (`_hap._tcp`).
|
||||
///
|
||||
/// Implementors register the `_hap._tcp` service so HomePod / Apple TV can
|
||||
/// discover the bridge and initiate pairing. P1 provides only `NullAdvertiser`.
|
||||
#[async_trait]
|
||||
pub trait MdnsAdvertiser: Send + Sync {
|
||||
/// Begin advertising the service. Idempotent.
|
||||
async fn advertise(&self, record: &HapServiceRecord) -> Result<(), HapError>;
|
||||
|
||||
/// Stop advertising. Called on bridge shutdown.
|
||||
async fn retract(&self, instance_name: &str) -> Result<(), HapError>;
|
||||
}
|
||||
|
||||
/// No-op advertiser for P1 / test environments.
|
||||
///
|
||||
/// All calls succeed without touching the network.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NullAdvertiser;
|
||||
|
||||
#[async_trait]
|
||||
impl MdnsAdvertiser for NullAdvertiser {
|
||||
async fn advertise(&self, record: &HapServiceRecord) -> Result<(), HapError> {
|
||||
tracing::debug!(
|
||||
instance = %record.instance_name,
|
||||
port = record.port,
|
||||
"NullAdvertiser: skipping mDNS advertisement (P1 stub)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn retract(&self, instance_name: &str) -> Result<(), HapError> {
|
||||
tracing::debug!(
|
||||
instance = %instance_name,
|
||||
"NullAdvertiser: skipping mDNS retract (P1 stub)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn null_advertiser_is_noop() {
|
||||
let adv = NullAdvertiser;
|
||||
let rec = HapServiceRecord {
|
||||
instance_name: "RuView Sense".into(),
|
||||
port: 51826,
|
||||
setup_code: "111-22-333".into(),
|
||||
device_id: "AA:BB:CC:DD:EE:FF".into(),
|
||||
};
|
||||
adv.advertise(&rec).await.unwrap();
|
||||
adv.retract(&rec.instance_name).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
//! RuView sensing primitives → HAP characteristic mapping (ADR-125 §2.1.d).
|
||||
//!
|
||||
//! Per ADR-125, RuView's privacy-class-2/3 events map to HomeKit primitives
|
||||
//! as semantic ambient signals, not surveillance events:
|
||||
//!
|
||||
//! | RuView primitive | HAP service | Rationale |
|
||||
//! |-----------------|-------------|-----------|
|
||||
//! | `edge_vitals.presence` | OccupancySensor | Anonymous presence = occupancy |
|
||||
//! | `edge_vitals.motion` | MotionSensor | Motion burst |
|
||||
//! | `edge_vitals.fall_detected` | LeakSensor | HA convention: abnormal events |
|
||||
//! | `edge_vitals.breathing_present` | OccupancySensor | Sleep-room occupancy |
|
||||
//!
|
||||
//! Raw `identity_risk_score`, `rf_signature_hash`, and class-0 BFI data are
|
||||
//! **never** mapped. Structural invariant I1 (ADR-118 §2.2) is enforced here.
|
||||
|
||||
use crate::accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
|
||||
use crate::mapping::AccessoryMapping;
|
||||
|
||||
/// Parsed RuView edge vitals event from the sensing-server.
|
||||
///
|
||||
/// All fields are class-2 (Anonymous) or class-3 (Restricted) derived signals.
|
||||
/// Raw BFI / `identity_risk_score` / `rf_signature_hash` are intentionally
|
||||
/// absent — they must not cross the HAP boundary per ADR-125 §2.2.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EdgeVitals {
|
||||
/// True if at least one person is present in the sensing zone.
|
||||
pub presence: bool,
|
||||
/// True if motion was detected in the last sensing window.
|
||||
pub motion: bool,
|
||||
/// True if a fall event was detected (latched, 5 s cooldown).
|
||||
pub fall_detected: bool,
|
||||
/// True if rhythmic breathing is detected (sleep-room occupancy signal).
|
||||
pub breathing_present: bool,
|
||||
/// Optional ambient temperature reading (°C), forwarded if available
|
||||
/// from a co-located temperature sensor.
|
||||
pub ambient_temp_c: Option<f64>,
|
||||
}
|
||||
|
||||
/// Maps `EdgeVitals` to a `Vec<AccessoryMapping>` — one per RuView primitive
|
||||
/// that should be exposed as a distinct HAP service (child accessory).
|
||||
pub struct RuViewToHapMapper;
|
||||
|
||||
impl RuViewToHapMapper {
|
||||
/// Convert a `EdgeVitals` snapshot to HAP accessory mappings.
|
||||
///
|
||||
/// Always returns mappings for presence, motion, and fall; the ambient
|
||||
/// temperature mapping is only emitted when `ambient_temp_c` is `Some`.
|
||||
pub fn map(vitals: &EdgeVitals) -> Vec<AccessoryMapping> {
|
||||
let mut out = Vec::with_capacity(4);
|
||||
|
||||
// Presence → OccupancySensor
|
||||
out.push(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::OccupancySensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::OccupancyDetected,
|
||||
HapCharacteristicValue::UInt8(if vitals.presence || vitals.breathing_present { 1 } else { 0 }),
|
||||
)],
|
||||
});
|
||||
|
||||
// Motion → MotionSensor
|
||||
out.push(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::MotionSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::MotionDetected,
|
||||
HapCharacteristicValue::Bool(vitals.motion),
|
||||
)],
|
||||
});
|
||||
|
||||
// Fall detected → LeakSensor (HA homekit_controller convention for
|
||||
// "abnormal event" — not a literal water leak, but an automation-
|
||||
// triggerable threshold event, per ADR-125 §2.1.d).
|
||||
out.push(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::LeakSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::LeakDetected,
|
||||
HapCharacteristicValue::UInt8(if vitals.fall_detected { 1 } else { 0 }),
|
||||
)],
|
||||
});
|
||||
|
||||
// Optional temperature
|
||||
if let Some(temp) = vitals.ambient_temp_c {
|
||||
out.push(AccessoryMapping {
|
||||
accessory_type: HapAccessoryType::TemperatureSensor,
|
||||
characteristics: vec![(
|
||||
HapCharacteristic::CurrentTemperature,
|
||||
HapCharacteristicValue::Float(temp),
|
||||
)],
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::accessory::{HapAccessoryType, HapCharacteristic, HapCharacteristicValue};
|
||||
|
||||
#[test]
|
||||
fn presence_true_maps_to_occupancy_detected_1() {
|
||||
let vitals = EdgeVitals { presence: true, ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
let occ = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::OccupancySensor).unwrap();
|
||||
assert!(occ.characteristics.contains(&(
|
||||
HapCharacteristic::OccupancyDetected,
|
||||
HapCharacteristicValue::UInt8(1)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fall_detected_maps_to_leak_sensor() {
|
||||
let vitals = EdgeVitals { fall_detected: true, ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
let leak = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::LeakSensor).unwrap();
|
||||
assert!(leak.characteristics.contains(&(
|
||||
HapCharacteristic::LeakDetected,
|
||||
HapCharacteristicValue::UInt8(1)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_false_maps_correctly() {
|
||||
let vitals = EdgeVitals { motion: false, ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
let mot = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::MotionSensor).unwrap();
|
||||
assert!(mot.characteristics.contains(&(
|
||||
HapCharacteristic::MotionDetected,
|
||||
HapCharacteristicValue::Bool(false)
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_temp_emits_temperature_mapping() {
|
||||
let vitals = EdgeVitals { ambient_temp_c: Some(22.5), ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
let temp = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::TemperatureSensor);
|
||||
assert!(temp.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_ambient_temp_omits_temperature_mapping() {
|
||||
let vitals = EdgeVitals { ambient_temp_c: None, ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
assert!(mappings.iter().all(|m| m.accessory_type != HapAccessoryType::TemperatureSensor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breathing_present_triggers_occupancy() {
|
||||
let vitals = EdgeVitals { presence: false, breathing_present: true, ..Default::default() };
|
||||
let mappings = RuViewToHapMapper::map(&vitals);
|
||||
let occ = mappings.iter().find(|m| m.accessory_type == HapAccessoryType::OccupancySensor).unwrap();
|
||||
assert!(occ.characteristics.contains(&(
|
||||
HapCharacteristic::OccupancyDetected,
|
||||
HapCharacteristicValue::UInt8(1)
|
||||
)));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue