Skip to content

Commit 20f930b

Browse files
authored
Merge pull request #104 from chrivers/chrivers/entertainment-reversing
First version of entertainment mode support! > [!IMPORTANT] > ***This is the big one people have been waiting for!*** :-) After spending countless hours reverse-engineering the proprietary Zigbee data format used by Philips Hue lights for "Entertainment Mode", *even countlesser* hours have gone into implementing support in Bifrost. Today, I am proud to announce that Bifrost is the **first program in the world** to have an entirely Open Source (Free Software) implementation of Hue Entertainment mode! This has been a monumental effort. Before starting this work, the Bifrost code base was about 7200 lines of code. Now, it is over 15000 lines!. In other words, implementing entertainment mode more than doubled the code base! I think it's fair to say, that this was more complicated than anticipated. To make this work at all, the Zigbee2Mqtt project was updated with patches from myself (@chrivers), @danielhitch, and the author of Zigbee2Mqtt, @Koenkk. Special thanks to @Koenkk for taking time out of his busy schedule, to help us get the necessary bits in place. The new code was first released in version 2.1.1. > [!WARNING] > Zigbee2Mqtt MUST BE AT LEAST version 2.1.1 for Entertainment Mode to work. > [!IMPORTANT] > Even though version 2.1.1 is the minimum version, version 2.1.3 or greater is > highly recommended, since some important bugs were fixed after version 2.1.1. This is the very first of Bifrost that supports Entertainment Mode at all, so please bear with us while we iron out any bugs or rough edges. Should work: - Creating Entertainment Areas from the Hue App. - Updating Entertainment Area settings. - Adding lights or 7-segment strips to entertainment areas. - Streaming to Entertainment Areas from "Hue Sync for PC" (tested). - Streaming to Entertainment Areas from Play HDMI sync box 8K (tested). - Streaming to one or more lights. - Streaming to a combination of lamps and light strips. Perhaps working: (please let us know what your experience is!) - Adding 3-segment strips to entertainment areas. - Adding non-color lights to entertainment areas. - Streaming to Entertainment Areas from Play HDMI sync box (older, non-8K version). Not yet working: - Adjusting "stream mode" ("From Device" vs "From Bridge") - Adjusting "relative brightness" for lights - Streaming in XY color mode (most things seem to use RGB mode)
2 parents ebaf722 + 03ad88b commit 20f930b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2348
-281
lines changed

Cargo.lock

+262-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+37-6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ keywords = [
3636
[workspace]
3737
members = [
3838
"crates/hue",
39+
"crates/svc",
3940
"crates/zcl",
4041
]
4142

@@ -44,7 +45,7 @@ unstable_features = "forbid"
4445
unused_lifetimes = "warn"
4546
unused_qualifications = "warn"
4647

47-
[lints.clippy]
48+
[workspace.lints.clippy]
4849
all = { level = "warn", priority = -1 }
4950
correctness = { level = "warn", priority = -1 }
5051
pedantic = { level = "warn", priority = -1 }
@@ -57,19 +58,43 @@ multiple_crate_versions = "allow"
5758
missing_errors_doc = "allow"
5859
missing_panics_doc = "allow"
5960

61+
[lints]
62+
workspace = true
63+
6064
[features]
61-
default = ["server", "server-banner"]
65+
default = [
66+
"server",
67+
"server-banner",
68+
"tls-openssl",
69+
# "tls-rustls",
70+
]
71+
72+
tls-openssl = [
73+
"axum-server/tls-openssl",
74+
"reqwest/native-tls",
75+
"dep:openssl"
76+
]
77+
78+
tls-rustls = [
79+
"axum-server/rustls",
80+
"axum-server/tls-rustls-no-provider",
81+
"reqwest/rustls-tls"
82+
]
6283

6384
server = []
6485
server-banner = ["server", "dep:termcolor", "dep:itertools"]
6586

87+
[profile.dev]
88+
debug = "limited"
89+
split-debuginfo = "unpacked"
90+
6691
[dependencies]
6792
axum = { version = "0.8.1", features = ["json", "tokio", "macros", "multipart"], default-features = false }
6893
axum-core = "0.5.0"
69-
axum-server = { version = "0.7.1", features = ["rustls", "tls-rustls-no-provider"], default-features = false }
94+
axum-server = { version = "0.7.1", features = [], default-features = false }
7095
bytes = "1.10.0"
7196
chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false }
72-
clap = { version = "4.5.29", features = ["std", "color", "derive"], default-features = false }
97+
clap = { version = "4.5.29", features = ["std", "color", "derive", "help", "usage"], default-features = false }
7398
config = { version = "0.15.8", default-features = false, features = ["yaml"] }
7499
futures = "0.3.31"
75100
hyper = "1.6.0"
@@ -83,7 +108,7 @@ serde = { version = "1.0.217", features = ["derive"], default-features = false }
83108
serde_json = "1.0.138"
84109
serde_yml = "0"
85110
thiserror = "2.0.11"
86-
tokio = { version = "1.43.0", features = ["io-util", "process", "rt-multi-thread"], default-features = false }
111+
tokio = { version = "1.43.0", features = ["io-util", "process", "rt-multi-thread", "signal"], default-features = false }
87112
tokio-stream = { version = "0.1.17", features = ["sync"], default-features = false }
88113
tokio-tungstenite = "0.26.1"
89114
tower = "0.5.2"
@@ -102,12 +127,18 @@ sha1 = "0.10.6"
102127
rustls-pemfile = "2.2.0"
103128
termcolor = { version = "1.4.1", optional = true }
104129
itertools = { version = "0.14.0", optional = true }
105-
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls"] }
130+
reqwest = { version = "0.12.12", default-features = false, features = ["json"] }
106131
url = { version = "2.5.4", features = ["serde"] }
107132
hex = "0.4.3"
108133
async-trait = "0.1.86"
109134
hue = { version = "0.1.0", path = "crates/hue" }
110135
zcl = { path = "crates/zcl" }
136+
openssl = { version = "0.10", optional = true }
137+
tokio-util = { version = "0.7.13", features = ["net"] }
138+
tokio-openssl = "0.6.5"
139+
udp-stream = "0.0.12"
140+
maplit = "1.0.2"
141+
svc = { version = "0.1.0", path = "crates/svc" }
111142

