#![allow(clippy::struct_excessive_bools)] use std::collections::HashMap; use std::fmt::Debug; use std::fmt::Display; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct RawMessage { pub topic: String, pub payload: Value, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "topic", content = "payload")] pub enum Message { #[serde(rename = "bridge/info")] BridgeInfo(Box), #[serde(rename = "bridge/state")] BridgeState(Value), #[serde(rename = "bridge/event")] BridgeEvent(BridgeEvent), #[serde(rename = "bridge/devices")] BridgeDevices(BridgeDevices), #[serde(rename = "bridge/groups")] BridgeGroups(BridgeGroups), #[serde(rename = "bridge/logging")] BridgeLogging(BridgeLogging), #[serde(rename = "bridge/definitions")] BridgeDefinitions(Value), #[serde(rename = "bridge/extensions")] BridgeExtensions(Value), #[serde(rename = "bridge/converters")] BridgeConverters(Value), #[serde(rename = "bridge/response/options")] BridgeOptions(Value), #[serde(rename = "bridge/response/touchlink/scan")] BridgeTouchlinkScan(Value), #[serde(rename = "bridge/response/permit_join")] BridgePermitJoin(Value), #[serde(rename = "bridge/response/networkmap")] BridgeNetworkmap(Value), #[serde(rename = "bridge/config")] BridgeConfig(Value), #[serde(rename = "bridge/response/group/add")] BridgeResponseGroupAdd(Response), #[serde(rename = "bridge/response/group/remove")] BridgeResponseGroupRemove(Response), #[serde(rename = "bridge/response/group/rename")] BridgeResponseGroupRename(Response), #[serde(rename = "bridge/response/group/options")] BridgeResponseGroupOptions(Response), #[serde(rename = "bridge/response/group/members/add")] BridgeGroupMembersAdd(Response), #[serde(rename = "bridge/response/group/members/remove")] BridgeGroupMembersRemove(Response), #[serde(rename = "bridge/response/device/remove")] BridgeDeviceRemove(Response), #[serde(rename = "bridge/response/device/options")] BridgeDeviceOptions(Value), #[serde(rename = "bridge/response/device/configure_reporting")] BridgeDeviceConfigureReporting(Value), #[serde(rename = "bridge/response/device/ota_update/check")] BridgeDeviceOtaUpdateCheck(Value), } #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub enum Endpoint { #[default] Default, #[serde(untagged)] Name(String), #[serde(untagged)] Number(u32), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GroupMemberChange { pub device: String, pub group: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub endpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub skip_disable_reporting: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PermitJoin { pub time: u32, pub device: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DeviceRemove { pub id: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DeviceRemoveResponse { pub id: String, pub block: bool, pub force: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "status", rename_all = "lowercase")] pub enum Response { Ok { data: T, #[serde(default)] transaction: Option, }, Error { error: Value, #[serde(default)] transaction: Option, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupAdd { pub id: Option, pub friendly_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupRemove { pub id: String, #[serde(default)] pub force: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupMemberAddRemove { pub device: String, pub endpoint: u8, pub group: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupRename { pub from: String, pub to: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupOptions { pub from: Value, pub to: Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceRename { pub from: String, pub to: String, #[serde(default)] pub homeassistant_rename: bool, } #[derive(Serialize, Deserialize, Clone, Hash, Debug, Copy)] #[serde(rename_all = "snake_case")] pub enum Availability { Online, Offline, } #[derive(Serialize, Deserialize, Clone, Hash)] #[serde(transparent)] pub struct IeeeAddress(#[serde(deserialize_with = "ieee_address")] u64); impl Debug for IeeeAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "IeeeAddress({:016x})", self.0) } } impl Display for IeeeAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "0x{:016x}", self.0) } } fn ieee_address<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error; let s: &str = Deserialize::deserialize(deserializer)?; let num = u64::from_str_radix(s.trim_start_matches("0x"), 16).map_err(Error::custom)?; Ok(num) } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum BridgeOnlineState { Online, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BridgeState { pub state: BridgeOnlineState, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BridgeEvent { /* FIXME: needs proper mapping */ /* See: /lib/extension/bridge.ts */ pub data: Value, #[serde(rename = "type")] pub event_type: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BridgeLogging { pub level: String, pub message: String, pub topic: Option, } type BridgeGroups = Vec; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Group { pub friendly_name: String, #[serde(default)] pub description: Option, pub id: u32, pub members: Vec, pub scenes: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GroupMember { pub endpoint: u32, pub ieee_address: IeeeAddress, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct EndpointLink { pub endpoint: u32, pub ieee_address: IeeeAddress, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GroupLink { pub id: u32, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Scene { pub id: u32, pub name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BridgeInfo { pub commit: String, pub config: Config, pub config_schema: BridgeConfigSchema, pub coordinator: Coordinator, pub log_level: String, pub network: Network, pub permit_join: bool, pub restart_required: bool, pub version: String, pub zigbee_herdsman: Version, pub zigbee_herdsman_converters: Version, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BridgeConfigSchema { pub definitions: Value, #[serde(default)] pub required: Vec, pub properties: Value, #[serde(rename = "type")] pub config_type: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub advanced: ConfigAdvanced, #[serde(default)] pub availability: Value, #[serde(default)] pub version: Value, pub blocklist: Vec>, pub device_options: Value, pub devices: HashMap, #[serde(default)] pub external_converters: Vec>, pub frontend: Value, pub groups: HashMap, #[serde(with = "crate::serde_util::struct_or_false")] pub homeassistant: Option, pub map_options: Value, pub mqtt: Value, pub ota: Value, pub passlist: Vec>, pub serial: ConfigSerial, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Version { pub version: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Network { pub channel: i64, pub extended_pan_id: Value, pub pan_id: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Coordinator { pub ieee_address: IeeeAddress, /* stict parsing disabled for now, format too volatile between versions */ /* pub meta: CoordinatorMeta, */ pub meta: Value, #[serde(rename = "type")] pub coordinator_type: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigAdvanced { pub adapter_concurrent: Option, pub adapter_delay: Option, pub cache_state: bool, pub cache_state_persistent: bool, pub cache_state_send_on_startup: bool, pub channel: i64, pub elapsed: bool, pub ext_pan_id: Vec, pub homeassistant_legacy_entity_attributes: Option, pub last_seen: String, pub log_debug_namespace_ignore: String, pub log_debug_to_mqtt_frontend: bool, pub log_directory: String, pub log_file: String, pub log_level: String, pub log_namespaced_levels: Value, pub log_output: Vec, pub log_rotation: bool, pub log_symlink_current: bool, pub log_syslog: Value, pub output: String, pub pan_id: i64, pub timestamp_format: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CoordinatorMeta { pub build: i64, pub ezsp: i64, pub major: i64, pub minor: i64, pub patch: i64, pub revision: String, pub special: i64, #[serde(rename = "type")] pub meta_type: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigSerial { pub adapter: Option, pub disable_led: bool, #[serde(default)] pub port: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConfigHomeassistant { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default)] pub experimental_event_entities: Option, #[serde(default)] pub legacy_action_sensor: Option, pub discovery_topic: String, pub status_topic: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupValue { #[serde(default)] pub devices: Vec, pub friendly_name: String, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub enum PowerSource { #[serde(rename = "Unknown")] #[default] Unknown = 0, #[serde(rename = "Mains (single phase)")] MainsSinglePhase = 1, #[serde(rename = "Mains (3 phase)")] MainsThreePhase = 2, #[serde(rename = "Battery")] Battery = 3, #[serde(rename = "DC Source")] DcSource = 4, #[serde(rename = "Emergency mains constantly powered")] EmergencyMainsConstantly = 5, #[serde(rename = "Emergency mains and transfer switch")] EmergencyMainsAndTransferSwitch = 6, } pub type BridgeDevices = Vec; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DeviceType { Coordinator, Router, EndDevice, Unknown, GreenPower, } #[allow(clippy::pub_underscore_fields)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Device { pub description: Option, pub date_code: Option, pub definition: Option, pub disabled: bool, pub endpoints: HashMap, pub friendly_name: String, pub ieee_address: IeeeAddress, pub interview_completed: bool, pub interviewing: bool, pub manufacturer: Option, pub model_id: Option, pub network_address: u16, #[serde(default)] pub power_source: PowerSource, pub software_build_id: Option, pub supported: Option, #[serde(rename = "type")] pub device_type: DeviceType, /* all other fields */ #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default, flatten)] pub __: HashMap, } impl Device { #[must_use] pub fn exposes(&self) -> &[Expose] { self.definition.as_ref().map_or(&[], |def| &def.exposes) } #[must_use] pub fn expose_light(&self) -> Option<&ExposeLight> { self.exposes().iter().find_map(|exp| { if let Expose::Light(light) = exp { Some(light) } else { None } }) } #[must_use] pub fn expose_gradient(&self) -> Option<&ExposeList> { self.exposes().iter().find_map(|exp| { if let Expose::List(grad) = exp { if grad .base .property .as_ref() .is_some_and(|prop| prop == "gradient") { Some(grad) } else { None } } else { None } }) } #[must_use] pub fn expose_action(&self) -> bool { self.exposes().iter().any(|exp| { if let Expose::Enum(ExposeEnum { base, .. }) = exp { base.name.as_deref() == Some("action") } else { false } }) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceDefinition { pub model: String, pub vendor: String, pub description: String, pub exposes: Vec, pub supports_ota: bool, pub options: Vec, #[serde(default)] pub icon: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum Expose { Binary(ExposeBinary), Composite(ExposeComposite), Enum(ExposeEnum), Light(ExposeLight), Lock(ExposeLock), Numeric(ExposeNumeric), Switch(ExposeSwitch), List(ExposeList), /* FIXME: Not modelled yet */ Text(ExposeGeneric), Cover(ExposeGeneric), Fan(ExposeGeneric), Climate(ExposeGeneric), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeGeneric { #[serde(flatten)] pub base: ExposeBase, #[serde(flatten)] pub other: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ExposeCategory { Config, Diagnostic, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeBase { pub name: Option, pub label: Option, #[serde(default)] pub access: u8, pub endpoint: Option, pub property: Option, pub description: Option, #[serde(default)] pub features: Vec, pub category: Option, } impl Expose { #[must_use] pub const fn base(&self) -> &ExposeBase { #[allow(clippy::match_same_arms)] match self { Self::Binary(exp) => &exp.base, Self::Composite(exp) => &exp.base, Self::Enum(exp) => &exp.base, Self::Light(exp) => &exp.base, Self::List(exp) => &exp.base, Self::Lock(exp) => &exp.base, Self::Numeric(exp) => &exp.base, Self::Switch(exp) => &exp.base, Self::Text(exp) => &exp.base, Self::Cover(exp) => &exp.base, Self::Fan(exp) => &exp.base, Self::Climate(exp) => &exp.base, } } #[must_use] pub fn name(&self) -> Option<&str> { self.base().name.as_deref() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeBinary { #[serde(flatten)] pub base: ExposeBase, pub value_off: Value, pub value_on: Value, pub value_toggle: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeComposite { #[serde(flatten)] pub base: ExposeBase, // FIXME } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeEnum { #[serde(flatten)] pub base: ExposeBase, pub values: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeLight { #[serde(flatten)] pub base: ExposeBase, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeLock { #[serde(flatten)] pub base: ExposeBase, } impl ExposeLight { #[must_use] pub fn feature(&self, name: &str) -> Option<&Expose> { self.base .features .iter() .find(|exp| exp.name() == Some(name)) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeList { #[serde(flatten)] pub base: ExposeBase, pub item_type: Box, #[serde(default)] pub length_min: Option, #[serde(default)] pub length_max: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeNumeric { #[serde(flatten)] pub base: ExposeBase, pub unit: Option, pub value_max: Option, pub value_min: Option, pub value_step: Option, #[serde(default)] pub presets: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExposeSwitch { #[serde(flatten)] pub base: ExposeBase, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceEndpoint { pub bindings: Vec, pub configured_reportings: Vec, pub clusters: DeviceEndpointClusters, pub scenes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceEndpointConfiguredReporting { pub attribute: Value, pub cluster: String, pub maximum_report_interval: i64, pub minimum_report_interval: i64, #[serde(default)] pub reportable_change: Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Preset { pub description: String, pub name: String, pub value: Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceEndpointBinding { pub cluster: String, pub target: DeviceEndpointBindingTarget, } // NOTE: definition diverges from z2m, but is more strict #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum DeviceEndpointBindingTarget { Group(GroupLink), Endpoint(EndpointLink), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceEndpointClusters { pub input: Vec, pub output: Vec, }