112143
[dev-dependencies]
113144
clap-stdin = "0.6.0"

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ To install Bifrost from source, you will need the following:
3131
3. The MAC address of the network interface you want to run the server on
3232
4. `build-essential` package for compiling the source code (on Debian/Ubuntu systems)
3333

34-
First, install `build-essential` :
34+
First, install a few necessary build dependencies:
3535

3636
```sh
37-
sudo apt install build-essential
37+
sudo apt install build-essential pkg-config libssl-dev
3838
```
3939

4040
When you have these things available, install bifrost:

crates/zcl/src/attr.rs

+31-18
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ pub enum ZclDataType {
103103
/** IEEE address (U64) type */
104104
ZclIeeeaddr = 0xf0,
105105

106+
/** 128-bit security key */
107+
ZclSecurityKey = 0xf1,
108+
106109
/** Invalid data type */
107110
ZclInvalid = 0xff,
108111
}
@@ -149,32 +152,34 @@ pub enum ZclAttrValue {
149152
Bytes(Vec<u8>),
150153
String(String),
151154
IeeeAddr(Vec<u8>),
155+
SecurityKey([u8; 16]),
152156
Unsupported,
153157
}
154158

155159
impl Debug for ZclAttrValue {
156160
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157161
match self {
158162
Self::Null => write!(f, "Null"),
159-
Self::X8(val) => write!(f, "x8:{}", val),
160-
Self::X16(val) => write!(f, "x16:{}", val),
161-
Self::X32(val) => write!(f, "x32:{}", val),
162-
Self::Bool(val) => write!(f, "bool:{}", val),
163-
Self::B8(val) => write!(f, "b8:{:02X}", val),
164-
Self::B16(val) => write!(f, "b16:{:04X}", val),
165-
Self::B32(val) => write!(f, "b32:{:08X}", val),
166-
Self::B40(val) => write!(f, "b40:{:010X}", val),
167-
Self::B48(val) => write!(f, "b48:{:012X}", val),
168-
Self::B56(val) => write!(f, "b56:{:014X}", val),
169-
Self::B64(val) => write!(f, "b64:{:016X}", val),
170-
Self::U8(val) => write!(f, "u8:{:02X}", val),
171-
Self::U16(val) => write!(f, "u16:{:04X}", val),
172-
Self::U32(val) => write!(f, "u32:{:08X}", val),
173-
Self::I16(val) => write!(f, "i16:{:04X}", val),
174-
Self::E8(val) => write!(f, "e8:{:02X}", val),
163+
Self::X8(val) => write!(f, "x8:{val}"),
164+
Self::X16(val) => write!(f, "x16:{val}"),
165+
Self::X32(val) => write!(f, "x32:{val}"),
166+
Self::Bool(val) => write!(f, "bool:{val}"),
167+
Self::B8(val) => write!(f, "b8:{val:02X}"),
168+
Self::B16(val) => write!(f, "b16:{val:04X}"),
169+
Self::B32(val) => write!(f, "b32:{val:08X}"),
170+
Self::B40(val) => write!(f, "b40:{val:010X}"),
171+
Self::B48(val) => write!(f, "b48:{val:012X}"),
172+
Self::B56(val) => write!(f, "b56:{val:014X}"),
173+
Self::B64(val) => write!(f, "b64:{val:016X}"),
174+
Self::U8(val) => write!(f, "u8:{val:02X}"),
175+
Self::U16(val) => write!(f, "u16:{val:04X}"),
176+
Self::U32(val) => write!(f, "u32:{val:08X}"),
177+
Self::I16(val) => write!(f, "i16:{val:04X}"),
178+
Self::E8(val) => write!(f, "e8:{val:02X}"),
175179
Self::Bytes(val) => write!(f, "hex:{}", hex::encode(val)),
176-
Self::String(val) => write!(f, "str:{}", &val),
180+
Self::String(val) => write!(f, "str:{val}"),
177181
Self::IeeeAddr(val) => write!(f, "ieeeaddr {}", hex::encode(val)),
182+
Self::SecurityKey(val) => write!(f, "seckey {}", hex::encode(val)),
178183
Self::Unsupported => write!(f, "Unsupported"),
179184
}
180185
}
@@ -240,6 +245,11 @@ impl ZclAttr {
240245
ZclAttrValue::String(String::from_utf8(buf)?)
241246
}
242247
ZclDataType::ZclIeeeaddr => todo!(),
248+
ZclDataType::ZclSecurityKey => {
249+
let mut buf = [0; 16];
250+
rdr.read_exact(&mut buf)?;
251+
ZclAttrValue::SecurityKey(buf)
252+
}
243253
ZclDataType::ZclInvalid => todo!(),
244254
};
245255

@@ -261,6 +271,7 @@ pub struct ZclReadAttrResp {
261271
}
262272

263273
impl ZclReadAttrResp {
274+
#[allow(clippy::cast_possible_truncation)]
264275
pub fn parse(data: &[u8]) -> ZclResult<Self> {
265276
let mut attr = vec![];
266277

@@ -279,6 +290,7 @@ pub struct ZclWriteAttr {
279290
}
280291

281292
impl ZclWriteAttr {
293+
#[allow(clippy::cast_possible_truncation)]
282294
pub fn parse(data: &[u8]) -> ZclResult<Self> {
283295
let mut attr = vec![];
284296

@@ -297,6 +309,7 @@ pub struct ZclReportAttr {
297309
}
298310

299311
impl ZclReportAttr {
312+
#[allow(clippy::cast_possible_truncation)]
300313
pub fn parse(data: &[u8]) -> ZclResult<Self> {
301314
let mut attr = vec![];
302315

@@ -316,7 +329,7 @@ pub struct ZclDefaultResp {
316329
}
317330

318331
impl ZclDefaultResp {
319-
pub fn parse(data: &[u8]) -> ZclResult<Self> {
332+
pub const fn parse(data: &[u8]) -> ZclResult<Self> {
320333
Ok(Self {
321334
cmd: data[0],
322335
stat: data[1],

crates/zcl/src/cluster/colorctrl.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::frame::{ZclFrame, ZclFrameDirection};
22

3+
#[must_use]
34
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
45
if frame.manufacturer_specific() {
56
return None;

crates/zcl/src/cluster/effects.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::frame::{ZclFrame, ZclFrameDirection};
22

3+
#[must_use]
34
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
45
if frame.flags.direction == ZclFrameDirection::ClientToServer {
56
match frame.cmd {

crates/zcl/src/cluster/groups.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::frame::{ZclFrame, ZclFrameDirection};
22

3+
#[must_use]
34
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
45
if frame.flags.direction == ZclFrameDirection::ClientToServer {
56
match frame.cmd {

crates/zcl/src/cluster/hue_fc01.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use packed_struct::PackedStructSlice;
22

33
use crate::error::ZclResult;
44
use crate::frame::ZclFrame;
5-
use hue::zigbee::{HueEntFrame, HueEntSegmentConfig, HueEntStop};
5+
use hue::zigbee::{HueEntFrame, HueEntSegmentConfig, HueEntSegmentLayout, HueEntStop};
66

77
pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
88
if !frame.cluster_specific() {
@@ -12,6 +12,14 @@ pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
1212
match frame.cmd {
1313
1 => Ok(Some(format!("{:x?}", HueEntFrame::parse(data)?))),
1414
3 => Ok(Some(format!("{:x?}", HueEntStop::unpack_from_slice(data)?))),
15+
4 => {
16+
let res = if frame.c2s() && data.len() == 1 {
17+
"HueEntSegmentLayoutReq".to_string()
18+
} else {
19+
format!("{:x?}", HueEntSegmentLayout::parse(data)?)
20+
};
21+
Ok(Some(res))
22+
}
1523
7 => Ok(Some(format!("{:x?}", HueEntSegmentConfig::parse(data)?))),
1624
_ => Ok(None),
1725
}

crates/zcl/src/cluster/hue_fc03.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
1010

1111
match frame.cmd {
1212
0x00 => {
13-
let zflags = Flags::from_bits((data[0] as u16) | (data[1] as u16) << 8).unwrap();
13+
let zflags = Flags::from_bits(u16::from(data[0]) | (u16::from(data[1]) << 8)).unwrap();
1414
Ok(Some(format!("{:?} {}", zflags, hex::encode(&data[2..]))))
1515
}
1616
_ => Ok(None),

crates/zcl/src/cluster/levelctrl.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::frame::{ZclFrame, ZclFrameDirection};
22

3+
#[must_use]
34
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
45
if frame.manufacturer_specific() {
56
return None;

crates/zcl/src/cluster/onoff.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::frame::{ZclFrame, ZclFrameDirection};
22

3+
#[must_use]
34
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
45
if frame.manufacturer_specific() {
56
return None;

crates/zcl/src/cluster/scenes.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ use hue::zigbee::Flags;
44

55
use crate::frame::{ZclFrame, ZclFrameDirection};
66

7+
#[must_use]
78
pub fn describe(frame: &ZclFrame, data: &[u8]) -> Option<String> {
89
if frame.manufacturer_specific() {
910
if frame.flags.direction == ZclFrameDirection::ClientToServer {
1011
match frame.cmd {
1112
0x02 => Some(format!(
1213
"SetComposite {:?}",
13-
Flags::from_bits((data[3] as u16) | (data[4] as u16) << 8).unwrap()
14+
Flags::from_bits(u16::from(data[3]) | (u16::from(data[4]) << 8)).unwrap()
1415
)),
1516
_ => None,
1617
}

crates/zcl/src/frame.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,18 @@ impl ZclFrame {
8383
})
8484
}
8585

86+
#[must_use]
87+
pub fn c2s(&self) -> bool {
88+
self.flags.direction == ZclFrameDirection::ClientToServer
89+
}
90+
91+
#[must_use]
8692
pub fn cluster_specific(&self) -> bool {
8793
self.flags.frame_type == ZclFrameType::ClusterSpecific
8894
}
8995

90-
pub fn manufacturer_specific(&self) -> bool {
96+
#[must_use]
97+
pub const fn manufacturer_specific(&self) -> bool {
9198
self.flags.manufacturer_specific
9299
}
93100
}

doc/config-reference.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,24 @@ bridge:
3333
gateway: 10.0.0.1
3434
timezone: Europe/Copenhagen
3535

36-
# http port for emulated bridge
36+
# HTTP port for emulated bridge
3737
#
3838
# beware: most client programs do NOT support non-standard ports.
3939
# This is for advanced users (e.g. bifrost behind a reverse proxy)
4040
http_port: 80
4141

42-
# https port for emulated bridge
42+
# HTTPS port for emulated bridge
4343
#
4444
# beware: most client programs do NOT support non-standard ports.
4545
# This is for advanced users (e.g. bifrost behind a reverse proxy)
4646
https_port: 443
4747

48+
# DTLS port for emulated bridge (Hue Entertainment streaming)
49+
#
50+
# beware: client programs do NOT support non-standard ports.
51+
# For advanced users (e.g. bifrost behind a port forwarded firewall)
52+
entm_port: 2100
53+
4854
# Zigbee2mqtt section
4955
#
5056
# Make a sub-section for each zigbee2mqtt server you want to connect

0 commit comments

Comments
 (0)