From b986a649e6fe6e72ea85469d8c3bed06d65737d8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Sep 2024 20:08:40 +0200 Subject: [PATCH 01/49] Start defining and adding various device-classes --- plugwise/devices.py | 115 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 plugwise/devices.py diff --git a/plugwise/devices.py b/plugwise/devices.py new file mode 100644 index 000000000..6ac08ce67 --- /dev/null +++ b/plugwise/devices.py @@ -0,0 +1,115 @@ +"""Plugwise Device classes.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypedDict + + +@dataclass +class AnnaData: + """Plugwise Anna data.""" + + active_preset: str + available_schedules: list[str] + dev_class: str + firmware: str + hardware: str + location: str + mode: str + model: str + model_id: str + name: str + preset_modes: list[str] + select_schedule: str + sensors: AnnaSensor + temperature_offset: SetpointDict + thermostat: ThermostatDict + vendor: str + + +class AnnaSensors(TypedDict, total=False): + """Anna sensors class.""" + + illuminance: float + setpoint: float + setpoint_high: float + setpoint_low: float + temperature: float + + +@dataclass +class SetpointDict: + """Generic setpoint dict. + + Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. + """ + + lower_bound: float + resolution: float + setpoint: float + upper_bound: float + + +class ThermostatDict(TypedDict, total=False): + """Thermostat dict class.""" + + lower_bound: float + resolution: float + setpoint: float + setpoint_high: float + setpoint_low: float + upper_bound: float + + +class OnOffTherm(TypedDict, total=False): + """On-off heater/cooler device class.""" + + binary_sensors: HeaterCentralBinarySensors + dev_class: str + location: str + model: str + name: str + + +class OpenTherm(TypedDict, total=False): + """OpenTherm heater/cooler device class.""" + + available: str + binary_sensors: HeaterCentralBinarySensors + dev_class: str + location: str + max_dhw_temperature: SetpointDict + maximum_boiler_temperature: SetpointDict + model: str + model_id: str + name: str + sensors: HeaterCentralSensors + switches: HeaterCentralSwitches + vendor : str + + +class HeaterCentralBinarySensors(TypedDict, total=False): + """Heater-central binary_sensors class.""" + + cooling_state: bool + dhw_state: bool + flame_state: bool + heating_state: bool + + +class HeaterCentralSensors(TypedDict, total=False): + """Heater-central sensors class.""" + + dhw_temperature: float + intended_boiler_temperature: float + modulation_level: float + outdoor_air_temperature: float + return_temperature: float + water_pressure: float + water_temperature: float + + +class HeaterCentralSwitches(TypedDict, total=False): + """Heater-central switches class.""" + + dhw_cm_switch: bool From 71b5dbd2c716627c5fdd49dfb77d5056e0ec71de Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 20 Oct 2024 12:49:58 +0200 Subject: [PATCH 02/49] Updates --- plugwise/devices.py | 270 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 262 insertions(+), 8 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 6ac08ce67..3a59ef229 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -5,12 +5,129 @@ from typing import TypedDict -@dataclass -class AnnaData: - """Plugwise Anna data.""" +class BaseGateway: + """Plugwise Base Gateway data class.""" + + dev_class: str + firmware: str + location: str + mac_address: str + model: str + name: str + vendor: str + + +class SmileP1Gateway(BaseGateway): + """Plugwise Smile P1 Gateway data class.""" + + binary_sensors: GatewayBinarySensors + hardware: str + model_id: str + + +class SmartEnergyMeter: + """DSMR Energy Meter data class.""" + + available: bool + dev_class: str + location: str + model: str + name: str + sensors: SmartEnergySensors + vendor: str + + +class SmartEnergySensors(TypedDict, total=False): + """DSMR Energy Meter sensors class.""" + electricity_consumed_off_peak_cumulative: float + electricity_consumed_off_peak_interval: int + electricity_consumed_off_peak_point: int + electricity_consumed_peak_cumulative: float + electricity_consumed_peak_interval: int + electricity_consumed_peak_point: int, + electricity_phase_one_consumed: int + electricity_phase_one_produced: int + electricity_phase_three_consumed: int + electricity_phase_three_produced: int + electricity_phase_two_consumed: int + electricity_phase_two_produced: int + electricity_produced_off_peak_cumulative: float + electricity_produced_off_peak_interval: int + electricity_produced_off_peak_point: int + electricity_produced_peak_cumulative: float + electricity_produced_peak_interval: int + electricity_produced_peak_point: int + gas_consumed_cumulative: float + gas_consumed_interval: float + net_electricity_cumulative:float + net_electricity_point: int + voltage_phase_one: float + voltage_phase_three:float + voltage_phase_two: float + + +class StretchGateway(BaseGateway): + """Plugwise Stretch Gateway data class.""" + + zigbee_mac_address: str + + +class SmileThermostatGateway(SmileP1Gateway): + """Plugwise Anna Smile-T Gateway data class.""" + + sensors: GatewaySensors + + +class AdamGateway(SmileThermostatGateway): + """Plugwise Adam HA Gateway data class.""" + + gateway_modes: list[str] + regulation_modes: list[str] + select_gateway_mode: str + select_regulation_mode: str + zigbee_mac_address: str + + +class GatewayBinarySensors + """Gateway binary_sensors class.""" + + plugwise_notification: bool + + +class GatewaySensors + """Gateway sensors class.""" + + outdoor_temperature: float + + +class AnnaData(TypedDict, total=False): + """Plugwise Anna data class.""" + + active_preset: str + available_schedules: list[str] + dev_class: str + firmware: str + hardware: str + location: str + mode: str + model: str + model_id: str + name: str + preset_modes: list[str] + select_schedule: str + sensors: AnnaSensors + temperature_offset: SetpointDict + thermostat: ThermostatDict + vendor: str + + +class AnnaAdamData(TypedDict, total=False): + """Plugwise Anna-connected-to-Adam data class.""" active_preset: str + available: bool available_schedules: list[str] + control_state: str dev_class: str firmware: str hardware: str @@ -21,7 +138,7 @@ class AnnaData: name: str preset_modes: list[str] select_schedule: str - sensors: AnnaSensor + sensors: AnnaSensors temperature_offset: SetpointDict thermostat: ThermostatDict vendor: str @@ -30,6 +147,8 @@ class AnnaData: class AnnaSensors(TypedDict, total=False): """Anna sensors class.""" + cooling_activation_outdoor_temperature: float + cooling_deactivation_threshold: float illuminance: float setpoint: float setpoint_high: float @@ -37,9 +156,8 @@ class AnnaSensors(TypedDict, total=False): temperature: float -@dataclass class SetpointDict: - """Generic setpoint dict. + """Generic setpoint dict class. Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. """ @@ -61,7 +179,7 @@ class ThermostatDict(TypedDict, total=False): upper_bound: float -class OnOffTherm(TypedDict, total=False): +class OnOffTherm: """On-off heater/cooler device class.""" binary_sensors: HeaterCentralBinarySensors @@ -78,8 +196,8 @@ class OpenTherm(TypedDict, total=False): binary_sensors: HeaterCentralBinarySensors dev_class: str location: str - max_dhw_temperature: SetpointDict maximum_boiler_temperature: SetpointDict + max_dhw_temperature: SetpointDict model: str model_id: str name: str @@ -91,16 +209,20 @@ class OpenTherm(TypedDict, total=False): class HeaterCentralBinarySensors(TypedDict, total=False): """Heater-central binary_sensors class.""" + compressor_state: bool + cooling_enabled: bool cooling_state: bool dhw_state: bool flame_state: bool heating_state: bool + secondary_boiler_state: bool class HeaterCentralSensors(TypedDict, total=False): """Heater-central sensors class.""" dhw_temperature: float + domestic_hot_water_setpoint: float intended_boiler_temperature: float modulation_level: float outdoor_air_temperature: float @@ -112,4 +234,136 @@ class HeaterCentralSensors(TypedDict, total=False): class HeaterCentralSwitches(TypedDict, total=False): """Heater-central switches class.""" + cooling_ena_switch: bool dhw_cm_switch: bool + + +@dataclass +class LisaData(TypedDict, total=False): + """Plugwise Lisa data class.""" + + active_preset: str + available: bool + available_schedules: list[str] + binary_sensors: LisaBinarySensors + control_state: str + dev_class: str + firmware: str + hardware: str + location: str + mode: str + model: str + model_id: str + name: str + preset_modes: list[str] + select_schedule: str + sensors: WirelessThermostatBinarySensors + temperature_offset: SetpointDict + thermostat: ThermostatDict + vendor: str + zigbee_mac_address: str + + +class LisaSensors(TypedDict, total=False): + """Lisa sensors class.""" + + battery: int + setpoint: float + setpoint_high: float + setpoint_low: float + temperature: float + + +class WirelessThermostatBinarySensors: + """Lisa sensors class.""" + + low_battery: bool + +@dataclass +class TomParentData(TypedDict, total=False): + """Plugwise Lisa data class.""" + + active_preset: str + available: bool + available_schedules: list[str] + binary_sensors: WirelessThermostatBinarySensors + control_state: str + dev_class: str + firmware: str + hardware: str + location: str + mode: str + model: str + model_id: str + name: str + preset_modes: list[str] + select_schedule: str + sensors: TomSensors + temperature_offset: SetpointDict + thermostat: ThermostatDict + vendor: str + zigbee_mac_address: str + + +@dataclass +class TomChildData(TypedDict, total=False): + """Plugwise Lisa data class.""" + + available: bool + binary_sensors: WirelessThermostatBinarySensors + dev_class: str + firmware: str + hardware: str + location: str + model: str + model_id: str + name: str + sensors: TomSensors + temperature_offset: SetpointDict + vendor: str + zigbee_mac_address: str + + +class TomSensors(TypedDict, total=False): + """Tom sensors class.""" + + battery: int + setpoint: float + setpoint_high: float + setpoint_low: float + temperature: float + temperature_difference: float + valve_position: float + + +class PlugData: + """Plug data class.""" + + available: bool + dev_class: str + firmware: str + # hardware: str + location: str + model: str + model_id: str + name: str + sensors: PlugSensors + switches: PlugSwitches + vendor: str + zigbee_mac_address: str + + +class PlugSensors: + """Plug sensors class.""" + + electricity_consumed: float + electricity_consumed_interval: float + electricity_produced: float + electricity_produced_interval: float + + +class PlugSwitches(TypedDict, total=False): + """Plug switches class.""" + + lock: bool + relay: bool \ No newline at end of file From 92c388552f3299650c07c1cd9073ecb04bfb2181 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Wed, 4 Dec 2024 20:28:27 +0100 Subject: [PATCH 03/49] More updates --- plugwise/devices.py | 271 +++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 143 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 3a59ef229..068164e81 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -5,6 +5,7 @@ from typing import TypedDict +@dataclass class BaseGateway: """Plugwise Base Gateway data class.""" @@ -17,6 +18,7 @@ class BaseGateway: vendor: str +@dataclass class SmileP1Gateway(BaseGateway): """Plugwise Smile P1 Gateway data class.""" @@ -25,6 +27,46 @@ class SmileP1Gateway(BaseGateway): model_id: str +@dataclass +class StretchGateway(BaseGateway): + """Plugwise Stretch Gateway data class.""" + + zigbee_mac_address: str + + +@dataclass +class SmileThermostatGateway(SmileP1Gateway): + """Plugwise Anna Smile-T Gateway data class.""" + + sensors: GatewaySensors + + +@dataclass +class GatewayBinarySensors: + """Gateway binary_sensors class.""" + + plugwise_notification: bool + + +@dataclass +class GatewaySensors: + """Gateway sensors class.""" + + outdoor_temperature: float + + +@dataclass +class AdamGateway(SmileThermostatGateway): + """Plugwise Adam HA Gateway data class.""" + + gateway_modes: list[str] + regulation_modes: list[str] + select_gateway_mode: str + select_regulation_mode: str + zigbee_mac_address: str + + +@dataclass class SmartEnergyMeter: """DSMR Energy Meter data class.""" @@ -39,6 +81,7 @@ class SmartEnergyMeter: class SmartEnergySensors(TypedDict, total=False): """DSMR Energy Meter sensors class.""" + electricity_consumed_off_peak_cumulative: float electricity_consumed_off_peak_interval: int electricity_consumed_off_peak_point: int @@ -66,40 +109,6 @@ class SmartEnergySensors(TypedDict, total=False): voltage_phase_two: float -class StretchGateway(BaseGateway): - """Plugwise Stretch Gateway data class.""" - - zigbee_mac_address: str - - -class SmileThermostatGateway(SmileP1Gateway): - """Plugwise Anna Smile-T Gateway data class.""" - - sensors: GatewaySensors - - -class AdamGateway(SmileThermostatGateway): - """Plugwise Adam HA Gateway data class.""" - - gateway_modes: list[str] - regulation_modes: list[str] - select_gateway_mode: str - select_regulation_mode: str - zigbee_mac_address: str - - -class GatewayBinarySensors - """Gateway binary_sensors class.""" - - plugwise_notification: bool - - -class GatewaySensors - """Gateway sensors class.""" - - outdoor_temperature: float - - class AnnaData(TypedDict, total=False): """Plugwise Anna data class.""" @@ -121,13 +130,43 @@ class AnnaData(TypedDict, total=False): vendor: str -class AnnaAdamData(TypedDict, total=False): - """Plugwise Anna-connected-to-Adam data class.""" +class AnnaSensors(TypedDict, total=False): + """Anna sensors class.""" + cooling_activation_outdoor_temperature: float + cooling_deactivation_threshold: float + illuminance: float + setpoint: float + setpoint_high: float + setpoint_low: float + temperature: float + + +@dataclass +class ThermoZone: + """Plugwise Adam ThermoZone data class.""" active_preset: str - available: bool available_schedules: list[str] + climate_mode: str control_state: str + preset_modes: list[str] + select_schedule: str + sensors: ThermoZoneSensors + thermostat: ThermostatDict + + +class ThermoZoneSensors(TypedDict, total=False): + """ThermoZone sensors class.""" + + electricity_consumed: float + electricity_produced: float + temperature: float + + +class AnnaAdamData(TypedDict, total=False): + """Plugwise Anna-connected-to-Adam data class.""" + + available: bool dev_class: str firmware: str hardware: str @@ -136,26 +175,54 @@ class AnnaAdamData(TypedDict, total=False): model: str model_id: str name: str - preset_modes: list[str] - select_schedule: str sensors: AnnaSensors temperature_offset: SetpointDict - thermostat: ThermostatDict vendor: str -class AnnaSensors(TypedDict, total=False): - """Anna sensors class.""" +class JipLisaTomData(TypedDict, total=False): + """JipLisaTomData data class. - cooling_activation_outdoor_temperature: float - cooling_deactivation_threshold: float - illuminance: float + Covering Plugwise Jip, Lisa and Tom/Floor devices. + """ + + available: bool + binary_sensors: WirelessThermostatBinarySensors + dev_class: str + firmware: str + hardware: str + location: str + mode: str + model: str + model_id: str + name: str + sensors: JipLisaTomSensors + temperature_offset: SetpointDict + vendor: str + zigbee_mac_address: str + + +class JipLisaTomSensors(TypedDict, total=False): + """Tom sensors class.""" + + battery: int + humidity: int setpoint: float setpoint_high: float setpoint_low: float temperature: float + temperature_difference: float + valve_position: float +@dataclass +class WirelessThermostatBinarySensors: + """Lisa sensors class.""" + + low_battery: bool + + +@dataclass class SetpointDict: """Generic setpoint dict class. @@ -179,6 +246,7 @@ class ThermostatDict(TypedDict, total=False): upper_bound: float +@dataclass class OnOffTherm: """On-off heater/cooler device class.""" @@ -239,103 +307,6 @@ class HeaterCentralSwitches(TypedDict, total=False): @dataclass -class LisaData(TypedDict, total=False): - """Plugwise Lisa data class.""" - - active_preset: str - available: bool - available_schedules: list[str] - binary_sensors: LisaBinarySensors - control_state: str - dev_class: str - firmware: str - hardware: str - location: str - mode: str - model: str - model_id: str - name: str - preset_modes: list[str] - select_schedule: str - sensors: WirelessThermostatBinarySensors - temperature_offset: SetpointDict - thermostat: ThermostatDict - vendor: str - zigbee_mac_address: str - - -class LisaSensors(TypedDict, total=False): - """Lisa sensors class.""" - - battery: int - setpoint: float - setpoint_high: float - setpoint_low: float - temperature: float - - -class WirelessThermostatBinarySensors: - """Lisa sensors class.""" - - low_battery: bool - -@dataclass -class TomParentData(TypedDict, total=False): - """Plugwise Lisa data class.""" - - active_preset: str - available: bool - available_schedules: list[str] - binary_sensors: WirelessThermostatBinarySensors - control_state: str - dev_class: str - firmware: str - hardware: str - location: str - mode: str - model: str - model_id: str - name: str - preset_modes: list[str] - select_schedule: str - sensors: TomSensors - temperature_offset: SetpointDict - thermostat: ThermostatDict - vendor: str - zigbee_mac_address: str - - -@dataclass -class TomChildData(TypedDict, total=False): - """Plugwise Lisa data class.""" - - available: bool - binary_sensors: WirelessThermostatBinarySensors - dev_class: str - firmware: str - hardware: str - location: str - model: str - model_id: str - name: str - sensors: TomSensors - temperature_offset: SetpointDict - vendor: str - zigbee_mac_address: str - - -class TomSensors(TypedDict, total=False): - """Tom sensors class.""" - - battery: int - setpoint: float - setpoint_high: float - setpoint_low: float - temperature: float - temperature_difference: float - valve_position: float - - class PlugData: """Plug data class.""" @@ -353,6 +324,7 @@ class PlugData: zigbee_mac_address: str +@dataclass class PlugSensors: """Plug sensors class.""" @@ -362,8 +334,21 @@ class PlugSensors: electricity_produced_interval: float +@dataclass class PlugSwitches(TypedDict, total=False): """Plug switches class.""" lock: bool - relay: bool \ No newline at end of file + relay: bool + + +class PlugwiseP1: + """Plugwise P1 data class.""" + data: dict[str, SmileP1Gateway|SmartEnergyMeter] + +class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): + """Plugwise Anna data class.""" + + +class Adam(AdamGateway, AnnaAdamData, JipLisaTomData, ThermoZone, PlugData, OnOffTherm, OpenTherm): + """Plugwise Anna data class.""" \ No newline at end of file From af57be659324451b83aa56a81d49c7e347574911 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 28 Sep 2025 10:41:19 +0200 Subject: [PATCH 04/49] Add comments --- plugwise/devices.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 068164e81..defab51e0 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -206,10 +206,10 @@ class JipLisaTomSensors(TypedDict, total=False): """Tom sensors class.""" battery: int - humidity: int - setpoint: float - setpoint_high: float - setpoint_low: float + humidity: int # Jip only + setpoint: float # heat or cool + setpoint_high: float # heat_cool + setpoint_low: float # heat_cool temperature: float temperature_difference: float valve_position: float @@ -240,9 +240,9 @@ class ThermostatDict(TypedDict, total=False): lower_bound: float resolution: float - setpoint: float - setpoint_high: float - setpoint_low: float + setpoint: float # heat or cool + setpoint_high: float # heat_cool + setpoint_low: float # heat_cool upper_bound: float From c95c7b294039565dc912c88740b2f69365267041 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 28 Sep 2025 11:56:19 +0200 Subject: [PATCH 05/49] Updating... --- plugwise/devices.py | 64 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index defab51e0..b58e9e431 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -22,7 +22,7 @@ class BaseGateway: class SmileP1Gateway(BaseGateway): """Plugwise Smile P1 Gateway data class.""" - binary_sensors: GatewayBinarySensors + binary_sensors: GatewayBinarySensors # Not for legacy? hardware: str model_id: str @@ -45,14 +45,14 @@ class SmileThermostatGateway(SmileP1Gateway): class GatewayBinarySensors: """Gateway binary_sensors class.""" - plugwise_notification: bool + plugwise_notification: bool # None for some? @dataclass class GatewaySensors: """Gateway sensors class.""" - outdoor_temperature: float + outdoor_temperature: float | None # None when not enabled? @dataclass @@ -79,34 +79,68 @@ class SmartEnergyMeter: vendor: str -class SmartEnergySensors(TypedDict, total=False): - """DSMR Energy Meter sensors class.""" +@dataclass +class SmartEnergySensors: + """DSMR Energy Meter sensors class (P1 v4).""" electricity_consumed_off_peak_cumulative: float electricity_consumed_off_peak_interval: int electricity_consumed_off_peak_point: int electricity_consumed_peak_cumulative: float electricity_consumed_peak_interval: int - electricity_consumed_peak_point: int, + electricity_consumed_peak_point: int electricity_phase_one_consumed: int electricity_phase_one_produced: int - electricity_phase_three_consumed: int - electricity_phase_three_produced: int - electricity_phase_two_consumed: int - electricity_phase_two_produced: int + electricity_phase_three_consumed: int | None + electricity_phase_three_produced: int | None + electricity_phase_two_consumed: int | None + electricity_phase_two_produced: int | None electricity_produced_off_peak_cumulative: float electricity_produced_off_peak_interval: int electricity_produced_off_peak_point: int electricity_produced_peak_cumulative: float electricity_produced_peak_interval: int electricity_produced_peak_point: int - gas_consumed_cumulative: float - gas_consumed_interval: float + gas_consumed_cumulative: float | None + gas_consumed_interval: float | None + net_electricity_cumulative:float + net_electricity_point: int + voltage_phase_one: float | None + voltage_phase_three:float | None + voltage_phase_two: float | None + + +@dataclass +class SmartEnergyLegacyMeter: + """Legacy DSMR Energy Meter data class.""" + + available: bool + dev_class: str + location: str + model: str + name: str + sensors: SmartEnergyLegacySensors + vendor: str + + +@dataclass +class SmartEnergyLegacySensors: + """Legacy DSMR Energy Meter sensors class (P1 v2).""" + + electricity_consumed_off_peak_cumulative: float + electricity_consumed_off_peak_interval: int + electricity_consumed_peak_cumulative: float + electricity_consumed_peak_interval: int + electricity_consumed_point: int + electricity_produced_off_peak_cumulative: float + electricity_produced_off_peak_interval: int + electricity_produced_peak_cumulative: float + electricity_produced_peak_interval: int + electricity_produced_point: int + gas_consumed_cumulative: float | None + gas_consumed_interval: float | None net_electricity_cumulative:float net_electricity_point: int - voltage_phase_one: float - voltage_phase_three:float - voltage_phase_two: float class AnnaData(TypedDict, total=False): From 8ae36eae4fbcbcb4a26af1994e9a1c6864c178e6 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 28 Sep 2025 10:13:04 +0000 Subject: [PATCH 06/49] fixup: device_classes Python code fixed using ruff --- plugwise/devices.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index b58e9e431..07a1a3daa 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -1,4 +1,5 @@ """Plugwise Device classes.""" + from __future__ import annotations from dataclasses import dataclass @@ -103,10 +104,10 @@ class SmartEnergySensors: electricity_produced_peak_point: int gas_consumed_cumulative: float | None gas_consumed_interval: float | None - net_electricity_cumulative:float + net_electricity_cumulative: float net_electricity_point: int voltage_phase_one: float | None - voltage_phase_three:float | None + voltage_phase_three: float | None voltage_phase_two: float | None @@ -139,7 +140,7 @@ class SmartEnergyLegacySensors: electricity_produced_point: int gas_consumed_cumulative: float | None gas_consumed_interval: float | None - net_electricity_cumulative:float + net_electricity_cumulative: float net_electricity_point: int @@ -179,6 +180,7 @@ class AnnaSensors(TypedDict, total=False): @dataclass class ThermoZone: """Plugwise Adam ThermoZone data class.""" + active_preset: str available_schedules: list[str] climate_mode: str @@ -238,7 +240,7 @@ class JipLisaTomData(TypedDict, total=False): class JipLisaTomSensors(TypedDict, total=False): """Tom sensors class.""" - + battery: int humidity: int # Jip only setpoint: float # heat or cool @@ -259,7 +261,7 @@ class WirelessThermostatBinarySensors: @dataclass class SetpointDict: """Generic setpoint dict class. - + Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. """ @@ -305,7 +307,7 @@ class OpenTherm(TypedDict, total=False): name: str sensors: HeaterCentralSensors switches: HeaterCentralSwitches - vendor : str + vendor: str class HeaterCentralBinarySensors(TypedDict, total=False): @@ -378,11 +380,21 @@ class PlugSwitches(TypedDict, total=False): class PlugwiseP1: """Plugwise P1 data class.""" - data: dict[str, SmileP1Gateway|SmartEnergyMeter] + + data: dict[str, SmileP1Gateway | SmartEnergyMeter] + class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): """Plugwise Anna data class.""" -class Adam(AdamGateway, AnnaAdamData, JipLisaTomData, ThermoZone, PlugData, OnOffTherm, OpenTherm): - """Plugwise Anna data class.""" \ No newline at end of file +class Adam( + AdamGateway, + AnnaAdamData, + JipLisaTomData, + ThermoZone, + PlugData, + OnOffTherm, + OpenTherm, +): + """Plugwise Anna data class.""" From 7cc287a943261f73fe9c0bf946b144fc18415cb8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 28 Sep 2025 12:20:42 +0200 Subject: [PATCH 07/49] Continue updating --- plugwise/devices.py | 175 ++++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 81 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 07a1a3daa..4432e58b3 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -111,18 +111,6 @@ class SmartEnergySensors: voltage_phase_two: float | None -@dataclass -class SmartEnergyLegacyMeter: - """Legacy DSMR Energy Meter data class.""" - - available: bool - dev_class: str - location: str - model: str - name: str - sensors: SmartEnergyLegacySensors - vendor: str - @dataclass class SmartEnergyLegacySensors: @@ -144,36 +132,36 @@ class SmartEnergyLegacySensors: net_electricity_point: int -class AnnaData(TypedDict, total=False): - """Plugwise Anna data class.""" +@dataclass +class AnnaData: + """Plugwise Anna data class, also for legacy Anna.""" - active_preset: str + active_preset: str | None available_schedules: list[str] + climate_mode: str + control_state: str dev_class: str firmware: str hardware: str location: str - mode: str model: str - model_id: str name: str - preset_modes: list[str] - select_schedule: str + preset_modes: list[str] | None + select_schedule: str | None sensors: AnnaSensors - temperature_offset: SetpointDict + temperature_offset: SetpointDict | None # not for legacy thermostat: ThermostatDict vendor: str -class AnnaSensors(TypedDict, total=False): +@dataclass +class AnnaSensors: """Anna sensors class.""" - cooling_activation_outdoor_temperature: float - cooling_deactivation_threshold: float illuminance: float - setpoint: float - setpoint_high: float - setpoint_low: float + setpoint: float | None + setpoint_high: float | None + setpoint_low: float | None temperature: float @@ -181,54 +169,56 @@ class AnnaSensors(TypedDict, total=False): class ThermoZone: """Plugwise Adam ThermoZone data class.""" - active_preset: str + active_preset: str | None available_schedules: list[str] climate_mode: str control_state: str + dev_class: str + model: str + name: str preset_modes: list[str] select_schedule: str sensors: ThermoZoneSensors thermostat: ThermostatDict + thermostats: ThermostatsDict + vendor: str -class ThermoZoneSensors(TypedDict, total=False): +@dataclass +class ThermoZoneSensors: """ThermoZone sensors class.""" - electricity_consumed: float - electricity_produced: float + electricity_consumed: float | None # only with Plug(s) in the zone + electricity_produced: float | None # only with Plug(s) in the zone temperature: float -class AnnaAdamData(TypedDict, total=False): +@dataclass +class AnnaAdamData: """Plugwise Anna-connected-to-Adam data class.""" - available: bool dev_class: str - firmware: str - hardware: str location: str - mode: str model: str model_id: str name: str sensors: AnnaSensors - temperature_offset: SetpointDict vendor: str -class JipLisaTomData(TypedDict, total=False): +@dataclass +class JipLisaTomData: """JipLisaTomData data class. Covering Plugwise Jip, Lisa and Tom/Floor devices. """ available: bool - binary_sensors: WirelessThermostatBinarySensors + binary_sensors: WirelessThermostatBinarySensors | None # Not for AC powered Lisa/Tom dev_class: str firmware: str hardware: str location: str - mode: str model: str model_id: str name: str @@ -238,17 +228,18 @@ class JipLisaTomData(TypedDict, total=False): zigbee_mac_address: str -class JipLisaTomSensors(TypedDict, total=False): +@dataclass +class JipLisaTomSensors: """Tom sensors class.""" - battery: int - humidity: int # Jip only - setpoint: float # heat or cool - setpoint_high: float # heat_cool - setpoint_low: float # heat_cool + battery: int | None # not when AC powered, Lisa/Tom + humidity: int | None # Jip only + setpoint: float | None # heat or cool + setpoint_high: float | None # heat_cool + setpoint_low: float | None # heat_cool temperature: float - temperature_difference: float - valve_position: float + temperature_difference: float | None # Tom only + valve_position: float | None # Tom only @dataclass @@ -271,17 +262,26 @@ class SetpointDict: upper_bound: float -class ThermostatDict(TypedDict, total=False): +@dataclass +class ThermostatDict: """Thermostat dict class.""" lower_bound: float resolution: float - setpoint: float # heat or cool - setpoint_high: float # heat_cool - setpoint_low: float # heat_cool + setpoint: float | None # heat or cool + setpoint_high: float | None # heat_cool + setpoint_low: float| None # heat_cool upper_bound: float +@dataclass +class ThermostatsDict: + """Thermostats dict class.""" + + primary: list[str] + secondary: list[str] + + @dataclass class OnOffTherm: """On-off heater/cooler device class.""" @@ -293,68 +293,72 @@ class OnOffTherm: name: str -class OpenTherm(TypedDict, total=False): +@dataclass +class OpenTherm: """OpenTherm heater/cooler device class.""" available: str binary_sensors: HeaterCentralBinarySensors dev_class: str location: str - maximum_boiler_temperature: SetpointDict - max_dhw_temperature: SetpointDict + maximum_boiler_temperature: SetpointDict | None + max_dhw_temperature: SetpointDict | None model: str - model_id: str + model_id: str | None name: str sensors: HeaterCentralSensors switches: HeaterCentralSwitches vendor: str -class HeaterCentralBinarySensors(TypedDict, total=False): +@dataclass +class HeaterCentralBinarySensors: """Heater-central binary_sensors class.""" - compressor_state: bool - cooling_enabled: bool - cooling_state: bool + compressor_state: bool | None + cooling_enabled: bool | None + cooling_state: bool | None dhw_state: bool flame_state: bool heating_state: bool - secondary_boiler_state: bool + secondary_boiler_state: bool | None -class HeaterCentralSensors(TypedDict, total=False): +@dataclass +class HeaterCentralSensors: """Heater-central sensors class.""" - dhw_temperature: float - domestic_hot_water_setpoint: float - intended_boiler_temperature: float - modulation_level: float - outdoor_air_temperature: float + dhw_temperature: float | None + domestic_hot_water_setpoint: float | None + intended_boiler_temperature: float | None + modulation_level: float | None + outdoor_air_temperature: float | None return_temperature: float - water_pressure: float + water_pressure: float | None water_temperature: float -class HeaterCentralSwitches(TypedDict, total=False): +@dataclass +class HeaterCentralSwitches: """Heater-central switches class.""" - cooling_ena_switch: bool + cooling_ena_switch: bool | None dhw_cm_switch: bool @dataclass class PlugData: - """Plug data class.""" + """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - available: bool + available: bool | None dev_class: str - firmware: str - # hardware: str + firmware: str | None + hardware: str | None location: str - model: str + model: str | None model_id: str name: str - sensors: PlugSensors + sensors: PlugSensors | None switches: PlugSwitches vendor: str zigbee_mac_address: str @@ -364,29 +368,30 @@ class PlugData: class PlugSensors: """Plug sensors class.""" - electricity_consumed: float + electricity_consumed: float | None electricity_consumed_interval: float - electricity_produced: float - electricity_produced_interval: float + electricity_produced: float | None + electricity_produced_interval: float | None @dataclass -class PlugSwitches(TypedDict, total=False): +class PlugSwitches: """Plug switches class.""" - lock: bool + lock: bool | None relay: bool class PlugwiseP1: """Plugwise P1 data class.""" - data: dict[str, SmileP1Gateway | SmartEnergyMeter] + data: dict[str, SmileP1Gateway | SmartEnergyMeter | SmartEnergyLegacySensors] class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): """Plugwise Anna data class.""" + data: dict[str, SmileThermostatGateway | OnOffTherm | OpenTherm | AnnaData] class Adam( AdamGateway, @@ -398,3 +403,11 @@ class Adam( OpenTherm, ): """Plugwise Anna data class.""" + + data: dict[str, AdamGateway | OnOffTherm | OpenTherm | AnnaAdamData | JipLisaTomData | ThermoZone | PlugData] + + +class Stretch: + """Plugwise Stretch data class.""" + + data: dict[str, StretchGateway | PlugData] From 7580d29992e62ab1b190b9b19d1d4b69a76dc92e Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 29 Sep 2025 08:54:27 +0000 Subject: [PATCH 08/49] fixup: device_classes Python code fixed using ruff --- plugwise/devices.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 4432e58b3..aef33d503 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -111,7 +111,6 @@ class SmartEnergySensors: voltage_phase_two: float | None - @dataclass class SmartEnergyLegacySensors: """Legacy DSMR Energy Meter sensors class (P1 v2).""" @@ -214,7 +213,9 @@ class JipLisaTomData: """ available: bool - binary_sensors: WirelessThermostatBinarySensors | None # Not for AC powered Lisa/Tom + binary_sensors: ( + WirelessThermostatBinarySensors | None + ) # Not for AC powered Lisa/Tom dev_class: str firmware: str hardware: str @@ -238,7 +239,7 @@ class JipLisaTomSensors: setpoint_high: float | None # heat_cool setpoint_low: float | None # heat_cool temperature: float - temperature_difference: float | None # Tom only + temperature_difference: float | None # Tom only valve_position: float | None # Tom only @@ -270,7 +271,7 @@ class ThermostatDict: resolution: float setpoint: float | None # heat or cool setpoint_high: float | None # heat_cool - setpoint_low: float| None # heat_cool + setpoint_low: float | None # heat_cool upper_bound: float @@ -393,6 +394,7 @@ class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): data: dict[str, SmileThermostatGateway | OnOffTherm | OpenTherm | AnnaData] + class Adam( AdamGateway, AnnaAdamData, @@ -404,7 +406,16 @@ class Adam( ): """Plugwise Anna data class.""" - data: dict[str, AdamGateway | OnOffTherm | OpenTherm | AnnaAdamData | JipLisaTomData | ThermoZone | PlugData] + data: dict[ + str, + AdamGateway + | OnOffTherm + | OpenTherm + | AnnaAdamData + | JipLisaTomData + | ThermoZone + | PlugData, + ] class Stretch: From fd6bc5b3e0ac66a8e7212450046c799ac738e46b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 11:43:04 +0200 Subject: [PATCH 09/49] Try using the added dataclasses --- plugwise/constants.py | 83 ++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 1ddd37c5f..2570a7ef5 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -6,6 +6,22 @@ import logging from typing import Final, Literal, TypedDict, get_args +from plugwise.devices import ( + AdamGateway, + AnnaAdamData, + AnnaData, + JipLisaTomData, + PlugData, + OnOffTherm, + OpenTherm, + SmartEnergyLegacySensors, + SmartEnergyMeter, + SmileP1Gateway, + SmileThermostatGateway, + StretchGateway, + ThermoZone, +) + LOGGER = logging.getLogger(__name__) # Copied homeassistant.consts @@ -524,66 +540,29 @@ class ActuatorData(TypedDict, total=False): upper_bound: float -class GwEntityData(TypedDict, total=False): +class GwEntityData( + AdamGateway, + AnnaAdamData, + AnnaData, + JipLisaTomData, + PlugData, + OnOffTherm, + OpenTherm, + SmartEnergyLegacySensors, + SmartEnergyMeter, + SmileP1Gateway, + SmileThermostatGateway, + StretchGateway, + ThermoZone, +): """The Gateway Entity data class. Covering the collected output-data per device or location. """ - # Appliance base data - dev_class: str - firmware: str - hardware: str - location: str - mac_address: str - members: list[str] - model: str - model_id: str | None - name: str - vendor: str - zigbee_mac_address: str - # For temporary use cooling_enabled: bool domestic_hot_water_setpoint: float elga_status_code: int c_heating_state: bool thermostat_supports_cooling: bool - - # Device availability - available: bool | None - - # Loria - select_dhw_mode: str - dhw_modes: list[str] - - # Gateway - gateway_modes: list[str] - notifications: dict[str, dict[str, str]] - regulation_modes: list[str] - select_gateway_mode: str - select_regulation_mode: str - - # Thermostat-related - select_zone_profile: str - thermostats: dict[str, list[str]] - zone_profiles: list[str] - # Presets: - active_preset: str | None - preset_modes: list[str] | None - # Schedules: - available_schedules: list[str] - select_schedule: str | None - - climate_mode: str - # Extra for Adam Master Thermostats - control_state: str - - # Dict-types - binary_sensors: SmileBinarySensors - max_dhw_temperature: ActuatorData - maximum_boiler_temperature: ActuatorData - sensors: SmileSensors - switches: SmileSwitches - temperature_offset: ActuatorData - thermostat: ActuatorData From 31803681810316da5111f4abdd5bd2e78f30e36e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 11:47:08 +0200 Subject: [PATCH 10/49] Clean up --- plugwise/devices.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index aef33d503..ff40ffb69 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TypedDict @dataclass From 159bfedc34d5852bfd279f920c7e96a515dd6cb5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 11:48:04 +0200 Subject: [PATCH 11/49] Try 2 --- plugwise/constants.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 2570a7ef5..d49459424 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -3,10 +3,11 @@ from __future__ import annotations from collections import namedtuple +from dataclasses import dataclass import logging from typing import Final, Literal, TypedDict, get_args -from plugwise.devices import ( +from .devices import ( AdamGateway, AnnaAdamData, AnnaData, @@ -540,6 +541,7 @@ class ActuatorData(TypedDict, total=False): upper_bound: float +@dataclass class GwEntityData( AdamGateway, AnnaAdamData, @@ -551,7 +553,7 @@ class GwEntityData( SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, - SmileThermostatGateway, + #SmileThermostatGateway, StretchGateway, ThermoZone, ): From 34e8dee05b3ff0ada8f963fbc133f0c35097bcf2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 12:04:45 +0200 Subject: [PATCH 12/49] Clean up 2 --- plugwise/constants.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index d49459424..947484565 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -7,18 +7,17 @@ import logging from typing import Final, Literal, TypedDict, get_args -from .devices import ( +from plugwise.devices import ( AdamGateway, AnnaAdamData, - AnnaData, + AnnaData, JipLisaTomData, - PlugData, OnOffTherm, OpenTherm, + PlugData, SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, - SmileThermostatGateway, StretchGateway, ThermoZone, ) @@ -545,7 +544,7 @@ class ActuatorData(TypedDict, total=False): class GwEntityData( AdamGateway, AnnaAdamData, - AnnaData, + AnnaData, JipLisaTomData, PlugData, OnOffTherm, @@ -553,7 +552,6 @@ class GwEntityData( SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, - #SmileThermostatGateway, StretchGateway, ThermoZone, ): From 01fa7d519d41dafac0c87a1acc7310fedc931ede Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 12:14:10 +0200 Subject: [PATCH 13/49] Disable unused classes --- plugwise/devices.py | 78 ++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index ff40ffb69..368250ece 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -382,42 +382,42 @@ class PlugSwitches: relay: bool -class PlugwiseP1: - """Plugwise P1 data class.""" - - data: dict[str, SmileP1Gateway | SmartEnergyMeter | SmartEnergyLegacySensors] - - -class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): - """Plugwise Anna data class.""" - - data: dict[str, SmileThermostatGateway | OnOffTherm | OpenTherm | AnnaData] - - -class Adam( - AdamGateway, - AnnaAdamData, - JipLisaTomData, - ThermoZone, - PlugData, - OnOffTherm, - OpenTherm, -): - """Plugwise Anna data class.""" - - data: dict[ - str, - AdamGateway - | OnOffTherm - | OpenTherm - | AnnaAdamData - | JipLisaTomData - | ThermoZone - | PlugData, - ] - - -class Stretch: - """Plugwise Stretch data class.""" - - data: dict[str, StretchGateway | PlugData] +# class PlugwiseP1: +# """Plugwise P1 data class.""" +# +# data: dict[str, SmileP1Gateway | SmartEnergyMeter | SmartEnergyLegacySensors] + + +# class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): +# """Plugwise Anna data class.""" +# +# data: dict[str, SmileThermostatGateway | OnOffTherm | OpenTherm | AnnaData] + + +# class Adam( +# AdamGateway, +# AnnaAdamData, +# JipLisaTomData, +# ThermoZone, +# PlugData, +# OnOffTherm, +# OpenTherm, +#): +# """Plugwise Anna data class.""" +# +# data: dict[ +# str, +# AdamGateway +# | OnOffTherm +# | OpenTherm +# | AnnaAdamData +# | JipLisaTomData +# | ThermoZone +# | PlugData, +# ] + + +# class Stretch: +# """Plugwise Stretch data class.""" +# +# data: dict[str, StretchGateway | PlugData] From ebd9d9665bb3635935e8def691a0e704e0cbd8a1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 12:17:36 +0200 Subject: [PATCH 14/49] Ruff-fix --- plugwise/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 368250ece..cfea922de 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -402,7 +402,7 @@ class PlugSwitches: # PlugData, # OnOffTherm, # OpenTherm, -#): +# ): # """Plugwise Anna data class.""" # # data: dict[ From bbacccee6e6244168ada6ce9f7abd5332b5f3c4c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:10:15 +0200 Subject: [PATCH 15/49] Try 3 --- plugwise/__init__.py | 22 ++++++++++++++--- plugwise/constants.py | 53 ++++++++++++++++++++++++++++++---------- plugwise/legacy/smile.py | 7 ++++-- plugwise/smile.py | 7 ++++-- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index bf4f3b04b..edaba8230 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -8,6 +8,8 @@ from typing import cast from plugwise.constants import ( + ADAM, + ANNA, DEFAULT_LEGACY_TIMEOUT, DEFAULT_PORT, DEFAULT_TIMEOUT, @@ -16,12 +18,16 @@ LOGGER, MODULES, NONE, + SMILE_P1, SMILES, STATE_OFF, STATE_ON, STATUS, SYSTEM, - GwEntityData, + PlugwiseAdamData, + PlugwiseAnnaData, + PlugwiseP1Data, + PlugwiseStretchData, ThermoLoc, ) from plugwise.exceptions import ( @@ -326,9 +332,17 @@ async def _smile_detect_legacy( self.smile.legacy = True return return_model - async def async_update(self) -> dict[str, GwEntityData]: - """Update the Plughwise Gateway entities and their data and states.""" - data: dict[str, GwEntityData] = {} + async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: + """Update the Plugwise Gateway entities and their data and states.""" + if self.smile.type == ANNA: + data: dict[str, PlugwiseAnnaData] = {} + if self.smile.type == ADAM: + data: dict[str, PlugwiseAdamData] = {} + if self.smile.type == SMILE_P1: + data: dict[str, PlugwiseP1Data] = {} + if self.smile.type == "Stretch": + data: dict[str, PlugwiseStretchData] = {} + try: data = await self._smile_api.async_update() except (DataMissingError, KeyError) as err: diff --git a/plugwise/constants.py b/plugwise/constants.py index 947484565..5bfa7d3a5 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -18,6 +18,7 @@ SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, + SmileThermostatGateway, StretchGateway, ThermoZone, ) @@ -541,28 +542,54 @@ class ActuatorData(TypedDict, total=False): @dataclass -class GwEntityData( +class GwEntityData: + """The base Gateway Entity data class.""" + + # For temporary use + cooling_enabled: bool + domestic_hot_water_setpoint: float + elga_status_code: int + c_heating_state: bool + thermostat_supports_cooling: bool + + +@dataclass +class PlugwiseAnnaData( + AnnaData, + GwEntityData, + OnOffTherm, + OpenTherm, + SmileThermostatGateway, +): + """The Plugwise Anna Data class.""" + + +@dataclass +class PlugwiseAdamData( AdamGateway, AnnaAdamData, - AnnaData, + GwEntityData, JipLisaTomData, PlugData, OnOffTherm, OpenTherm, + ThermoZone, +): + """The Plugwise Adam Data class.""" + + +@dataclass +class PlugwiseP1Data( SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, - StretchGateway, - ThermoZone, ): - """The Gateway Entity data class. + """The Plugwise P1 Data class.""" - Covering the collected output-data per device or location. - """ - # For temporary use - cooling_enabled: bool - domestic_hot_water_setpoint: float - elga_status_code: int - c_heating_state: bool - thermostat_supports_cooling: bool +@dataclass +class PlugwiseStretchData( + PlugData, + StretchGateway, +): + """The Plugwise Stretch Data class.""" diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 54a21c15d..abc15ed5e 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -20,7 +20,10 @@ RULES, STATE_OFF, STATE_ON, - GwEntityData, + PlugwiseAdamData, + PlugwiseAnnaData, + PlugwiseP1Data, + PlugwiseStretchData, ThermoLoc, ) from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError @@ -87,7 +90,7 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update(self) -> dict[str, GwEntityData]: + async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: """Perform an full update update at day-change: re-collect all gateway entities and their data and states. Otherwise perform an incremental update: only collect the entities updated data and states. diff --git a/plugwise/smile.py b/plugwise/smile.py index 22010af5c..56109ce90 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -25,7 +25,10 @@ RULES, STATE_OFF, STATE_ON, - GwEntityData, + PlugwiseAdamData, + PlugwiseAnnaData, + PlugwiseP1Data, + PlugwiseStretchData, SwitchType, ThermoLoc, ) @@ -120,7 +123,7 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update(self) -> dict[str, GwEntityData]: + async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: """Perform an full update: re-collect all gateway entities and their data and states. Any change in the connected entities will be detected immediately. From 5669b27a8e6cf66398ae49aab99e2e62ca3410ce Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:26:20 +0200 Subject: [PATCH 16/49] Ruffed --- plugwise/__init__.py | 6 +++- plugwise/constants.py | 4 +-- plugwise/devices.py | 63 ++++++++++------------------------------ plugwise/legacy/smile.py | 6 +++- plugwise/smile.py | 6 +++- 5 files changed, 33 insertions(+), 52 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index edaba8230..1b31a25ed 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -332,7 +332,11 @@ async def _smile_detect_legacy( self.smile.legacy = True return return_model - async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: + async def async_update( + self, + ) -> dict[ + str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData + ]: """Update the Plugwise Gateway entities and their data and states.""" if self.smile.type == ANNA: data: dict[str, PlugwiseAnnaData] = {} diff --git a/plugwise/constants.py b/plugwise/constants.py index 5bfa7d3a5..83249e6aa 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -18,7 +18,7 @@ SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, - SmileThermostatGateway, + SmileTGateway, StretchGateway, ThermoZone, ) @@ -559,7 +559,7 @@ class PlugwiseAnnaData( GwEntityData, OnOffTherm, OpenTherm, - SmileThermostatGateway, + SmileTGateway, ): """The Plugwise Anna Data class.""" diff --git a/plugwise/devices.py b/plugwise/devices.py index cfea922de..9ecb7f31b 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -6,38 +6,40 @@ @dataclass -class BaseGateway: +class BaseClass: """Plugwise Base Gateway data class.""" + available: bool | None dev_class: str firmware: str + hardware: str | None location: str mac_address: str model: str + model_id: str | None name: str vendor: str @dataclass -class SmileP1Gateway(BaseGateway): +class SmileP1Gateway(BaseClass): """Plugwise Smile P1 Gateway data class.""" binary_sensors: GatewayBinarySensors # Not for legacy? - hardware: str - model_id: str @dataclass -class StretchGateway(BaseGateway): +class StretchGateway(BaseClass): """Plugwise Stretch Gateway data class.""" zigbee_mac_address: str @dataclass -class SmileThermostatGateway(SmileP1Gateway): +class SmileTGateway(BaseClass): """Plugwise Anna Smile-T Gateway data class.""" + binary_sensors: GatewayBinarySensors # Not for legacy? sensors: GatewaySensors @@ -56,7 +58,7 @@ class GatewaySensors: @dataclass -class AdamGateway(SmileThermostatGateway): +class AdamGateway(SmileTGateway): """Plugwise Adam HA Gateway data class.""" gateway_modes: list[str] @@ -67,16 +69,10 @@ class AdamGateway(SmileThermostatGateway): @dataclass -class SmartEnergyMeter: +class SmartEnergyMeter(BaseClass): """DSMR Energy Meter data class.""" - available: bool - dev_class: str - location: str - model: str - name: str sensors: SmartEnergySensors - vendor: str @dataclass @@ -131,25 +127,18 @@ class SmartEnergyLegacySensors: @dataclass -class AnnaData: +class AnnaData(BaseClass): """Plugwise Anna data class, also for legacy Anna.""" active_preset: str | None available_schedules: list[str] climate_mode: str control_state: str - dev_class: str - firmware: str - hardware: str - location: str - model: str - name: str preset_modes: list[str] | None select_schedule: str | None sensors: AnnaSensors temperature_offset: SetpointDict | None # not for legacy thermostat: ThermostatDict - vendor: str @dataclass @@ -164,23 +153,18 @@ class AnnaSensors: @dataclass -class ThermoZone: +class ThermoZone(BaseClass): """Plugwise Adam ThermoZone data class.""" active_preset: str | None available_schedules: list[str] climate_mode: str control_state: str - dev_class: str - model: str - name: str preset_modes: list[str] select_schedule: str sensors: ThermoZoneSensors thermostat: ThermostatDict thermostats: ThermostatsDict - vendor: str - @dataclass class ThermoZoneSensors: @@ -192,39 +176,24 @@ class ThermoZoneSensors: @dataclass -class AnnaAdamData: +class AnnaAdamData(BaseClass): """Plugwise Anna-connected-to-Adam data class.""" - dev_class: str - location: str - model: str - model_id: str - name: str sensors: AnnaSensors - vendor: str @dataclass -class JipLisaTomData: +class JipLisaTomData(BaseClass): """JipLisaTomData data class. Covering Plugwise Jip, Lisa and Tom/Floor devices. """ - available: bool binary_sensors: ( WirelessThermostatBinarySensors | None ) # Not for AC powered Lisa/Tom - dev_class: str - firmware: str - hardware: str - location: str - model: str - model_id: str - name: str sensors: JipLisaTomSensors temperature_offset: SetpointDict - vendor: str zigbee_mac_address: str @@ -388,10 +357,10 @@ class PlugSwitches: # data: dict[str, SmileP1Gateway | SmartEnergyMeter | SmartEnergyLegacySensors] -# class Anna(SmileThermostatGateway, AnnaData, OnOffTherm, OpenTherm): +# class Anna(SmileTGateway, AnnaData, OnOffTherm, OpenTherm): # """Plugwise Anna data class.""" # -# data: dict[str, SmileThermostatGateway | OnOffTherm | OpenTherm | AnnaData] +# data: dict[str, SmileTGateway | OnOffTherm | OpenTherm | AnnaData] # class Adam( diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index abc15ed5e..87d338f26 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -90,7 +90,11 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: + async def async_update( + self, + ) -> dict[ + str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData + ]: """Perform an full update update at day-change: re-collect all gateway entities and their data and states. Otherwise perform an incremental update: only collect the entities updated data and states. diff --git a/plugwise/smile.py b/plugwise/smile.py index 56109ce90..5b9458431 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -123,7 +123,11 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update(self) -> dict[str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData]: + async def async_update( + self, + ) -> dict[ + str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData + ]: """Perform an full update: re-collect all gateway entities and their data and states. Any change in the connected entities will be detected immediately. From a4bc814e253222a062f8f7a0055e5391353c254f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:43:07 +0200 Subject: [PATCH 17/49] Ruff --- plugwise/devices.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugwise/devices.py b/plugwise/devices.py index 9ecb7f31b..8ec497614 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -166,6 +166,7 @@ class ThermoZone(BaseClass): thermostat: ThermostatDict thermostats: ThermostatsDict + @dataclass class ThermoZoneSensors: """ThermoZone sensors class.""" From 6e2368331bcbf6d2d5139b086e5ce31d880f12ac Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:44:30 +0200 Subject: [PATCH 18/49] Try 4 --- plugwise/devices.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 8ec497614..fd6b31454 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -58,13 +58,15 @@ class GatewaySensors: @dataclass -class AdamGateway(SmileTGateway): +class AdamGateway(BaseClass): """Plugwise Adam HA Gateway data class.""" + binary_sensors: GatewayBinarySensors # Not for legacy? gateway_modes: list[str] regulation_modes: list[str] select_gateway_mode: str select_regulation_mode: str + sensors: GatewaySensors zigbee_mac_address: str @@ -253,32 +255,21 @@ class ThermostatsDict: @dataclass -class OnOffTherm: +class OnOffTherm(BaseClass): """On-off heater/cooler device class.""" binary_sensors: HeaterCentralBinarySensors - dev_class: str - location: str - model: str - name: str @dataclass -class OpenTherm: +class OpenTherm(BaseClass): """OpenTherm heater/cooler device class.""" - available: str binary_sensors: HeaterCentralBinarySensors - dev_class: str - location: str maximum_boiler_temperature: SetpointDict | None max_dhw_temperature: SetpointDict | None - model: str - model_id: str | None - name: str sensors: HeaterCentralSensors switches: HeaterCentralSwitches - vendor: str @dataclass @@ -317,20 +308,11 @@ class HeaterCentralSwitches: @dataclass -class PlugData: +class PlugData(BaseClass): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - available: bool | None - dev_class: str - firmware: str | None - hardware: str | None - location: str - model: str | None - model_id: str - name: str sensors: PlugSensors | None switches: PlugSwitches - vendor: str zigbee_mac_address: str From 3c8a17eb845587aa7d815ece50cbcea5b44e44b4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:52:31 +0200 Subject: [PATCH 19/49] Try 5 --- plugwise/devices.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index fd6b31454..5e5eb91d3 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -39,7 +39,7 @@ class StretchGateway(BaseClass): class SmileTGateway(BaseClass): """Plugwise Anna Smile-T Gateway data class.""" - binary_sensors: GatewayBinarySensors # Not for legacy? + binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors # Not for legacy? sensors: GatewaySensors @@ -61,7 +61,7 @@ class GatewaySensors: class AdamGateway(BaseClass): """Plugwise Adam HA Gateway data class.""" - binary_sensors: GatewayBinarySensors # Not for legacy? + binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors # Not for legacy? gateway_modes: list[str] regulation_modes: list[str] select_gateway_mode: str @@ -258,14 +258,14 @@ class ThermostatsDict: class OnOffTherm(BaseClass): """On-off heater/cooler device class.""" - binary_sensors: HeaterCentralBinarySensors + binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors @dataclass class OpenTherm(BaseClass): """OpenTherm heater/cooler device class.""" - binary_sensors: HeaterCentralBinarySensors + binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors maximum_boiler_temperature: SetpointDict | None max_dhw_temperature: SetpointDict | None sensors: HeaterCentralSensors From e7f629799a2c5a1eb2cd316bc11fc59ab46da2fe Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 29 Sep 2025 19:58:10 +0200 Subject: [PATCH 20/49] Try 6 --- plugwise/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 5e5eb91d3..baa3a8830 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -269,7 +269,7 @@ class OpenTherm(BaseClass): maximum_boiler_temperature: SetpointDict | None max_dhw_temperature: SetpointDict | None sensors: HeaterCentralSensors - switches: HeaterCentralSwitches + switches: HeaterCentralSwitches | PlugSwitches @dataclass From 0563cca364d60d6a29e2ae7b42a0142afb7d9db8 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 5 Oct 2025 18:14:54 +0200 Subject: [PATCH 21/49] Save PlugwiseData overview --- plugwise/devices.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/plugwise/devices.py b/plugwise/devices.py index baa3a8830..41be4cb3b 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -333,6 +333,53 @@ class PlugSwitches: lock: bool | None relay: bool +################################################## +class PlugwiseData +""" +Overview of existing options: + +- Gateway Adam + - Climate device + - OnOff + - Opentherm + - Zones (1 to many) with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - Location (Home) with weather data - only outdoor_temp used + - Single devices (appliances) assigned to a Zone, or not + - Anna (wired thermostat) + - Lisa (ZigBee thermostat) + - Jip (ZigBee thermostat) + - Tom/Floor (ZigBee valve/thermostat) + - Plug (energy switch/meter) + - Aqara Plug (energy switch/meter) + - Noname switch (energy switch) + +- Gateway SmileT + - Climate device + - OnOff + - OpenTherm + - Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - Location (Home) with weather data - only outdoor_temp used + - Single devices (appliances) + - Anna (wired thermostat) + - P1-DSMR device (new Anna P1) (?) + +- Gateway SmileT legacy + - OnOff/OpenTherm device + - Anna (wired thermostat) + - Location (Home) with weather data (optional?) - only outdoor_temp used + +- Gateway P1 + - P1-DSMR device (in Home location) + +- Gateway P1 legacy + - P1-DSMR device (in modules) + +- Gateway Stretch (legacy) + - Single devices (Zigbee) + - ?? + +""" +################################################## # class PlugwiseP1: # """Plugwise P1 data class.""" From 4691714f31d6dcff6e7814fb5dfa07ef8d35b3a2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 6 Oct 2025 12:12:25 +0200 Subject: [PATCH 22/49] Working --- plugwise/devices.py | 248 ++++++++++++++++++++++++++++++-------------- 1 file changed, 168 insertions(+), 80 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 41be4cb3b..5d2bcfd99 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -9,12 +9,12 @@ class BaseClass: """Plugwise Base Gateway data class.""" - available: bool | None + available: bool | None # not for gateway, should always be available dev_class: str firmware: str hardware: str | None location: str - mac_address: str + mac_address: str model: str model_id: str | None name: str @@ -22,16 +22,15 @@ class BaseClass: @dataclass -class SmileP1Gateway(BaseClass): - """Plugwise Smile P1 Gateway data class.""" - - binary_sensors: GatewayBinarySensors # Not for legacy? - - -@dataclass -class StretchGateway(BaseClass): - """Plugwise Stretch Gateway data class.""" +class AdamGateway(BaseClass): + """Plugwise Adam HA Gateway data class.""" + binary_sensors: GatewayBinarySensors + gateway_modes: list[str] + regulation_modes: list[str] + select_gateway_mode: str + select_regulation_mode: str + sensors: Weather zigbee_mac_address: str @@ -39,43 +38,56 @@ class StretchGateway(BaseClass): class SmileTGateway(BaseClass): """Plugwise Anna Smile-T Gateway data class.""" - binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors # Not for legacy? - sensors: GatewaySensors + binary_sensors: GatewayBinarySensors + sensors: Weather @dataclass -class GatewayBinarySensors: - """Gateway binary_sensors class.""" +class SmileTLegacyGateway(BaseClass): + """Plugwise legacy Anna Smile-T Gateway data class.""" - plugwise_notification: bool # None for some? + sensors: Weather @dataclass -class GatewaySensors: - """Gateway sensors class.""" +class SmileP1Gateway(BaseClass): + """Plugwise Smile P1 Gateway data class.""" - outdoor_temperature: float | None # None when not enabled? + binary_sensors: GatewayBinarySensors @dataclass -class AdamGateway(BaseClass): - """Plugwise Adam HA Gateway data class.""" +class SmileP1LegacyGateway(BaseClass): + """Plugwise legacy Smile P1 Gateway data class.""" + + +@dataclass +class StretchGateway(BaseClass): + """Plugwise Stretch Gateway data class.""" - binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors # Not for legacy? - gateway_modes: list[str] - regulation_modes: list[str] - select_gateway_mode: str - select_regulation_mode: str - sensors: GatewaySensors zigbee_mac_address: str +@dataclass +class GatewayBinarySensors: + """Gateway binary_sensors class.""" + + plugwise_notification: bool + + +@dataclass +class Weather: + """Gateway weather sensor class.""" + + outdoor_temperature: float | None # None when not available + + @dataclass class SmartEnergyMeter(BaseClass): """DSMR Energy Meter data class.""" sensors: SmartEnergySensors - +SmartEnergyMeterSmartEnergyMeter @dataclass class SmartEnergySensors: @@ -255,26 +267,43 @@ class ThermostatsDict: @dataclass -class OnOffTherm(BaseClass): - """On-off heater/cooler device class.""" +class OnOff(BaseClass): + """On-off climate device class.""" - binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors + binary_sensors: OnOffBinarySensors + sensors: OnOffSensors + + +@dataclass +class OpOffBinarySensors: + """OpenTherm binary_sensors class.""" + + heating_state: bool + + +@dataclass +class OnOffSensors: + """Heater-central sensors class.""" + + intended_boiler_temperature: float | None + modulation_level: float | None + water_temperature: float @dataclass class OpenTherm(BaseClass): - """OpenTherm heater/cooler device class.""" + """OpenTherm climate device class.""" - binary_sensors: GatewayBinarySensors | HeaterCentralBinarySensors + binary_sensors: OpenThermBinarySensors maximum_boiler_temperature: SetpointDict | None max_dhw_temperature: SetpointDict | None - sensors: HeaterCentralSensors - switches: HeaterCentralSwitches | PlugSwitches + sensors: OpenThermSensors + switches: OpenThermSwitches @dataclass -class HeaterCentralBinarySensors: - """Heater-central binary_sensors class.""" +class OpenThermBinarySensors: + """OpenTherm binary_sensors class.""" compressor_state: bool | None cooling_enabled: bool | None @@ -286,8 +315,8 @@ class HeaterCentralBinarySensors: @dataclass -class HeaterCentralSensors: - """Heater-central sensors class.""" +class OpenThermSensors: + """OpenTherm sensors class.""" dhw_temperature: float | None domestic_hot_water_setpoint: float | None @@ -300,8 +329,8 @@ class HeaterCentralSensors: @dataclass -class HeaterCentralSwitches: - """Heater-central switches class.""" +class OpenThermSwitches: + """OpenTherm switches class.""" cooling_ena_switch: bool | None dhw_cm_switch: bool @@ -335,50 +364,109 @@ class PlugSwitches: ################################################## class PlugwiseData -""" -Overview of existing options: - -- Gateway Adam - - Climate device - - OnOff - - Opentherm - - Zones (1 to many) with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter - - Location (Home) with weather data - only outdoor_temp used - - Single devices (appliances) assigned to a Zone, or not - - Anna (wired thermostat) - - Lisa (ZigBee thermostat) - - Jip (ZigBee thermostat) - - Tom/Floor (ZigBee valve/thermostat) - - Plug (energy switch/meter) - - Aqara Plug (energy switch/meter) - - Noname switch (energy switch) - -- Gateway SmileT - - Climate device - - OnOff - - OpenTherm - - Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter - - Location (Home) with weather data - only outdoor_temp used - - Single devices (appliances) + """ + Overview of existing options: + + - Gateway Adam + - Climate device + - OnOff + - Opentherm + - Zones (1 to many) with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - Location (Home) with weather data - only outdoor_temp used + - Single devices (appliances) assigned to a Zone, or not + - Anna (wired thermostat) + - Lisa (ZigBee thermostat) + - Jip (ZigBee thermostat) + - Tom/Floor (ZigBee valve/thermostat) + - Plug (energy switch/meter) + - Aqara Plug (energy switch/meter) + - Noname switch (energy switch) + + - Gateway SmileT + - Climate device + - OnOff + - OpenTherm + - Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - Location (Home) with weather data - only outdoor_temp used + - Single devices (appliances) + - Anna (wired thermostat) + - P1-DSMR device (new Anna P1) (?) + + - Gateway SmileT legacy + - OnOff/OpenTherm device - Anna (wired thermostat) - - P1-DSMR device (new Anna P1) (?) - -- Gateway SmileT legacy - - OnOff/OpenTherm device - - Anna (wired thermostat) - - Location (Home) with weather data (optional?) - only outdoor_temp used + - Location (Home) with weather data (optional?) - only outdoor_temp used -- Gateway P1 - - P1-DSMR device (in Home location) + - Gateway P1 + - P1-DSMR device (in Home location) -- Gateway P1 legacy - - P1-DSMR device (in modules) + - Gateway P1 legacy + - P1-DSMR device (in modules) -- Gateway Stretch (legacy) - - Single devices (Zigbee) - - ?? + - Gateway Stretch (legacy) + - Single devices (Zigbee) + - ?? + """ -""" + adam: AdamGateway() + smile_t: SmileTGateway() + smile_t_legacy: SmileTLegacyGateway() + # smile_t_p1: AnnaP1Gateway() # double? + smile_p1: SmileP1Gateway() + smile_p1_legacy: SmileP1LegacyGateway() + stretch: StretchGateway + onoff: OnOff() + opentherm: OpenTherm() + zones: list[Zone()] + weather: Weather() + anna: Anna() + anna_legacy: AnnaLegacy() + anna_adam: AnnaAdam() + lisa: Lisa() + jip: Jip() + tom_floor: TomFloor() + plug: Plug() + plug_legacy: PlugLegacy() + aqara_plug: AqaraPlug() + misc_plug: MiscPlug() + p1_dsmr: P1_DSMR() + + def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: + """Update the status object with data received from the Plugwise API.""" + if "adam" in data: + self.adam.update_from_dict(data["adam"]) + if "smile_t" in data: + self.smile_t.update_from_dict(data["smile_t"]) + # if "smile_t_p1" in data: + # self.smile_t_p1.update_from_dict(data["smile_t_p1"]) + if "smile_p1" in data: + self.smile_p1.update_from_dict(data["smile_p1"]) + if "stretch" in data: + self.stretch.update_from_dict(data["stretch"]) + if "onoff" in data: + self.onoff.update_from_dict(data["onoff"]) + if "opentherm" in data: + self.opentherm.update_from_dict(data["opentherm"]) + if "zones" in data: + self.zones.update_from_dict(data["zones"]) + if "anna" in data: + self.anna.update_from_dict(data["anna"]) + if "anna_adam" in data: + self.anna_adam.update_from_dict(data["anna_adam"]) + if "lisa" in data: + self.lisa.update_from_dict(data["lisa"]) + if "jip" in data: + self.zones.update_from_dict(data["jip"]) + if "tom_floor" in data: + self.tom_floor.update_from_dict(data["tom_floor"]) + if "plug" in data: + self.plug.update_from_dict(data["plug"]) + if "aqara_plug" in data: + self.opentherm.update_from_dict(data["aqara_plug"]) + if "misc_plug" in data: + self.misc_plug.update_from_dict(data["misc_plug"]) + if "p1_dsmr" in data: + self.p1_dsmr.update_from_dict(data["p1_dsmr"]) ################################################## # class PlugwiseP1: From 28a3240f9a8b36297170503c230fa429e8c6f1c9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 6 Oct 2025 16:39:05 +0200 Subject: [PATCH 23/49] Working 2 --- plugwise/devices.py | 67 ++++++++------------------------------------- 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 5d2bcfd99..21c3a70a4 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -141,15 +141,11 @@ class SmartEnergyLegacySensors: @dataclass -class AnnaData(BaseClass): - """Plugwise Anna data class, also for legacy Anna.""" +class Anna(BaseClass): + """Plugwise Anna class, also for legacy Anna.""" - active_preset: str | None - available_schedules: list[str] climate_mode: str control_state: str - preset_modes: list[str] | None - select_schedule: str | None sensors: AnnaSensors temperature_offset: SetpointDict | None # not for legacy thermostat: ThermostatDict @@ -167,8 +163,8 @@ class AnnaSensors: @dataclass -class ThermoZone(BaseClass): - """Plugwise Adam ThermoZone data class.""" +class Zone(BaseClass): + """Plugwise climate Zone data class.""" active_preset: str | None available_schedules: list[str] @@ -176,18 +172,18 @@ class ThermoZone(BaseClass): control_state: str preset_modes: list[str] select_schedule: str - sensors: ThermoZoneSensors + sensors: ZoneSensors thermostat: ThermostatDict thermostats: ThermostatsDict @dataclass -class ThermoZoneSensors: - """ThermoZone sensors class.""" +class ZoneSensors: + """ Climate Zone sensors class.""" - electricity_consumed: float | None # only with Plug(s) in the zone - electricity_produced: float | None # only with Plug(s) in the zone - temperature: float + electricity_consumed: float | None + electricity_produced: float | None + temperature: float | None @dataclass @@ -309,7 +305,7 @@ class OpenThermBinarySensors: cooling_enabled: bool | None cooling_state: bool | None dhw_state: bool - flame_state: bool + flame_state: bool | None heating_state: bool secondary_boiler_state: bool | None @@ -467,44 +463,3 @@ def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: self.misc_plug.update_from_dict(data["misc_plug"]) if "p1_dsmr" in data: self.p1_dsmr.update_from_dict(data["p1_dsmr"]) -################################################## - -# class PlugwiseP1: -# """Plugwise P1 data class.""" -# -# data: dict[str, SmileP1Gateway | SmartEnergyMeter | SmartEnergyLegacySensors] - - -# class Anna(SmileTGateway, AnnaData, OnOffTherm, OpenTherm): -# """Plugwise Anna data class.""" -# -# data: dict[str, SmileTGateway | OnOffTherm | OpenTherm | AnnaData] - - -# class Adam( -# AdamGateway, -# AnnaAdamData, -# JipLisaTomData, -# ThermoZone, -# PlugData, -# OnOffTherm, -# OpenTherm, -# ): -# """Plugwise Anna data class.""" -# -# data: dict[ -# str, -# AdamGateway -# | OnOffTherm -# | OpenTherm -# | AnnaAdamData -# | JipLisaTomData -# | ThermoZone -# | PlugData, -# ] - - -# class Stretch: -# """Plugwise Stretch data class.""" -# -# data: dict[str, StretchGateway | PlugData] From 77fc81373600e5dea95c6f99e61732fed48abd69 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 14 Nov 2025 12:59:37 +0100 Subject: [PATCH 24/49] BaseClass --> DeviceBase --- plugwise/devices.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 21c3a70a4..c15360a41 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -6,8 +6,12 @@ @dataclass -class BaseClass: - """Plugwise Base Gateway data class.""" +class DeviceBase: + """Plugwise Device Base class. + + Every device will have most of these data points. + """ + available: bool | None # not for gateway, should always be available dev_class: str @@ -22,7 +26,7 @@ class BaseClass: @dataclass -class AdamGateway(BaseClass): +class AdamGateway(DeviceBase): """Plugwise Adam HA Gateway data class.""" binary_sensors: GatewayBinarySensors @@ -35,7 +39,7 @@ class AdamGateway(BaseClass): @dataclass -class SmileTGateway(BaseClass): +class SmileTGateway(DeviceBase): """Plugwise Anna Smile-T Gateway data class.""" binary_sensors: GatewayBinarySensors @@ -43,26 +47,26 @@ class SmileTGateway(BaseClass): @dataclass -class SmileTLegacyGateway(BaseClass): +class SmileTLegacyGateway(DeviceBase): """Plugwise legacy Anna Smile-T Gateway data class.""" sensors: Weather @dataclass -class SmileP1Gateway(BaseClass): +class SmileP1Gateway(DeviceBase): """Plugwise Smile P1 Gateway data class.""" binary_sensors: GatewayBinarySensors @dataclass -class SmileP1LegacyGateway(BaseClass): +class SmileP1LegacyGateway(DeviceBase): """Plugwise legacy Smile P1 Gateway data class.""" @dataclass -class StretchGateway(BaseClass): +class StretchGateway(DeviceBase): """Plugwise Stretch Gateway data class.""" zigbee_mac_address: str @@ -83,7 +87,7 @@ class Weather: @dataclass -class SmartEnergyMeter(BaseClass): +class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" sensors: SmartEnergySensors @@ -141,7 +145,7 @@ class SmartEnergyLegacySensors: @dataclass -class Anna(BaseClass): +class Anna(DeviceBase): """Plugwise Anna class, also for legacy Anna.""" climate_mode: str @@ -163,7 +167,7 @@ class AnnaSensors: @dataclass -class Zone(BaseClass): +class Zone(DeviceBase): """Plugwise climate Zone data class.""" active_preset: str | None @@ -187,14 +191,14 @@ class ZoneSensors: @dataclass -class AnnaAdamData(BaseClass): +class AnnaAdamData(DeviceBase): """Plugwise Anna-connected-to-Adam data class.""" sensors: AnnaSensors @dataclass -class JipLisaTomData(BaseClass): +class JipLisaTomData(DeviceBase): """JipLisaTomData data class. Covering Plugwise Jip, Lisa and Tom/Floor devices. @@ -263,7 +267,7 @@ class ThermostatsDict: @dataclass -class OnOff(BaseClass): +class OnOff(DeviceBase): """On-off climate device class.""" binary_sensors: OnOffBinarySensors @@ -287,7 +291,7 @@ class OnOffSensors: @dataclass -class OpenTherm(BaseClass): +class OpenTherm(DeviceBase): """OpenTherm climate device class.""" binary_sensors: OpenThermBinarySensors @@ -333,7 +337,7 @@ class OpenThermSwitches: @dataclass -class PlugData(BaseClass): +class PlugData(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" sensors: PlugSensors | None From 001e625500ae8368190fcb7cf3f34c5282811619 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 14 Nov 2025 13:03:01 +0100 Subject: [PATCH 25/49] Add zone_profile related --- plugwise/devices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise/devices.py b/plugwise/devices.py index c15360a41..cdb9fd380 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -176,9 +176,11 @@ class Zone(DeviceBase): control_state: str preset_modes: list[str] select_schedule: str + select_zone_profile: str sensors: ZoneSensors thermostat: ThermostatDict thermostats: ThermostatsDict + zone_profiles: list[str] @dataclass From 08385f50b3edefc7147ec66d573644607bbd47c4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 14 Nov 2025 13:08:23 +0100 Subject: [PATCH 26/49] Add Emma, other small updates --- plugwise/devices.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index cdb9fd380..470410a5c 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -91,7 +91,7 @@ class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" sensors: SmartEnergySensors -SmartEnergyMeterSmartEnergyMeter + @dataclass class SmartEnergySensors: @@ -200,10 +200,10 @@ class AnnaAdamData(DeviceBase): @dataclass -class JipLisaTomData(DeviceBase): +class EmmaJipLisaTomData(DeviceBase): """JipLisaTomData data class. - Covering Plugwise Jip, Lisa and Tom/Floor devices. + Covering Plugwise Emma, Jip, Lisa and Tom/Floor devices. """ binary_sensors: ( @@ -215,11 +215,11 @@ class JipLisaTomData(DeviceBase): @dataclass -class JipLisaTomSensors: - """Tom sensors class.""" +class EmmaJipLisaTomSensors: + """Emma-Jip_lisa-Tom sensors class.""" battery: int | None # not when AC powered, Lisa/Tom - humidity: int | None # Jip only + humidity: int | None # Emma and Jip only setpoint: float | None # heat or cool setpoint_high: float | None # heat_cool setpoint_low: float | None # heat_cool @@ -351,7 +351,7 @@ class PlugData(DeviceBase): class PlugSensors: """Plug sensors class.""" - electricity_consumed: float | None + electricity_consumed: float | None # why None? electricity_consumed_interval: float electricity_produced: float | None electricity_produced_interval: float | None From 02da136b376d3b7e4e2e65085657c55815d5fa63 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 09:36:51 +0100 Subject: [PATCH 27/49] Set Optional and default to None where applicable --- plugwise/devices.py | 111 ++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 470410a5c..4b2bcfbd3 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Optional @dataclass @@ -13,14 +14,14 @@ class DeviceBase: """ - available: bool | None # not for gateway, should always be available + available: Optional[bool] = None # not for gateway, should always be available dev_class: str firmware: str - hardware: str | None + hardware: Optional[str] = None location: str mac_address: str model: str - model_id: str | None + model_id: Optional[str] = None name: str vendor: str @@ -83,7 +84,7 @@ class GatewayBinarySensors: class Weather: """Gateway weather sensor class.""" - outdoor_temperature: float | None # None when not available + outdoor_temperature: Optional[float] = None # None when not available @dataclass @@ -105,23 +106,23 @@ class SmartEnergySensors: electricity_consumed_peak_point: int electricity_phase_one_consumed: int electricity_phase_one_produced: int - electricity_phase_three_consumed: int | None - electricity_phase_three_produced: int | None - electricity_phase_two_consumed: int | None - electricity_phase_two_produced: int | None + electricity_phase_three_consumed: Optional[int] = None + electricity_phase_three_produced: Optional[int] = None + electricity_phase_two_consumed: Optional[int] = None + electricity_phase_two_produced: Optional[int] = None electricity_produced_off_peak_cumulative: float electricity_produced_off_peak_interval: int electricity_produced_off_peak_point: int electricity_produced_peak_cumulative: float electricity_produced_peak_interval: int electricity_produced_peak_point: int - gas_consumed_cumulative: float | None - gas_consumed_interval: float | None + gas_consumed_cumulative: Optional[float] = None + gas_consumed_interval: Optional[float] = None net_electricity_cumulative: float net_electricity_point: int - voltage_phase_one: float | None - voltage_phase_three: float | None - voltage_phase_two: float | None + voltage_phase_one: Optional[float] = None + voltage_phase_three: Optional[float] = None + voltage_phase_two: Optional[float] = None @dataclass @@ -138,8 +139,8 @@ class SmartEnergyLegacySensors: electricity_produced_peak_cumulative: float electricity_produced_peak_interval: int electricity_produced_point: int - gas_consumed_cumulative: float | None - gas_consumed_interval: float | None + gas_consumed_cumulative: Optional[float] = None + gas_consumed_interval: Optional[float] = None net_electricity_cumulative: float net_electricity_point: int @@ -151,7 +152,7 @@ class Anna(DeviceBase): climate_mode: str control_state: str sensors: AnnaSensors - temperature_offset: SetpointDict | None # not for legacy + temperature_offset: Optional[SetpointDict] = None # not for legacy thermostat: ThermostatDict @@ -160,9 +161,9 @@ class AnnaSensors: """Anna sensors class.""" illuminance: float - setpoint: float | None - setpoint_high: float | None - setpoint_low: float | None + setpoint: Optional[float] = None + setpoint_high: Optional[float] = None + setpoint_low: Optional[float] = None temperature: float @@ -170,7 +171,7 @@ class AnnaSensors: class Zone(DeviceBase): """Plugwise climate Zone data class.""" - active_preset: str | None + active_preset: Optional[str] = None available_schedules: list[str] climate_mode: str control_state: str @@ -187,9 +188,9 @@ class Zone(DeviceBase): class ZoneSensors: """ Climate Zone sensors class.""" - electricity_consumed: float | None - electricity_produced: float | None - temperature: float | None + electricity_consumed: Optional[float] = None + electricity_produced: Optional[float] = None + temperature: Optional[float] = None @dataclass @@ -207,7 +208,7 @@ class EmmaJipLisaTomData(DeviceBase): """ binary_sensors: ( - WirelessThermostatBinarySensors | None + Optional[WirelessThermostatBinarySensors] = None ) # Not for AC powered Lisa/Tom sensors: JipLisaTomSensors temperature_offset: SetpointDict @@ -218,14 +219,14 @@ class EmmaJipLisaTomData(DeviceBase): class EmmaJipLisaTomSensors: """Emma-Jip_lisa-Tom sensors class.""" - battery: int | None # not when AC powered, Lisa/Tom - humidity: int | None # Emma and Jip only - setpoint: float | None # heat or cool - setpoint_high: float | None # heat_cool - setpoint_low: float | None # heat_cool + battery: Optional[int] = None # not when AC powered, Lisa/Tom + humidity: Optional[int] = None # Emma and Jip only + setpoint: Optional[float] = None # heat or cool + setpoint_high: Optional[float] = None # heat_cool + setpoint_low: Optional[float] = None # heat_cool temperature: float - temperature_difference: float | None # Tom only - valve_position: float | None # Tom only + temperature_difference: Optional[float] = None # Tom only + valve_position: Optional[float] = None # Tom only @dataclass @@ -254,9 +255,9 @@ class ThermostatDict: lower_bound: float resolution: float - setpoint: float | None # heat or cool - setpoint_high: float | None # heat_cool - setpoint_low: float | None # heat_cool + setpoint: Optional[float] = None # heat or cool + setpoint_high: Optional[float] = None # heat_cool + setpoint_low: Optional[float] = None # heat_cool upper_bound: float @@ -287,8 +288,8 @@ class OpOffBinarySensors: class OnOffSensors: """Heater-central sensors class.""" - intended_boiler_temperature: float | None - modulation_level: float | None + intended_boiler_temperature: Optional[float] = None + modulation_level: Optional[float] = None water_temperature: float @@ -297,8 +298,8 @@ class OpenTherm(DeviceBase): """OpenTherm climate device class.""" binary_sensors: OpenThermBinarySensors - maximum_boiler_temperature: SetpointDict | None - max_dhw_temperature: SetpointDict | None + maximum_boiler_temperature: Optional[SetpointDict] = None + max_dhw_temperature: Optional[SetpointDict] = None sensors: OpenThermSensors switches: OpenThermSwitches @@ -307,26 +308,26 @@ class OpenTherm(DeviceBase): class OpenThermBinarySensors: """OpenTherm binary_sensors class.""" - compressor_state: bool | None - cooling_enabled: bool | None - cooling_state: bool | None + compressor_state: Optional[lbool] = None + cooling_enabled: Optional[bool] = None + cooling_state: Optional[bool] = None dhw_state: bool - flame_state: bool | None + flame_state: Optional[bool] = None heating_state: bool - secondary_boiler_state: bool | None + secondary_boiler_state: Optional[bool] = None @dataclass class OpenThermSensors: """OpenTherm sensors class.""" - dhw_temperature: float | None - domestic_hot_water_setpoint: float | None - intended_boiler_temperature: float | None - modulation_level: float | None - outdoor_air_temperature: float | None + dhw_temperature: Optional[float] = None + domestic_hot_water_setpoint: Optional[float] = None + intended_boiler_temperature: Optional[float] = None + modulation_level: Optional[float] = None + outdoor_air_temperature: Optional[float] = None return_temperature: float - water_pressure: float | None + water_pressure: Optional[float] = None water_temperature: float @@ -334,7 +335,7 @@ class OpenThermSensors: class OpenThermSwitches: """OpenTherm switches class.""" - cooling_ena_switch: bool | None + cooling_ena_switch: Optional[bool] = None dhw_cm_switch: bool @@ -342,7 +343,7 @@ class OpenThermSwitches: class PlugData(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - sensors: PlugSensors | None + sensors: Optional[PlugSensors] = None switches: PlugSwitches zigbee_mac_address: str @@ -351,17 +352,17 @@ class PlugData(DeviceBase): class PlugSensors: """Plug sensors class.""" - electricity_consumed: float | None # why None? + electricity_consumed: Optional[float] = None # Not present for Aqara Plug electricity_consumed_interval: float - electricity_produced: float | None - electricity_produced_interval: float | None + electricity_produced: Optional[float] = None + electricity_produced_interval: Optional[float] = None @dataclass class PlugSwitches: """Plug switches class.""" - lock: bool | None + lock: Optional[bool] = None relay: bool ################################################## From 83da65390bb708f7ac66ffbbb3ec497e2b98bfdd Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 09:51:56 +0100 Subject: [PATCH 28/49] Fixes --- plugwise/devices.py | 62 ++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 4b2bcfbd3..20936a870 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional +from typing import Any, Optional @dataclass @@ -194,23 +194,21 @@ class ZoneSensors: @dataclass -class AnnaAdamData(DeviceBase): +class AnnaAdam(DeviceBase): """Plugwise Anna-connected-to-Adam data class.""" sensors: AnnaSensors @dataclass -class EmmaJipLisaTomData(DeviceBase): - """JipLisaTomData data class. +class EmmaJipLisaTom(DeviceBase): + """JipLisaTom data class. Covering Plugwise Emma, Jip, Lisa and Tom/Floor devices. """ - binary_sensors: ( - Optional[WirelessThermostatBinarySensors] = None - ) # Not for AC powered Lisa/Tom - sensors: JipLisaTomSensors + binary_sensors: Optional[WirelessThermostatBinarySensors] = None # Not for AC powered Lisa/Tom + sensors: EmmaJipLisaTomSensors temperature_offset: SetpointDict zigbee_mac_address: str @@ -278,7 +276,7 @@ class OnOff(DeviceBase): @dataclass -class OpOffBinarySensors: +class OnOffBinarySensors: """OpenTherm binary_sensors class.""" heating_state: bool @@ -308,7 +306,7 @@ class OpenTherm(DeviceBase): class OpenThermBinarySensors: """OpenTherm binary_sensors class.""" - compressor_state: Optional[lbool] = None + compressor_state: Optional[bool] = None cooling_enabled: Optional[bool] = None cooling_state: Optional[bool] = None dhw_state: bool @@ -366,7 +364,7 @@ class PlugSwitches: relay: bool ################################################## -class PlugwiseData +class PlugwiseData: """ Overview of existing options: @@ -411,28 +409,28 @@ class PlugwiseData - ?? """ - adam: AdamGateway() - smile_t: SmileTGateway() - smile_t_legacy: SmileTLegacyGateway() + adam: AdamGateway + smile_t: SmileTGateway + smile_t_legacy: SmileTLegacyGateway # smile_t_p1: AnnaP1Gateway() # double? - smile_p1: SmileP1Gateway() - smile_p1_legacy: SmileP1LegacyGateway() + smile_p1: SmileP1Gateway + smile_p1_legacy: SmileP1LegacyGateway stretch: StretchGateway - onoff: OnOff() - opentherm: OpenTherm() - zones: list[Zone()] - weather: Weather() - anna: Anna() - anna_legacy: AnnaLegacy() - anna_adam: AnnaAdam() - lisa: Lisa() - jip: Jip() - tom_floor: TomFloor() - plug: Plug() - plug_legacy: PlugLegacy() - aqara_plug: AqaraPlug() - misc_plug: MiscPlug() - p1_dsmr: P1_DSMR() + onoff: OnOff + opentherm: OpenTherm + zones: list[Zone] + weather: Weather + anna: Anna + anna_legacy: Anna + anna_adam: AnnaAdam + lisa: EmmaJipLisaTom + jip: EmmaJipLisaTom + tom_floor: EmmaJipLisaTom + plug: Plug + plug_legacy: Plug + aqara_plug: Plug + misc_plug: Plug + p1_dsmr: SmartEnergyMeter def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: """Update the status object with data received from the Plugwise API.""" @@ -444,7 +442,7 @@ def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: # self.smile_t_p1.update_from_dict(data["smile_t_p1"]) if "smile_p1" in data: self.smile_p1.update_from_dict(data["smile_p1"]) - if "stretch" in data: + if "stretch" in data: self.stretch.update_from_dict(data["stretch"]) if "onoff" in data: self.onoff.update_from_dict(data["onoff"]) From 4ebed65ad7dd39a65997e1576fad22cfa4952e42 Mon Sep 17 00:00:00 2001 From: autoruff Date: Sat, 15 Nov 2025 08:52:49 +0000 Subject: [PATCH 29/49] fixup: device_classes Python code fixed using ruff --- plugwise/devices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 20936a870..a8ad05a24 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -13,13 +13,12 @@ class DeviceBase: Every device will have most of these data points. """ - available: Optional[bool] = None # not for gateway, should always be available dev_class: str firmware: str hardware: Optional[str] = None location: str - mac_address: str + mac_address: str model: str model_id: Optional[str] = None name: str @@ -186,7 +185,7 @@ class Zone(DeviceBase): @dataclass class ZoneSensors: - """ Climate Zone sensors class.""" + """Climate Zone sensors class.""" electricity_consumed: Optional[float] = None electricity_produced: Optional[float] = None @@ -207,7 +206,9 @@ class EmmaJipLisaTom(DeviceBase): Covering Plugwise Emma, Jip, Lisa and Tom/Floor devices. """ - binary_sensors: Optional[WirelessThermostatBinarySensors] = None # Not for AC powered Lisa/Tom + binary_sensors: Optional[WirelessThermostatBinarySensors] = ( + None # Not for AC powered Lisa/Tom + ) sensors: EmmaJipLisaTomSensors temperature_offset: SetpointDict zigbee_mac_address: str @@ -363,6 +364,7 @@ class PlugSwitches: lock: Optional[bool] = None relay: bool + ################################################## class PlugwiseData: """ @@ -398,7 +400,7 @@ class PlugwiseData: - Anna (wired thermostat) - Location (Home) with weather data (optional?) - only outdoor_temp used - - Gateway P1 + - Gateway P1 - P1-DSMR device (in Home location) - Gateway P1 legacy From 62ff1cb7aaec0653b04201ba0de6f2763108e594 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 11:08:39 +0100 Subject: [PATCH 30/49] More updates, corrections --- plugwise/devices.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index a8ad05a24..d742bab3d 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -378,6 +378,9 @@ class PlugwiseData: - Location (Home) with weather data - only outdoor_temp used - Single devices (appliances) assigned to a Zone, or not - Anna (wired thermostat) + - Emma Pro (ZigBee thermostat) + - Emma Pro wired (wired thermostat) + - (Emma Essential (wired thermostat)) - Lisa (ZigBee thermostat) - Jip (ZigBee thermostat) - Tom/Floor (ZigBee valve/thermostat) @@ -385,18 +388,20 @@ class PlugwiseData: - Aqara Plug (energy switch/meter) - Noname switch (energy switch) - - Gateway SmileT + - Gateway SmileT (Anna, Anna P1) - Climate device - OnOff - OpenTherm - - Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - (Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter) - Location (Home) with weather data - only outdoor_temp used - Single devices (appliances) - Anna (wired thermostat) - - P1-DSMR device (new Anna P1) (?) + - P1-DSMR device (Anna P1) - - Gateway SmileT legacy - - OnOff/OpenTherm device + - Gateway SmileT (Anna) legacy + - Climate device + - OnOff + - Opentherm - Anna (wired thermostat) - Location (Home) with weather data (optional?) - only outdoor_temp used @@ -414,7 +419,7 @@ class PlugwiseData: adam: AdamGateway smile_t: SmileTGateway smile_t_legacy: SmileTLegacyGateway - # smile_t_p1: AnnaP1Gateway() # double? + smile_t_p1: SmileTGatewayGateway smile_p1: SmileP1Gateway smile_p1_legacy: SmileP1LegacyGateway stretch: StretchGateway From 7a089d4e7e7b1edd644b73f87c7fb699562d665c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 11:55:34 +0100 Subject: [PATCH 31/49] Reorder, variables with a default must be after variables without --- plugwise/devices.py | 86 +++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index d742bab3d..506bd00cb 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -13,14 +13,11 @@ class DeviceBase: Every device will have most of these data points. """ - available: Optional[bool] = None # not for gateway, should always be available dev_class: str firmware: str - hardware: Optional[str] = None location: str mac_address: str model: str - model_id: Optional[str] = None name: str vendor: str @@ -31,6 +28,8 @@ class AdamGateway(DeviceBase): binary_sensors: GatewayBinarySensors gateway_modes: list[str] + hardware: str + model_id: str regulation_modes: list[str] select_gateway_mode: str select_regulation_mode: str @@ -43,6 +42,8 @@ class SmileTGateway(DeviceBase): """Plugwise Anna Smile-T Gateway data class.""" binary_sensors: GatewayBinarySensors + hardware: str + model_id: str sensors: Weather @@ -58,6 +59,8 @@ class SmileP1Gateway(DeviceBase): """Plugwise Smile P1 Gateway data class.""" binary_sensors: GatewayBinarySensors + hardware: str + model_id: str @dataclass @@ -90,6 +93,7 @@ class Weather: class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" + available: bool sensors: SmartEnergySensors @@ -105,20 +109,20 @@ class SmartEnergySensors: electricity_consumed_peak_point: int electricity_phase_one_consumed: int electricity_phase_one_produced: int - electricity_phase_three_consumed: Optional[int] = None - electricity_phase_three_produced: Optional[int] = None - electricity_phase_two_consumed: Optional[int] = None - electricity_phase_two_produced: Optional[int] = None electricity_produced_off_peak_cumulative: float electricity_produced_off_peak_interval: int electricity_produced_off_peak_point: int electricity_produced_peak_cumulative: float electricity_produced_peak_interval: int electricity_produced_peak_point: int - gas_consumed_cumulative: Optional[float] = None - gas_consumed_interval: Optional[float] = None net_electricity_cumulative: float net_electricity_point: int + electricity_phase_three_consumed: Optional[int] = None + electricity_phase_three_produced: Optional[int] = None + electricity_phase_two_consumed: Optional[int] = None + electricity_phase_two_produced: Optional[int] = None + gas_consumed_cumulative: Optional[float] = None + gas_consumed_interval: Optional[float] = None voltage_phase_one: Optional[float] = None voltage_phase_three: Optional[float] = None voltage_phase_two: Optional[float] = None @@ -138,21 +142,23 @@ class SmartEnergyLegacySensors: electricity_produced_peak_cumulative: float electricity_produced_peak_interval: int electricity_produced_point: int - gas_consumed_cumulative: Optional[float] = None - gas_consumed_interval: Optional[float] = None net_electricity_cumulative: float net_electricity_point: int + gas_consumed_cumulative: Optional[float] = None + gas_consumed_interval: Optional[float] = None @dataclass class Anna(DeviceBase): """Plugwise Anna class, also for legacy Anna.""" + available: bool climate_mode: str control_state: str + hardware: str sensors: AnnaSensors - temperature_offset: Optional[SetpointDict] = None # not for legacy thermostat: ThermostatDict + temperature_offset: Optional[SetpointDict] = None # not for legacy @dataclass @@ -160,17 +166,16 @@ class AnnaSensors: """Anna sensors class.""" illuminance: float + temperature: float setpoint: Optional[float] = None setpoint_high: Optional[float] = None setpoint_low: Optional[float] = None - temperature: float @dataclass class Zone(DeviceBase): """Plugwise climate Zone data class.""" - active_preset: Optional[str] = None available_schedules: list[str] climate_mode: str control_state: str @@ -181,7 +186,9 @@ class Zone(DeviceBase): thermostat: ThermostatDict thermostats: ThermostatsDict zone_profiles: list[str] - + active_preset: Optional[str] = None + hardware: Optional[str] = None + model_id: Optional[str] = None @dataclass class ZoneSensors: @@ -196,6 +203,8 @@ class ZoneSensors: class AnnaAdam(DeviceBase): """Plugwise Anna-connected-to-Adam data class.""" + available: bool + model_id: str sensors: AnnaSensors @@ -206,24 +215,28 @@ class EmmaJipLisaTom(DeviceBase): Covering Plugwise Emma, Jip, Lisa and Tom/Floor devices. """ - binary_sensors: Optional[WirelessThermostatBinarySensors] = ( - None # Not for AC powered Lisa/Tom - ) + available: bool + hardware: str + model_id: str sensors: EmmaJipLisaTomSensors temperature_offset: SetpointDict zigbee_mac_address: str + binary_sensors: Optional[WirelessThermostatBinarySensors] = ( + None # Not for AC powered Lisa/Tom + ) + @dataclass class EmmaJipLisaTomSensors: """Emma-Jip_lisa-Tom sensors class.""" + temperature: float battery: Optional[int] = None # not when AC powered, Lisa/Tom humidity: Optional[int] = None # Emma and Jip only setpoint: Optional[float] = None # heat or cool setpoint_high: Optional[float] = None # heat_cool setpoint_low: Optional[float] = None # heat_cool - temperature: float temperature_difference: Optional[float] = None # Tom only valve_position: Optional[float] = None # Tom only @@ -254,10 +267,10 @@ class ThermostatDict: lower_bound: float resolution: float + upper_bound: float setpoint: Optional[float] = None # heat or cool setpoint_high: Optional[float] = None # heat_cool setpoint_low: Optional[float] = None # heat_cool - upper_bound: float @dataclass @@ -272,6 +285,7 @@ class ThermostatsDict: class OnOff(DeviceBase): """On-off climate device class.""" + available: bool binary_sensors: OnOffBinarySensors sensors: OnOffSensors @@ -287,32 +301,34 @@ class OnOffBinarySensors: class OnOffSensors: """Heater-central sensors class.""" + water_temperature: float intended_boiler_temperature: Optional[float] = None modulation_level: Optional[float] = None - water_temperature: float @dataclass class OpenTherm(DeviceBase): """OpenTherm climate device class.""" + available: bool binary_sensors: OpenThermBinarySensors - maximum_boiler_temperature: Optional[SetpointDict] = None - max_dhw_temperature: Optional[SetpointDict] = None sensors: OpenThermSensors switches: OpenThermSwitches + maximum_boiler_temperature: Optional[SetpointDict] = None + max_dhw_temperature: Optional[SetpointDict] = None + model_id: Optional[str] = None @dataclass class OpenThermBinarySensors: """OpenTherm binary_sensors class.""" + dhw_state: bool + heating_state: bool compressor_state: Optional[bool] = None cooling_enabled: Optional[bool] = None cooling_state: Optional[bool] = None - dhw_state: bool flame_state: Optional[bool] = None - heating_state: bool secondary_boiler_state: Optional[bool] = None @@ -320,39 +336,42 @@ class OpenThermBinarySensors: class OpenThermSensors: """OpenTherm sensors class.""" + return_temperature: float + water_temperature: float dhw_temperature: Optional[float] = None domestic_hot_water_setpoint: Optional[float] = None intended_boiler_temperature: Optional[float] = None modulation_level: Optional[float] = None outdoor_air_temperature: Optional[float] = None - return_temperature: float water_pressure: Optional[float] = None - water_temperature: float @dataclass class OpenThermSwitches: """OpenTherm switches class.""" - cooling_ena_switch: Optional[bool] = None dhw_cm_switch: bool + cooling_ena_switch: Optional[bool] = None @dataclass -class PlugData(DeviceBase): +class Plug(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - sensors: Optional[PlugSensors] = None + available: bool switches: PlugSwitches zigbee_mac_address: str + sensors: Optional[PlugSensors] = None + hardware: Optional[str] = None + model_id: Optional[str] = None @dataclass class PlugSensors: """Plug sensors class.""" - electricity_consumed: Optional[float] = None # Not present for Aqara Plug electricity_consumed_interval: float + electricity_consumed: Optional[float] = None # Not present for Aqara Plug electricity_produced: Optional[float] = None electricity_produced_interval: Optional[float] = None @@ -361,9 +380,8 @@ class PlugSensors: class PlugSwitches: """Plug switches class.""" - lock: Optional[bool] = None relay: bool - + lock: Optional[bool] = None ################################################## class PlugwiseData: @@ -419,7 +437,7 @@ class PlugwiseData: adam: AdamGateway smile_t: SmileTGateway smile_t_legacy: SmileTLegacyGateway - smile_t_p1: SmileTGatewayGateway + smile_t_p1: SmileTGateway smile_p1: SmileP1Gateway smile_p1_legacy: SmileP1LegacyGateway stretch: StretchGateway From 0f03126d4d4731468ac7ad63b2d6bbd98b67309d Mon Sep 17 00:00:00 2001 From: autoruff Date: Sat, 15 Nov 2025 10:56:37 +0000 Subject: [PATCH 32/49] fixup: device_classes Python code fixed using ruff --- plugwise/devices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise/devices.py b/plugwise/devices.py index 506bd00cb..8f9fd9792 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -190,6 +190,7 @@ class Zone(DeviceBase): hardware: Optional[str] = None model_id: Optional[str] = None + @dataclass class ZoneSensors: """Climate Zone sensors class.""" @@ -383,6 +384,7 @@ class PlugSwitches: relay: bool lock: Optional[bool] = None + ################################################## class PlugwiseData: """ From 4100cbc620afef0811eae8118b1466a48ed01186 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 17:07:28 +0100 Subject: [PATCH 33/49] Change entity data structure --- plugwise/common.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/plugwise/common.py b/plugwise/common.py index 9722975e6..295c760b5 100644 --- a/plugwise/common.py +++ b/plugwise/common.py @@ -146,12 +146,14 @@ def _appl_thermostat_info( def _create_gw_entities(self, appl: Munch) -> None: """Helper-function for creating/updating gw_entities.""" + appl.device = self.device_name(pw_class, model_id or name) self.gw_entities[appl.entity_id] = {"dev_class": appl.pwclass} self._count += 1 for key, value in { "available": appl.available, "firmware": appl.firmware, "hardware": appl.hardware, + "id": appl.entity_id, "location": appl.location, "mac_address": appl.mac, "model": appl.model, @@ -162,9 +164,49 @@ def _create_gw_entities(self, appl: Munch) -> None: }.items(): if value is not None or key == "location": appl_key = cast(ApplianceType, key) - self.gw_entities[appl.entity_id][appl_key] = value + self.gw_entities[appl.device][appl_key] = value self._count += 1 + def device_name(self, pw_type: str, model): None + """Returns device name/type based on pw_type and optionally model..""" + match pw_type: + case "smartmeter": + return "smartmeter" + case "thermostat": + match "143.1": + return "anna_adam" + match: "Anna": + return "anna" + case "zone_thermostat": + match "158-01": + return "lisa" + match "170-01": + return "emma" + match "106-03": + return "tom_floor" + case "zone_thermometer": + match "168-01": + return "jip" + case "heater_central" + match "Opentherm": + return "opentherm" + match "OnOff": + return "onoff" + case "gateway": + match model_id: + case: "smile_open_therm": + return "adam" + case: "smile_thermo": + if self.smile.anna_p1: + return "smile_t_p1" + return "smile_t" + case: "smile": + return "smile_p1" + case: "stretch": + return "stretch" + case s if s.endswith("_plug"): + return "plug" + def _reorder_devices(self) -> None: """Place the gateway and optional heater_central devices as 1st and 2nd.""" reordered = {} From 808958ed1ce233762386739728875d33c4a46f91 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 17:16:26 +0100 Subject: [PATCH 34/49] Update class names --- plugwise/constants.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 83249e6aa..a71660b49 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -9,18 +9,18 @@ from plugwise.devices import ( AdamGateway, - AnnaAdamData, - AnnaData, - JipLisaTomData, - OnOffTherm, + AnnaAdam, + Anna, + EmmaJipLisaTom, + OnOff, OpenTherm, - PlugData, + Plug, SmartEnergyLegacySensors, SmartEnergyMeter, SmileP1Gateway, SmileTGateway, StretchGateway, - ThermoZone, + Zone, ) LOGGER = logging.getLogger(__name__) @@ -555,9 +555,9 @@ class GwEntityData: @dataclass class PlugwiseAnnaData( - AnnaData, + Anna, GwEntityData, - OnOffTherm, + OnOff, OpenTherm, SmileTGateway, ): @@ -567,13 +567,13 @@ class PlugwiseAnnaData( @dataclass class PlugwiseAdamData( AdamGateway, - AnnaAdamData, + AnnaAdam, GwEntityData, - JipLisaTomData, - PlugData, - OnOffTherm, + EmmaJipLisaTom, + Plug, + OnOff, OpenTherm, - ThermoZone, + Zone, ): """The Plugwise Adam Data class.""" @@ -589,7 +589,7 @@ class PlugwiseP1Data( @dataclass class PlugwiseStretchData( - PlugData, + Plug, StretchGateway, ): """The Plugwise Stretch Data class.""" From c7a1cd37b8915efa6e13f2e7c7ba2a7adf91cec0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 17:39:36 +0100 Subject: [PATCH 35/49] Further simplifications --- plugwise/devices.py | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 8f9fd9792..59f15a0c0 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -391,39 +391,28 @@ class PlugwiseData: Overview of existing options: - Gateway Adam - - Climate device - - OnOff - - Opentherm + - Climate device: OnOff or Opentherm - Zones (1 to many) with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter - - Location (Home) with weather data - only outdoor_temp used - Single devices (appliances) assigned to a Zone, or not - Anna (wired thermostat) - - Emma Pro (ZigBee thermostat) - Emma Pro wired (wired thermostat) - (Emma Essential (wired thermostat)) + - Emma Pro (ZigBee thermostat) - Lisa (ZigBee thermostat) - Jip (ZigBee thermostat) - Tom/Floor (ZigBee valve/thermostat) - - Plug (energy switch/meter) - - Aqara Plug (energy switch/meter) - - Noname switch (energy switch) + - Plug (energy switch/meter) / Aqara Plug (energy switch/meter) / Noname switch (energy switch) - Gateway SmileT (Anna, Anna P1) - - Climate device - - OnOff - - OpenTherm + - Climate device: OnOff or OpenTherm - (Zone (Living room) with with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter) - - Location (Home) with weather data - only outdoor_temp used - Single devices (appliances) - Anna (wired thermostat) - P1-DSMR device (Anna P1) - Gateway SmileT (Anna) legacy - - Climate device - - OnOff - - Opentherm + - Climate device: OnOff or Opentherm - Anna (wired thermostat) - - Location (Home) with weather data (optional?) - only outdoor_temp used - Gateway P1 - P1-DSMR device (in Home location) @@ -438,25 +427,18 @@ class PlugwiseData: adam: AdamGateway smile_t: SmileTGateway - smile_t_legacy: SmileTLegacyGateway - smile_t_p1: SmileTGateway smile_p1: SmileP1Gateway - smile_p1_legacy: SmileP1LegacyGateway stretch: StretchGateway onoff: OnOff opentherm: OpenTherm zones: list[Zone] weather: Weather anna: Anna - anna_legacy: Anna anna_adam: AnnaAdam lisa: EmmaJipLisaTom jip: EmmaJipLisaTom tom_floor: EmmaJipLisaTom plug: Plug - plug_legacy: Plug - aqara_plug: Plug - misc_plug: Plug p1_dsmr: SmartEnergyMeter def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: @@ -489,9 +471,9 @@ def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: self.tom_floor.update_from_dict(data["tom_floor"]) if "plug" in data: self.plug.update_from_dict(data["plug"]) - if "aqara_plug" in data: - self.opentherm.update_from_dict(data["aqara_plug"]) - if "misc_plug" in data: - self.misc_plug.update_from_dict(data["misc_plug"]) + # if "aqara_plug" in data: + # self.opentherm.update_from_dict(data["aqara_plug"]) + # if "misc_plug" in data: + # self.misc_plug.update_from_dict(data["misc_plug"]) if "p1_dsmr" in data: self.p1_dsmr.update_from_dict(data["p1_dsmr"]) From 5353d73ae047c8de31529015feeb8c3975e7e269 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 15 Nov 2025 17:41:37 +0100 Subject: [PATCH 36/49] Try 2 --- plugwise/__init__.py | 26 +- plugwise/common.py | 44 +-- plugwise/constants.py | 114 ++++---- plugwise/devices.py | 562 +++++++++++++++++++++------------------ plugwise/legacy/smile.py | 11 +- plugwise/smile.py | 11 +- 6 files changed, 364 insertions(+), 404 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 1b31a25ed..bf4f3b04b 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -8,8 +8,6 @@ from typing import cast from plugwise.constants import ( - ADAM, - ANNA, DEFAULT_LEGACY_TIMEOUT, DEFAULT_PORT, DEFAULT_TIMEOUT, @@ -18,16 +16,12 @@ LOGGER, MODULES, NONE, - SMILE_P1, SMILES, STATE_OFF, STATE_ON, STATUS, SYSTEM, - PlugwiseAdamData, - PlugwiseAnnaData, - PlugwiseP1Data, - PlugwiseStretchData, + GwEntityData, ThermoLoc, ) from plugwise.exceptions import ( @@ -332,21 +326,9 @@ async def _smile_detect_legacy( self.smile.legacy = True return return_model - async def async_update( - self, - ) -> dict[ - str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData - ]: - """Update the Plugwise Gateway entities and their data and states.""" - if self.smile.type == ANNA: - data: dict[str, PlugwiseAnnaData] = {} - if self.smile.type == ADAM: - data: dict[str, PlugwiseAdamData] = {} - if self.smile.type == SMILE_P1: - data: dict[str, PlugwiseP1Data] = {} - if self.smile.type == "Stretch": - data: dict[str, PlugwiseStretchData] = {} - + async def async_update(self) -> dict[str, GwEntityData]: + """Update the Plughwise Gateway entities and their data and states.""" + data: dict[str, GwEntityData] = {} try: data = await self._smile_api.async_update() except (DataMissingError, KeyError) as err: diff --git a/plugwise/common.py b/plugwise/common.py index 295c760b5..9722975e6 100644 --- a/plugwise/common.py +++ b/plugwise/common.py @@ -146,14 +146,12 @@ def _appl_thermostat_info( def _create_gw_entities(self, appl: Munch) -> None: """Helper-function for creating/updating gw_entities.""" - appl.device = self.device_name(pw_class, model_id or name) self.gw_entities[appl.entity_id] = {"dev_class": appl.pwclass} self._count += 1 for key, value in { "available": appl.available, "firmware": appl.firmware, "hardware": appl.hardware, - "id": appl.entity_id, "location": appl.location, "mac_address": appl.mac, "model": appl.model, @@ -164,49 +162,9 @@ def _create_gw_entities(self, appl: Munch) -> None: }.items(): if value is not None or key == "location": appl_key = cast(ApplianceType, key) - self.gw_entities[appl.device][appl_key] = value + self.gw_entities[appl.entity_id][appl_key] = value self._count += 1 - def device_name(self, pw_type: str, model): None - """Returns device name/type based on pw_type and optionally model..""" - match pw_type: - case "smartmeter": - return "smartmeter" - case "thermostat": - match "143.1": - return "anna_adam" - match: "Anna": - return "anna" - case "zone_thermostat": - match "158-01": - return "lisa" - match "170-01": - return "emma" - match "106-03": - return "tom_floor" - case "zone_thermometer": - match "168-01": - return "jip" - case "heater_central" - match "Opentherm": - return "opentherm" - match "OnOff": - return "onoff" - case "gateway": - match model_id: - case: "smile_open_therm": - return "adam" - case: "smile_thermo": - if self.smile.anna_p1: - return "smile_t_p1" - return "smile_t" - case: "smile": - return "smile_p1" - case: "stretch": - return "stretch" - case s if s.endswith("_plug"): - return "plug" - def _reorder_devices(self) -> None: """Place the gateway and optional heater_central devices as 1st and 2nd.""" reordered = {} diff --git a/plugwise/constants.py b/plugwise/constants.py index a71660b49..1ddd37c5f 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -3,26 +3,9 @@ from __future__ import annotations from collections import namedtuple -from dataclasses import dataclass import logging from typing import Final, Literal, TypedDict, get_args -from plugwise.devices import ( - AdamGateway, - AnnaAdam, - Anna, - EmmaJipLisaTom, - OnOff, - OpenTherm, - Plug, - SmartEnergyLegacySensors, - SmartEnergyMeter, - SmileP1Gateway, - SmileTGateway, - StretchGateway, - Zone, -) - LOGGER = logging.getLogger(__name__) # Copied homeassistant.consts @@ -541,9 +524,24 @@ class ActuatorData(TypedDict, total=False): upper_bound: float -@dataclass -class GwEntityData: - """The base Gateway Entity data class.""" +class GwEntityData(TypedDict, total=False): + """The Gateway Entity data class. + + Covering the collected output-data per device or location. + """ + + # Appliance base data + dev_class: str + firmware: str + hardware: str + location: str + mac_address: str + members: list[str] + model: str + model_id: str | None + name: str + vendor: str + zigbee_mac_address: str # For temporary use cooling_enabled: bool @@ -552,44 +550,40 @@ class GwEntityData: c_heating_state: bool thermostat_supports_cooling: bool + # Device availability + available: bool | None + + # Loria + select_dhw_mode: str + dhw_modes: list[str] + + # Gateway + gateway_modes: list[str] + notifications: dict[str, dict[str, str]] + regulation_modes: list[str] + select_gateway_mode: str + select_regulation_mode: str + + # Thermostat-related + select_zone_profile: str + thermostats: dict[str, list[str]] + zone_profiles: list[str] + # Presets: + active_preset: str | None + preset_modes: list[str] | None + # Schedules: + available_schedules: list[str] + select_schedule: str | None + + climate_mode: str + # Extra for Adam Master Thermostats + control_state: str -@dataclass -class PlugwiseAnnaData( - Anna, - GwEntityData, - OnOff, - OpenTherm, - SmileTGateway, -): - """The Plugwise Anna Data class.""" - - -@dataclass -class PlugwiseAdamData( - AdamGateway, - AnnaAdam, - GwEntityData, - EmmaJipLisaTom, - Plug, - OnOff, - OpenTherm, - Zone, -): - """The Plugwise Adam Data class.""" - - -@dataclass -class PlugwiseP1Data( - SmartEnergyLegacySensors, - SmartEnergyMeter, - SmileP1Gateway, -): - """The Plugwise P1 Data class.""" - - -@dataclass -class PlugwiseStretchData( - Plug, - StretchGateway, -): - """The Plugwise Stretch Data class.""" + # Dict-types + binary_sensors: SmileBinarySensors + max_dhw_temperature: ActuatorData + maximum_boiler_temperature: ActuatorData + sensors: SmileSensors + switches: SmileSwitches + temperature_offset: ActuatorData + thermostat: ActuatorData diff --git a/plugwise/devices.py b/plugwise/devices.py index 59f15a0c0..ad8c3f29e 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -5,118 +5,150 @@ from dataclasses import dataclass from typing import Any, Optional +from .constants import ZONE_THERMOSTATS -@dataclass -class DeviceBase: - """Plugwise Device Base class. - Every device will have most of these data points. - """ +def process_key(data: dict[str, Any], key: str) -> Any | None: + """Return the key value from the data dict, when present.""" - dev_class: str - firmware: str - location: str - mac_address: str - model: str - name: str - vendor: str + if key in data: + return data[key] + return None -@dataclass -class AdamGateway(DeviceBase): - """Plugwise Adam HA Gateway data class.""" - - binary_sensors: GatewayBinarySensors - gateway_modes: list[str] - hardware: str - model_id: str - regulation_modes: list[str] - select_gateway_mode: str - select_regulation_mode: str - sensors: Weather - zigbee_mac_address: str +def process_dict( + data: dict[str, Any], + dict_type: str, + key: str) -> Any | None: + """Return the key value from the data dict, when present.""" -@dataclass -class SmileTGateway(DeviceBase): - """Plugwise Anna Smile-T Gateway data class.""" + if dict_type in data and key in data[dict_type]: + return data[dict_type][key] - binary_sensors: GatewayBinarySensors - hardware: str - model_id: str - sensors: Weather + return None @dataclass -class SmileTLegacyGateway(DeviceBase): - """Plugwise legacy Anna Smile-T Gateway data class.""" +class DeviceBase: + """Plugwise Device Base class. - sensors: Weather + Every device will have most of these data points. + """ + dev_class: Optional[str] = None + firmware: Optional[str] = None + location: Optional[str] = None + mac_address: Optional[str] = None + model: Optional[str] = None + name: Optional[str] = None + vendor: Optional[str] = None -@dataclass -class SmileP1Gateway(DeviceBase): - """Plugwise Smile P1 Gateway data class.""" + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this DeviceBase object with data from a dictionary.""" - binary_sensors: GatewayBinarySensors - hardware: str - model_id: str + self.dev_class.process_key(data, "dev_class") + self.firmware.process_key(data, "firmware") + self.location.process_key(data, "location") + self.mac_address.process_key(data, "mac_address") + self.model.process_key(data, "model") + self.name.process_key(data, "name") + self.vendor.process_key(data, "vendor") @dataclass -class SmileP1LegacyGateway(DeviceBase): - """Plugwise legacy Smile P1 Gateway data class.""" +class Gateway(DeviceBase): + """Plugwise Gateway class.""" + super().__init__() + binary_sensors: Optional[GatewayBinarySensors] = None + gateway_modes: Optional[list[str]] = None + hardware: Optional[str] = None + model_id: Optional[str] = None + regulation_modes: Optional[list[str]] = None + select_gateway_mode: Optional[str] = None + select_regulation_mode: Optional[str] = None + sensors: Optional[Weather]= None + zigbee_mac_address: Optional[str] = None -@dataclass -class StretchGateway(DeviceBase): - """Plugwise Stretch Gateway data class.""" + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this Gateway object with data from a dictionary.""" - zigbee_mac_address: str + super().update_from_dict(data) + self.binary_sensors.update_from_dict(data) + self.gateway_modes.process_key(data, "gateway_mode") + self.hardware.process_key(data, "gateway_mode") + self.model_id.process_key(data, "gateway_mode") + self.regulation_modes.process_key(data, "gateway_mode") + self.select_gateway_mode.process_key(data, "gateway_mode") + self.sensors.update_from_dict(data) + self.zigbee_mac_address.process_key(data, "gateway_mode") @dataclass class GatewayBinarySensors: """Gateway binary_sensors class.""" - plugwise_notification: bool + plugwise_notification: bool = False + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this GatewayBinarySensors object with data from a dictionary.""" + + self.plugwise_notification.process_dict( + data, "binary_sensors", "plugwise_notification" + ) @dataclass class Weather: """Gateway weather sensor class.""" - outdoor_temperature: Optional[float] = None # None when not available + outdoor_temperature: Optional[float] = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this GatewayBinarySensors object with data from a dictionary.""" + + self.outdoor_temperature.process_dict(data, "sensors", "outdoor_temperature") @dataclass class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" - available: bool - sensors: SmartEnergySensors + super().__init__() + available: Optional[bool] = None + sensors: Optional[SmartEnergySensors] = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this SmartEnergyMeter object with data from a dictionary.""" + + super().update_from_dict(data) + self.available.process_key(data, "available") + self.sensors.update_from_dict(data) @dataclass class SmartEnergySensors: """DSMR Energy Meter sensors class (P1 v4).""" - electricity_consumed_off_peak_cumulative: float - electricity_consumed_off_peak_interval: int - electricity_consumed_off_peak_point: int - electricity_consumed_peak_cumulative: float - electricity_consumed_peak_interval: int - electricity_consumed_peak_point: int - electricity_phase_one_consumed: int - electricity_phase_one_produced: int - electricity_produced_off_peak_cumulative: float - electricity_produced_off_peak_interval: int - electricity_produced_off_peak_point: int - electricity_produced_peak_cumulative: float - electricity_produced_peak_interval: int - electricity_produced_peak_point: int - net_electricity_cumulative: float - net_electricity_point: int + electricity_consumed_off_peak_cumulative: float = 0.0 + electricity_consumed_off_peak_interval: int = 0 + electricity_consumed_off_peak_point: Optional[int] = None + electricity_consumed_peak_cumulative: float = 0.0 + electricity_consumed_peak_interval: int = 0 + electricity_consumed_peak_point: Optional[int] = None + electricity_consumed_point: Optional[int] = None + electricity_phase_one_consumed: int = 0 + electricity_phase_one_produced: int = 0 + electricity_produced_off_peak_cumulative: float = 0.0 + electricity_produced_off_peak_interval: int = 0 + electricity_produced_off_peak_point: Optional[int] = None + electricity_produced_peak_cumulative: float = 0.0 + electricity_produced_peak_interval: int = 0 + electricity_produced_peak_point: Optional[int] = None + electricity_produced_point: Optional[int] = None + net_electricity_cumulative: float = 0.0 + net_electricity_point: int = 0 electricity_phase_three_consumed: Optional[int] = None electricity_phase_three_produced: Optional[int] = None electricity_phase_two_consumed: Optional[int] = None @@ -127,126 +159,154 @@ class SmartEnergySensors: voltage_phase_three: Optional[float] = None voltage_phase_two: Optional[float] = None - -@dataclass -class SmartEnergyLegacySensors: - """Legacy DSMR Energy Meter sensors class (P1 v2).""" - - electricity_consumed_off_peak_cumulative: float - electricity_consumed_off_peak_interval: int - electricity_consumed_peak_cumulative: float - electricity_consumed_peak_interval: int - electricity_consumed_point: int - electricity_produced_off_peak_cumulative: float - electricity_produced_off_peak_interval: int - electricity_produced_peak_cumulative: float - electricity_produced_peak_interval: int - electricity_produced_point: int - net_electricity_cumulative: float - net_electricity_point: int - gas_consumed_cumulative: Optional[float] = None - gas_consumed_interval: Optional[float] = None - - -@dataclass -class Anna(DeviceBase): - """Plugwise Anna class, also for legacy Anna.""" - - available: bool - climate_mode: str - control_state: str - hardware: str - sensors: AnnaSensors - thermostat: ThermostatDict - temperature_offset: Optional[SetpointDict] = None # not for legacy - - -@dataclass -class AnnaSensors: - """Anna sensors class.""" - - illuminance: float - temperature: float - setpoint: Optional[float] = None - setpoint_high: Optional[float] = None - setpoint_low: Optional[float] = None + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this SmartEnergySensors object with data from a dictionary.""" + + self.electricity_consumed_off_peak_cumulative.process_dict(data, "sensors", "electricity_consumed_off_peak_cumulative") + self.electricity_consumed_off_peak_interval.process_dict(data, "sensors", "electricity_consumed_off_peak_interval") + self.electricity_consumed_off_peak_point.process_dict(data, "sensors", "electricity_consumed_off_peak_point") + self.electricity_consumed_peak_cumulative.process_dict(data, "sensors", "electricity_consumed_peak_cumulative") + self.electricity_consumed_peak_interval.process_dict(data, "sensors", "electricity_consumed_peak_interval") + self.electricity_consumed_peak_point.process_dict(data, "sensors", "electricity_consumed_peak_point") + self.electricity_consumed_point.process_dict(data, "sensors", "electricity_consumed_point") + self.electricity_phase_one_consumed.process_dict(data, "sensors", "electricity_phase_one_consumed") + self.electricity_phase_one_produced.process_dict(data, "sensors", "electricity_phase_one_produced") + self.electricity_produced_off_peak_cumulative.process_dict(data, "sensors", "electricity_produced_off_peak_cumulative") + self.electricity_produced_off_peak_interval.process_dict(data, "sensors", "electricity_produced_off_peak_interval") + self.electricity_produced_off_peak_point.process_dict(data, "sensors", "electricity_produced_off_peak_point") + self.electricity_produced_peak_cumulative.process_dict(data, "sensors", "electricity_produced_peak_cumulative") + self.electricity_produced_peak_interval.process_dict(data, "sensors", "electricity_produced_peak_interval") + self.electricity_produced_peak_point.process_dict(data, "sensors", "electricity_produced_peak_point") + self.electricity_produced_point.process_dict(data, "sensors", "electricity_produced_point") + self.net_electricity_cumulative.process_dict(data, "sensors", "net_electricity_cumulative") + self.net_electricity_point.process_dict(data, "sensors", "net_electricity_point") + self.electricity_phase_three_consumed.process_dict(data, "sensors", "electricity_phase_three_consumed") + self.electricity_phase_three_produced.process_dict(data, "sensors", "electricity_phase_three_produced") + self.electricity_phase_two_consumed.process_dict(data, "sensors", "electricity_phase_two_consumed") + self.electricity_phase_two_produced.process_dict(data, "sensors", "electricity_phase_two_produced") + self.gas_consumed_cumulative.process_dict(data, "sensors", "gas_consumed_cumulative") + self.gas_consumed_interval.process_dict(data, "sensors", "gas_consumed_interval") + self.voltage_phase_one.process_dict(data, "sensors", "voltage_phase_one") + self.voltage_phase_three.process_dict(data, "sensors", "voltage_phase_three") + self.voltage_phase_two.process_dict(data, "sensors", "voltage_phase_two") @dataclass class Zone(DeviceBase): """Plugwise climate Zone data class.""" - available_schedules: list[str] - climate_mode: str - control_state: str - preset_modes: list[str] - select_schedule: str - select_zone_profile: str - sensors: ZoneSensors - thermostat: ThermostatDict - thermostats: ThermostatsDict - zone_profiles: list[str] + super().__init__() + available_schedules: list[str] = [] + climate_mode: str = "heat" + control_state: str = "heating" + preset_modes: list[str] = [] + select_zone_profile: str = "off" + zone_profiles: list[str] = [] active_preset: Optional[str] = None hardware: Optional[str] = None model_id: Optional[str] = None + select_schedule: Optional[str] = None + sensors: Optional[ZoneSensors] = None + thermostat: Optional[ThermostatDict] = None + thermostats: Optional[ThermostatsDict] = None -@dataclass -class ZoneSensors: - """Climate Zone sensors class.""" + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this climate Zone object with data from a dictionary.""" - electricity_consumed: Optional[float] = None - electricity_produced: Optional[float] = None - temperature: Optional[float] = None + super().update_from_dict(data) + self.available_schedules.process_key(data, "available_schedules") + self.climate_mode.process_key(data, "climate_mode") + self.control_state.process_key(data, "control_state") + self.preset_modes.process_key(data, "preset_modes") + self.select_zone_profile.process_key(data, "select_zone_profile") + self.zone_profiles.process_key(data, "zone_profiles") + self.active_preset.process_key(data, "active_preset") + self.hardware.process_key(data, "hardware") + self.model_id.process_key(data, "model_id") + self.select_schedule.process_key(data, "select_schedule") + self.sensors.update_from_dict(data) + self.thermostat.process_key(data, "thermostat") + self.thermostats.process_key(data, "thermostats") @dataclass -class AnnaAdam(DeviceBase): - """Plugwise Anna-connected-to-Adam data class.""" - - available: bool - model_id: str - sensors: AnnaSensors - - -@dataclass -class EmmaJipLisaTom(DeviceBase): - """JipLisaTom data class. +class ZoneSensors: + """Climate Zone sensors class.""" - Covering Plugwise Emma, Jip, Lisa and Tom/Floor devices. - """ + electricity_consumed: Optional[float] | None = None + electricity_produced: Optional[float] | None = None + temperature: Optional[float] | None = None - available: bool - hardware: str - model_id: str - sensors: EmmaJipLisaTomSensors - temperature_offset: SetpointDict - zigbee_mac_address: str + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" - binary_sensors: Optional[WirelessThermostatBinarySensors] = ( - None # Not for AC powered Lisa/Tom - ) + self.electricity_consumed.process_dict(data, "sensors", "electricity_consumed") + self.electricity_produced.process_dict(data, "sensors", "electricity_produced") + self.temperature.process_dict(data, "sensors", "temperature") @dataclass -class EmmaJipLisaTomSensors: - """Emma-Jip_lisa-Tom sensors class.""" +class Thermostat(DeviceBase): + """Plugwise Thermostat class, covering Anna (legacy) standalone or wired to Adam, + + Emma Essential/Pro standalone, or Emma Pro, Jip, Lisa and Tom/Floor connected to Adam. + """ - temperature: float - battery: Optional[int] = None # not when AC powered, Lisa/Tom - humidity: Optional[int] = None # Emma and Jip only + super().__init__() + available_schedules: list[str] = [] + control_state: str = "heating" + preset_modes: list[str] = [] + active_preset: Optional[str] = None + binary_sensors: Optional[WirelessThermostatBinarySensors] = None + climate_mode: Optional[str] = None + hardware: Optional[str] = None + model_id: Optional[str] = None + select_schedule: Optional[str] = None + sensors: Optional[ThermostatSensors] = None + temperature_offset: Optional[SetpointDict] = None # not for legacy + thermostat: Optional[ThermostatDict] = None + zigbee_mac_address: Optional[str] = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this Thermostat object with data from a dictionary.""" + + super().update_from_dict(data) + self.available_schedules.process_key(data, "available_schedules") + self.control_state.process_key(data, "control_state") + self.preset_modes.process_key(data, "preset_modes") + self.active_preset.process_key(data, "active_preset") + self.binary_sensors.update_from_dict(data) + self.climate_mode.process_key(data, "climate_mode") + self.hardware.process_key(data, "hardware") + self.model_id.process_key(data, "model_id") + self.select_schedule.process_key(data, "select_schedule") + self.sensors.update_from_dict(data) + self.temperature_offset.process_key(data, "temperature_offset") + self.thermostat.process_key(data, "thermostat") + self.zigbee_mac_address.process_key(data, "zigbee_mac_address") + + +@dataclass +class ThermostatSensors: + """Thermostat sensors class.""" + + battery: Optional[int] = None # not when AC powered, Lisa/Tom/Floor + humidity: Optional[int] = None # Emma and Jip + illuminance: Optional[float] = None # Anna + temperature: float = 0.0 setpoint: Optional[float] = None # heat or cool setpoint_high: Optional[float] = None # heat_cool setpoint_low: Optional[float] = None # heat_cool - temperature_difference: Optional[float] = None # Tom only - valve_position: Optional[float] = None # Tom only + temperature_difference: Optional[float] = None # Tom/Floor + valve_position: Optional[float] = None # Tom/Floor @dataclass class WirelessThermostatBinarySensors: - """Lisa sensors class.""" + """Wireless thermostat sensors class.""" - low_battery: bool + low_battery: bool = False @dataclass @@ -256,19 +316,19 @@ class SetpointDict: Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. """ - lower_bound: float - resolution: float - setpoint: float - upper_bound: float + lower_bound: float = 0.0 + resolution: float = 0.0 + setpoint: float = 0.0 + upper_bound: float = 0.0 @dataclass class ThermostatDict: """Thermostat dict class.""" - lower_bound: float - resolution: float - upper_bound: float + lower_bound: float = 0.0 + resolution: float = 0.0 + upper_bound: float = 0.0 setpoint: Optional[float] = None # heat or cool setpoint_high: Optional[float] = None # heat_cool setpoint_low: Optional[float] = None # heat_cool @@ -278,80 +338,71 @@ class ThermostatDict: class ThermostatsDict: """Thermostats dict class.""" - primary: list[str] - secondary: list[str] - - -@dataclass -class OnOff(DeviceBase): - """On-off climate device class.""" - - available: bool - binary_sensors: OnOffBinarySensors - sensors: OnOffSensors + primary: list[str] = [] + secondary: list[str] = [] @dataclass -class OnOffBinarySensors: - """OpenTherm binary_sensors class.""" +class ClimateDevice(DeviceBase): + """Climate-device class. - heating_state: bool - - -@dataclass -class OnOffSensors: - """Heater-central sensors class.""" - - water_temperature: float - intended_boiler_temperature: Optional[float] = None - modulation_level: Optional[float] = None - - -@dataclass -class OpenTherm(DeviceBase): - """OpenTherm climate device class.""" + Representing both OnOff and OpenTherm types. + """ - available: bool - binary_sensors: OpenThermBinarySensors - sensors: OpenThermSensors - switches: OpenThermSwitches + super().__init__() + available: Optional[bool] = None + binary_sensors: Optional[ClimateDeviceBinarySensors] = None maximum_boiler_temperature: Optional[SetpointDict] = None max_dhw_temperature: Optional[SetpointDict] = None model_id: Optional[str] = None + sensors: Optional[ClimateDeviceSensors] = None + switches: Optional[ClimateDeviceSwitches] = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ClimateDevice object with data from a dictionary.""" + + super().update_from_dict(data) + self.available.process_key(data, "available") + self.binary_sensors.update_from_dict(data) + self.maximum_boiler_temperature.process_key(data, "maximum_boiler_temperature") + self.max_dhw_temperature.process_key(data, "max_dhw_temperature") + self.model_id.process_key(data, "model_id") + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) @dataclass -class OpenThermBinarySensors: - """OpenTherm binary_sensors class.""" +class ClimateDeviceBinarySensors: + """Climate-device binary_sensors class.""" - dhw_state: bool - heating_state: bool compressor_state: Optional[bool] = None cooling_enabled: Optional[bool] = None cooling_state: Optional[bool] = None + dhw_state: Optional[bool] = None flame_state: Optional[bool] = None + heating_state: bool = False secondary_boiler_state: Optional[bool] = None @dataclass -class OpenThermSensors: - """OpenTherm sensors class.""" +class ClimateDeviceSensors: + """Climate-device sensors class.""" - return_temperature: float - water_temperature: float dhw_temperature: Optional[float] = None domestic_hot_water_setpoint: Optional[float] = None intended_boiler_temperature: Optional[float] = None modulation_level: Optional[float] = None outdoor_air_temperature: Optional[float] = None + return_temperature: Optional[float] = None + water_temperature: Optional[float] = None water_pressure: Optional[float] = None @dataclass -class OpenThermSwitches: - """OpenTherm switches class.""" +class ClimateDeviceSwitches: + """Climate-device switches class.""" - dhw_cm_switch: bool + dhw_cm_switch: Optional[bool] = None cooling_ena_switch: Optional[bool] = None @@ -359,19 +410,31 @@ class OpenThermSwitches: class Plug(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - available: bool - switches: PlugSwitches - zigbee_mac_address: str - sensors: Optional[PlugSensors] = None + super().__init__() + zigbee_mac_address: str = "" + available: bool = False hardware: Optional[str] = None model_id: Optional[str] = None + sensors: Optional[PlugSensors] = None + switches: Optional[PlugSwitches] = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this Plug object with data from a dictionary.""" + + super().update_from_dict(data) + self.zigbee_mac_address.process_key(data, "zigbee_mac_address") + self.available.process_key(data, "available") + self.hardware.process_key(data, "hardware") + self.model_id.process_key(data, "model_id") + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) @dataclass class PlugSensors: """Plug sensors class.""" - electricity_consumed_interval: float + electricity_consumed_interval: float = 0.0 electricity_consumed: Optional[float] = None # Not present for Aqara Plug electricity_produced: Optional[float] = None electricity_produced_interval: Optional[float] = None @@ -425,55 +488,32 @@ class PlugwiseData: - ?? """ - adam: AdamGateway - smile_t: SmileTGateway - smile_p1: SmileP1Gateway - stretch: StretchGateway - onoff: OnOff - opentherm: OpenTherm - zones: list[Zone] - weather: Weather - anna: Anna - anna_adam: AnnaAdam - lisa: EmmaJipLisaTom - jip: EmmaJipLisaTom - tom_floor: EmmaJipLisaTom - plug: Plug - p1_dsmr: SmartEnergyMeter + gateway: Gateway = Gateway() + climate_device: ClimateDevice = ClimateDevice() + zones: list[Zone] = [] + thermostats: list[Thermostat] = [] + plugs: list[Plug] = [] + p1_dsmr: SmartEnergyMeter = SmartEnergyMeter() def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: """Update the status object with data received from the Plugwise API.""" - if "adam" in data: - self.adam.update_from_dict(data["adam"]) - if "smile_t" in data: - self.smile_t.update_from_dict(data["smile_t"]) - # if "smile_t_p1" in data: - # self.smile_t_p1.update_from_dict(data["smile_t_p1"]) - if "smile_p1" in data: - self.smile_p1.update_from_dict(data["smile_p1"]) - if "stretch" in data: - self.stretch.update_from_dict(data["stretch"]) - if "onoff" in data: - self.onoff.update_from_dict(data["onoff"]) - if "opentherm" in data: - self.opentherm.update_from_dict(data["opentherm"]) - if "zones" in data: - self.zones.update_from_dict(data["zones"]) - if "anna" in data: - self.anna.update_from_dict(data["anna"]) - if "anna_adam" in data: - self.anna_adam.update_from_dict(data["anna_adam"]) - if "lisa" in data: - self.lisa.update_from_dict(data["lisa"]) - if "jip" in data: - self.zones.update_from_dict(data["jip"]) - if "tom_floor" in data: - self.tom_floor.update_from_dict(data["tom_floor"]) - if "plug" in data: - self.plug.update_from_dict(data["plug"]) - # if "aqara_plug" in data: - # self.opentherm.update_from_dict(data["aqara_plug"]) - # if "misc_plug" in data: - # self.misc_plug.update_from_dict(data["misc_plug"]) - if "p1_dsmr" in data: - self.p1_dsmr.update_from_dict(data["p1_dsmr"]) + + for device_id, device in data: + if device["device_class"] == "gateway": + self.gateway.update_from_dict(device) + if device["device_class"] == "heater_central": + self.climate_device.update_from_dict(device) + if device["device_class"] == "climate": + for zone in self.zones: + if zone.location == device_id: + zone.update_from_dict(device) + if device["device_class"] in ZONE_THERMOSTATS: + for thermostat in self.thermostats: + if thermostat.location == device["location"]: + thermostat.update_from_dict(device) + if device["device_class"].endswith("_plug"): + for plug in self.plugs: + if plug.location == device["location"]: + plug.update_from_dict(device) + if device["device_class"] == "smartmeter": + self.p1_dsmr.update_from_dict(device) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 87d338f26..54a21c15d 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -20,10 +20,7 @@ RULES, STATE_OFF, STATE_ON, - PlugwiseAdamData, - PlugwiseAnnaData, - PlugwiseP1Data, - PlugwiseStretchData, + GwEntityData, ThermoLoc, ) from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError @@ -90,11 +87,7 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update( - self, - ) -> dict[ - str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData - ]: + async def async_update(self) -> dict[str, GwEntityData]: """Perform an full update update at day-change: re-collect all gateway entities and their data and states. Otherwise perform an incremental update: only collect the entities updated data and states. diff --git a/plugwise/smile.py b/plugwise/smile.py index 5b9458431..22010af5c 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -25,10 +25,7 @@ RULES, STATE_OFF, STATE_ON, - PlugwiseAdamData, - PlugwiseAnnaData, - PlugwiseP1Data, - PlugwiseStretchData, + GwEntityData, SwitchType, ThermoLoc, ) @@ -123,11 +120,7 @@ def get_all_gateway_entities(self) -> None: self._all_entity_data() - async def async_update( - self, - ) -> dict[ - str, PlugwiseAnnaData | PlugwiseAdamData | PlugwiseP1Data | PlugwiseStretchData - ]: + async def async_update(self) -> dict[str, GwEntityData]: """Perform an full update: re-collect all gateway entities and their data and states. Any change in the connected entities will be detected immediately. From 9847605ffec14f1e2cde4fd0623751b29674e7c0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 11:29:29 +0100 Subject: [PATCH 37/49] Try 2 --- fixtures/adam_heatpump_cooling/data.json | 186 +++++++++++++++++++---- plugwise/devices.py | 2 +- 2 files changed, 156 insertions(+), 32 deletions(-) diff --git a/fixtures/adam_heatpump_cooling/data.json b/fixtures/adam_heatpump_cooling/data.json index 71bfad7e2..4075bf756 100644 --- a/fixtures/adam_heatpump_cooling/data.json +++ b/fixtures/adam_heatpump_cooling/data.json @@ -12,7 +12,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer SJ", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -27,11 +33,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["d3a276aeb3114a509bab1e4bf8c40348"], + "primary": [ + "d3a276aeb3114a509bab1e4bf8c40348" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "0ca13e8176204ca7bf6f09de59f81c83": { "available": true, @@ -132,7 +144,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer DB", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -147,11 +165,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["47e2c550a33846b680725aa3fb229473"], + "primary": [ + "47e2c550a33846b680725aa3fb229473" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "2e0fc4db2a6d4cbeb7cf786143543961": { "available": true, @@ -248,7 +272,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Badkamer 2", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "Werkdag schema", "select_zone_profile": "passive", "sensors": { @@ -262,11 +292,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["f04c985c11ad4848b8fcd710343f9dcf"], + "primary": [ + "f04c985c11ad4848b8fcd710343f9dcf" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "5ead63c65e5f44e7870ba2bd680ceb9e": { "available": true, @@ -294,7 +330,11 @@ }, "dev_class": "gateway", "firmware": "3.2.8", - "gateway_modes": ["away", "full", "vacation"], + "gateway_modes": [ + "away", + "full", + "vacation" + ], "hardware": "AME Smile 2.0 board", "location": "eedadcb297564f1483faa509179aebed", "mac_address": "012345670001", @@ -392,7 +432,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Badkamer 1", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "Werkdag schema", "select_zone_profile": "passive", "sensors": { @@ -407,11 +453,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["eac5db95d97241f6b17790897847ccf5"], + "primary": [ + "eac5db95d97241f6b17790897847ccf5" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "93ac3f7bf25342f58cbb77c4a99ac0b3": { "active_preset": "away", @@ -426,7 +478,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer RB", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -440,11 +498,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["c4ed311d54e341f58b4cdd201d1fde7e"], + "primary": [ + "c4ed311d54e341f58b4cdd201d1fde7e" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "96714ad90fc948bcbcb5021c4b9f5ae9": { "available": true, @@ -479,7 +543,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer SQ", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -494,11 +564,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["beb32da072274e698146db8b022f3c36"], + "primary": [ + "beb32da072274e698146db8b022f3c36" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "a03b6e8e76dd4646af1a77c31dd9370c": { "available": true, @@ -533,7 +609,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Keuken", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "Werkdag schema", "select_zone_profile": "active", "sensors": { @@ -548,11 +630,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["ea8372c0e3ad4622ad45a041d02425f5"], + "primary": [ + "ea8372c0e3ad4622ad45a041d02425f5" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "b52908550469425b812c87f766fe5303": { "active_preset": "away", @@ -567,7 +655,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Bijkeuken", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "active", "sensors": { @@ -582,11 +676,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["1053c8bbf8be43c6921742b146a625f1"], + "primary": [ + "1053c8bbf8be43c6921742b146a625f1" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "bbcffa48019f4b09b8368bbaf9559e68": { "available": true, @@ -699,7 +799,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer JM", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -714,11 +820,17 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": ["7fda9f84f01342f8afe9ebbbbff30c0f"], + "primary": [ + "7fda9f84f01342f8afe9ebbbbff30c0f" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] }, "ea8372c0e3ad4622ad45a041d02425f5": { "available": true, @@ -803,7 +915,13 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Woonkamer", - "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], + "preset_modes": [ + "no_frost", + "vacation", + "away", + "home", + "asleep" + ], "select_schedule": "Werkdag schema", "select_zone_profile": "active", "sensors": { @@ -818,10 +936,16 @@ "upper_bound": 35.0 }, "thermostats": { - "primary": ["ca79d23ae0094120b877558734cff85c"], + "primary": [ + "ca79d23ae0094120b877558734cff85c" + ], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": ["active", "off", "passive"] + "zone_profiles": [ + "active", + "off", + "passive" + ] } } diff --git a/plugwise/devices.py b/plugwise/devices.py index ad8c3f29e..2ac68c021 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -485,7 +485,7 @@ class PlugwiseData: - Gateway Stretch (legacy) - Single devices (Zigbee) - - ?? + - ??. """ gateway: Gateway = Gateway() From e3ca0a025738cba3e88c9cc6063d72a04ab2e9f2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 12:37:37 +0100 Subject: [PATCH 38/49] Ruff fixes --- fixtures/adam_heatpump_cooling/data.json | 186 +++----------- plugwise/devices.py | 312 +++++++++++++---------- 2 files changed, 207 insertions(+), 291 deletions(-) diff --git a/fixtures/adam_heatpump_cooling/data.json b/fixtures/adam_heatpump_cooling/data.json index 4075bf756..71bfad7e2 100644 --- a/fixtures/adam_heatpump_cooling/data.json +++ b/fixtures/adam_heatpump_cooling/data.json @@ -12,13 +12,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer SJ", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -33,17 +27,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "d3a276aeb3114a509bab1e4bf8c40348" - ], + "primary": ["d3a276aeb3114a509bab1e4bf8c40348"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "0ca13e8176204ca7bf6f09de59f81c83": { "available": true, @@ -144,13 +132,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer DB", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -165,17 +147,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "47e2c550a33846b680725aa3fb229473" - ], + "primary": ["47e2c550a33846b680725aa3fb229473"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "2e0fc4db2a6d4cbeb7cf786143543961": { "available": true, @@ -272,13 +248,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Badkamer 2", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "Werkdag schema", "select_zone_profile": "passive", "sensors": { @@ -292,17 +262,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "f04c985c11ad4848b8fcd710343f9dcf" - ], + "primary": ["f04c985c11ad4848b8fcd710343f9dcf"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "5ead63c65e5f44e7870ba2bd680ceb9e": { "available": true, @@ -330,11 +294,7 @@ }, "dev_class": "gateway", "firmware": "3.2.8", - "gateway_modes": [ - "away", - "full", - "vacation" - ], + "gateway_modes": ["away", "full", "vacation"], "hardware": "AME Smile 2.0 board", "location": "eedadcb297564f1483faa509179aebed", "mac_address": "012345670001", @@ -432,13 +392,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Badkamer 1", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "Werkdag schema", "select_zone_profile": "passive", "sensors": { @@ -453,17 +407,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "eac5db95d97241f6b17790897847ccf5" - ], + "primary": ["eac5db95d97241f6b17790897847ccf5"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "93ac3f7bf25342f58cbb77c4a99ac0b3": { "active_preset": "away", @@ -478,13 +426,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer RB", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -498,17 +440,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "c4ed311d54e341f58b4cdd201d1fde7e" - ], + "primary": ["c4ed311d54e341f58b4cdd201d1fde7e"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "96714ad90fc948bcbcb5021c4b9f5ae9": { "available": true, @@ -543,13 +479,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer SQ", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -564,17 +494,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "beb32da072274e698146db8b022f3c36" - ], + "primary": ["beb32da072274e698146db8b022f3c36"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "a03b6e8e76dd4646af1a77c31dd9370c": { "available": true, @@ -609,13 +533,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Keuken", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "Werkdag schema", "select_zone_profile": "active", "sensors": { @@ -630,17 +548,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "ea8372c0e3ad4622ad45a041d02425f5" - ], + "primary": ["ea8372c0e3ad4622ad45a041d02425f5"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "b52908550469425b812c87f766fe5303": { "active_preset": "away", @@ -655,13 +567,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Bijkeuken", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "active", "sensors": { @@ -676,17 +582,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "1053c8bbf8be43c6921742b146a625f1" - ], + "primary": ["1053c8bbf8be43c6921742b146a625f1"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "bbcffa48019f4b09b8368bbaf9559e68": { "available": true, @@ -799,13 +699,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Slaapkamer JM", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "off", "select_zone_profile": "passive", "sensors": { @@ -820,17 +714,11 @@ "upper_bound": 99.9 }, "thermostats": { - "primary": [ - "7fda9f84f01342f8afe9ebbbbff30c0f" - ], + "primary": ["7fda9f84f01342f8afe9ebbbbff30c0f"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] }, "ea8372c0e3ad4622ad45a041d02425f5": { "available": true, @@ -915,13 +803,7 @@ "dev_class": "climate", "model": "ThermoZone", "name": "Woonkamer", - "preset_modes": [ - "no_frost", - "vacation", - "away", - "home", - "asleep" - ], + "preset_modes": ["no_frost", "vacation", "away", "home", "asleep"], "select_schedule": "Werkdag schema", "select_zone_profile": "active", "sensors": { @@ -936,16 +818,10 @@ "upper_bound": 35.0 }, "thermostats": { - "primary": [ - "ca79d23ae0094120b877558734cff85c" - ], + "primary": ["ca79d23ae0094120b877558734cff85c"], "secondary": [] }, "vendor": "Plugwise", - "zone_profiles": [ - "active", - "off", - "passive" - ] + "zone_profiles": ["active", "off", "passive"] } } diff --git a/plugwise/devices.py b/plugwise/devices.py index 2ac68c021..85ef9566c 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from .constants import ZONE_THERMOSTATS @@ -17,10 +17,7 @@ def process_key(data: dict[str, Any], key: str) -> Any | None: return None -def process_dict( - data: dict[str, Any], - dict_type: str, - key: str) -> Any | None: +def process_dict(data: dict[str, Any], dict_type: str, key: str) -> Any | None: """Return the key value from the data dict, when present.""" if dict_type in data and key in data[dict_type]: @@ -36,13 +33,13 @@ class DeviceBase: Every device will have most of these data points. """ - dev_class: Optional[str] = None - firmware: Optional[str] = None - location: Optional[str] = None - mac_address: Optional[str] = None - model: Optional[str] = None - name: Optional[str] = None - vendor: Optional[str] = None + dev_class: str | None = None + firmware: str | None = None + location: str | None = None + mac_address: str | None = None + model: str | None = None + name: str | None = None + vendor: str | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this DeviceBase object with data from a dictionary.""" @@ -61,15 +58,15 @@ class Gateway(DeviceBase): """Plugwise Gateway class.""" super().__init__() - binary_sensors: Optional[GatewayBinarySensors] = None - gateway_modes: Optional[list[str]] = None - hardware: Optional[str] = None - model_id: Optional[str] = None - regulation_modes: Optional[list[str]] = None - select_gateway_mode: Optional[str] = None - select_regulation_mode: Optional[str] = None - sensors: Optional[Weather]= None - zigbee_mac_address: Optional[str] = None + binary_sensors: GatewayBinarySensors | None = None + gateway_modes: list[str] | None = None + hardware: str | None = None + model_id: str | None = None + regulation_modes: list[str] | None = None + select_gateway_mode: str | None = None + select_regulation_mode: str | None = None + sensors: Weather | None = None + zigbee_mac_address: str | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Gateway object with data from a dictionary.""" @@ -103,7 +100,7 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class Weather: """Gateway weather sensor class.""" - outdoor_temperature: Optional[float] = None + outdoor_temperature: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this GatewayBinarySensors object with data from a dictionary.""" @@ -116,8 +113,8 @@ class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" super().__init__() - available: Optional[bool] = None - sensors: Optional[SmartEnergySensors] = None + available: bool | None = None + sensors: SmartEnergySensors | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergyMeter object with data from a dictionary.""" @@ -133,59 +130,107 @@ class SmartEnergySensors: electricity_consumed_off_peak_cumulative: float = 0.0 electricity_consumed_off_peak_interval: int = 0 - electricity_consumed_off_peak_point: Optional[int] = None + electricity_consumed_off_peak_point: int | None = None electricity_consumed_peak_cumulative: float = 0.0 electricity_consumed_peak_interval: int = 0 - electricity_consumed_peak_point: Optional[int] = None - electricity_consumed_point: Optional[int] = None + electricity_consumed_peak_point: int | None = None + electricity_consumed_point: int | None = None electricity_phase_one_consumed: int = 0 electricity_phase_one_produced: int = 0 electricity_produced_off_peak_cumulative: float = 0.0 electricity_produced_off_peak_interval: int = 0 - electricity_produced_off_peak_point: Optional[int] = None + electricity_produced_off_peak_point: int | None = None electricity_produced_peak_cumulative: float = 0.0 electricity_produced_peak_interval: int = 0 - electricity_produced_peak_point: Optional[int] = None - electricity_produced_point: Optional[int] = None + electricity_produced_peak_point: int | None = None + electricity_produced_point: int | None = None net_electricity_cumulative: float = 0.0 net_electricity_point: int = 0 - electricity_phase_three_consumed: Optional[int] = None - electricity_phase_three_produced: Optional[int] = None - electricity_phase_two_consumed: Optional[int] = None - electricity_phase_two_produced: Optional[int] = None - gas_consumed_cumulative: Optional[float] = None - gas_consumed_interval: Optional[float] = None - voltage_phase_one: Optional[float] = None - voltage_phase_three: Optional[float] = None - voltage_phase_two: Optional[float] = None + electricity_phase_three_consumed: int | None = None + electricity_phase_three_produced: int | None = None + electricity_phase_two_consumed: int | None = None + electricity_phase_two_produced: int | None = None + gas_consumed_cumulative: float | None = None + gas_consumed_interval: float | None = None + voltage_phase_one: float | None = None + voltage_phase_three: float | None = None + voltage_phase_two: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergySensors object with data from a dictionary.""" - self.electricity_consumed_off_peak_cumulative.process_dict(data, "sensors", "electricity_consumed_off_peak_cumulative") - self.electricity_consumed_off_peak_interval.process_dict(data, "sensors", "electricity_consumed_off_peak_interval") - self.electricity_consumed_off_peak_point.process_dict(data, "sensors", "electricity_consumed_off_peak_point") - self.electricity_consumed_peak_cumulative.process_dict(data, "sensors", "electricity_consumed_peak_cumulative") - self.electricity_consumed_peak_interval.process_dict(data, "sensors", "electricity_consumed_peak_interval") - self.electricity_consumed_peak_point.process_dict(data, "sensors", "electricity_consumed_peak_point") - self.electricity_consumed_point.process_dict(data, "sensors", "electricity_consumed_point") - self.electricity_phase_one_consumed.process_dict(data, "sensors", "electricity_phase_one_consumed") - self.electricity_phase_one_produced.process_dict(data, "sensors", "electricity_phase_one_produced") - self.electricity_produced_off_peak_cumulative.process_dict(data, "sensors", "electricity_produced_off_peak_cumulative") - self.electricity_produced_off_peak_interval.process_dict(data, "sensors", "electricity_produced_off_peak_interval") - self.electricity_produced_off_peak_point.process_dict(data, "sensors", "electricity_produced_off_peak_point") - self.electricity_produced_peak_cumulative.process_dict(data, "sensors", "electricity_produced_peak_cumulative") - self.electricity_produced_peak_interval.process_dict(data, "sensors", "electricity_produced_peak_interval") - self.electricity_produced_peak_point.process_dict(data, "sensors", "electricity_produced_peak_point") - self.electricity_produced_point.process_dict(data, "sensors", "electricity_produced_point") - self.net_electricity_cumulative.process_dict(data, "sensors", "net_electricity_cumulative") - self.net_electricity_point.process_dict(data, "sensors", "net_electricity_point") - self.electricity_phase_three_consumed.process_dict(data, "sensors", "electricity_phase_three_consumed") - self.electricity_phase_three_produced.process_dict(data, "sensors", "electricity_phase_three_produced") - self.electricity_phase_two_consumed.process_dict(data, "sensors", "electricity_phase_two_consumed") - self.electricity_phase_two_produced.process_dict(data, "sensors", "electricity_phase_two_produced") - self.gas_consumed_cumulative.process_dict(data, "sensors", "gas_consumed_cumulative") - self.gas_consumed_interval.process_dict(data, "sensors", "gas_consumed_interval") + self.electricity_consumed_off_peak_cumulative.process_dict( + data, "sensors", "electricity_consumed_off_peak_cumulative" + ) + self.electricity_consumed_off_peak_interval.process_dict( + data, "sensors", "electricity_consumed_off_peak_interval" + ) + self.electricity_consumed_off_peak_point.process_dict( + data, "sensors", "electricity_consumed_off_peak_point" + ) + self.electricity_consumed_peak_cumulative.process_dict( + data, "sensors", "electricity_consumed_peak_cumulative" + ) + self.electricity_consumed_peak_interval.process_dict( + data, "sensors", "electricity_consumed_peak_interval" + ) + self.electricity_consumed_peak_point.process_dict( + data, "sensors", "electricity_consumed_peak_point" + ) + self.electricity_consumed_point.process_dict( + data, "sensors", "electricity_consumed_point" + ) + self.electricity_phase_one_consumed.process_dict( + data, "sensors", "electricity_phase_one_consumed" + ) + self.electricity_phase_one_produced.process_dict( + data, "sensors", "electricity_phase_one_produced" + ) + self.electricity_produced_off_peak_cumulative.process_dict( + data, "sensors", "electricity_produced_off_peak_cumulative" + ) + self.electricity_produced_off_peak_interval.process_dict( + data, "sensors", "electricity_produced_off_peak_interval" + ) + self.electricity_produced_off_peak_point.process_dict( + data, "sensors", "electricity_produced_off_peak_point" + ) + self.electricity_produced_peak_cumulative.process_dict( + data, "sensors", "electricity_produced_peak_cumulative" + ) + self.electricity_produced_peak_interval.process_dict( + data, "sensors", "electricity_produced_peak_interval" + ) + self.electricity_produced_peak_point.process_dict( + data, "sensors", "electricity_produced_peak_point" + ) + self.electricity_produced_point.process_dict( + data, "sensors", "electricity_produced_point" + ) + self.net_electricity_cumulative.process_dict( + data, "sensors", "net_electricity_cumulative" + ) + self.net_electricity_point.process_dict( + data, "sensors", "net_electricity_point" + ) + self.electricity_phase_three_consumed.process_dict( + data, "sensors", "electricity_phase_three_consumed" + ) + self.electricity_phase_three_produced.process_dict( + data, "sensors", "electricity_phase_three_produced" + ) + self.electricity_phase_two_consumed.process_dict( + data, "sensors", "electricity_phase_two_consumed" + ) + self.electricity_phase_two_produced.process_dict( + data, "sensors", "electricity_phase_two_produced" + ) + self.gas_consumed_cumulative.process_dict( + data, "sensors", "gas_consumed_cumulative" + ) + self.gas_consumed_interval.process_dict( + data, "sensors", "gas_consumed_interval" + ) self.voltage_phase_one.process_dict(data, "sensors", "voltage_phase_one") self.voltage_phase_three.process_dict(data, "sensors", "voltage_phase_three") self.voltage_phase_two.process_dict(data, "sensors", "voltage_phase_two") @@ -202,14 +247,13 @@ class Zone(DeviceBase): preset_modes: list[str] = [] select_zone_profile: str = "off" zone_profiles: list[str] = [] - active_preset: Optional[str] = None - hardware: Optional[str] = None - model_id: Optional[str] = None - select_schedule: Optional[str] = None - sensors: Optional[ZoneSensors] = None - thermostat: Optional[ThermostatDict] = None - thermostats: Optional[ThermostatsDict] = None - + active_preset: str | None = None + hardware: str | None = None + model_id: str | None = None + select_schedule: str | None = None + sensors: ZoneSensors | None = None + thermostat: ThermostatDict | None = None + thermostats: ThermostatsDict | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this climate Zone object with data from a dictionary.""" @@ -234,9 +278,9 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class ZoneSensors: """Climate Zone sensors class.""" - electricity_consumed: Optional[float] | None = None - electricity_produced: Optional[float] | None = None - temperature: Optional[float] | None = None + electricity_consumed: float | None = None + electricity_produced: float | None = None + temperature: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ZoneSensors object with data from a dictionary.""" @@ -248,25 +292,22 @@ def update_from_dict(self, data: dict[str, Any]) -> None: @dataclass class Thermostat(DeviceBase): - """Plugwise Thermostat class, covering Anna (legacy) standalone or wired to Adam, - - Emma Essential/Pro standalone, or Emma Pro, Jip, Lisa and Tom/Floor connected to Adam. - """ + """Plugwise Thermostat class, covering Anna (legacy) standalone or wired to Adam, Emma Essential/Pro standalone, or Emma Pro, Jip, Lisa and Tom/Floor connected to Adam.""" super().__init__() available_schedules: list[str] = [] control_state: str = "heating" preset_modes: list[str] = [] - active_preset: Optional[str] = None - binary_sensors: Optional[WirelessThermostatBinarySensors] = None - climate_mode: Optional[str] = None - hardware: Optional[str] = None - model_id: Optional[str] = None - select_schedule: Optional[str] = None - sensors: Optional[ThermostatSensors] = None - temperature_offset: Optional[SetpointDict] = None # not for legacy - thermostat: Optional[ThermostatDict] = None - zigbee_mac_address: Optional[str] = None + active_preset: str | None = None + binary_sensors: WirelessThermostatBinarySensors | None = None + climate_mode: str | None = None + hardware: str | None = None + model_id: str | None = None + select_schedule: str | None = None + sensors: ThermostatSensors | None = None + temperature_offset: SetpointDict | None = None # not for legacy + thermostat: ThermostatDict | None = None + zigbee_mac_address: str | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Thermostat object with data from a dictionary.""" @@ -281,7 +322,7 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.hardware.process_key(data, "hardware") self.model_id.process_key(data, "model_id") self.select_schedule.process_key(data, "select_schedule") - self.sensors.update_from_dict(data) + self.sensors.update_from_dict(data) self.temperature_offset.process_key(data, "temperature_offset") self.thermostat.process_key(data, "thermostat") self.zigbee_mac_address.process_key(data, "zigbee_mac_address") @@ -291,15 +332,15 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class ThermostatSensors: """Thermostat sensors class.""" - battery: Optional[int] = None # not when AC powered, Lisa/Tom/Floor - humidity: Optional[int] = None # Emma and Jip - illuminance: Optional[float] = None # Anna + battery: int | None = None # not when AC powered, Lisa/Tom/Floor + humidity: int | None = None # Emma and Jip + illuminance: float | None = None # Anna temperature: float = 0.0 - setpoint: Optional[float] = None # heat or cool - setpoint_high: Optional[float] = None # heat_cool - setpoint_low: Optional[float] = None # heat_cool - temperature_difference: Optional[float] = None # Tom/Floor - valve_position: Optional[float] = None # Tom/Floor + setpoint: float | None = None # heat or cool + setpoint_high: float | None = None # heat_cool + setpoint_low: float | None = None # heat_cool + temperature_difference: float | None = None # Tom/Floor + valve_position: float | None = None # Tom/Floor @dataclass @@ -329,9 +370,9 @@ class ThermostatDict: lower_bound: float = 0.0 resolution: float = 0.0 upper_bound: float = 0.0 - setpoint: Optional[float] = None # heat or cool - setpoint_high: Optional[float] = None # heat_cool - setpoint_low: Optional[float] = None # heat_cool + setpoint: float | None = None # heat or cool + setpoint_high: float | None = None # heat_cool + setpoint_low: float | None = None # heat_cool @dataclass @@ -350,13 +391,13 @@ class ClimateDevice(DeviceBase): """ super().__init__() - available: Optional[bool] = None - binary_sensors: Optional[ClimateDeviceBinarySensors] = None - maximum_boiler_temperature: Optional[SetpointDict] = None - max_dhw_temperature: Optional[SetpointDict] = None - model_id: Optional[str] = None - sensors: Optional[ClimateDeviceSensors] = None - switches: Optional[ClimateDeviceSwitches] = None + available: bool | None = None + binary_sensors: ClimateDeviceBinarySensors | None = None + maximum_boiler_temperature: SetpointDict | None = None + max_dhw_temperature: SetpointDict | None = None + model_id: str | None = None + sensors: ClimateDeviceSensors | None = None + switches: ClimateDeviceSwitches | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDevice object with data from a dictionary.""" @@ -367,43 +408,43 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.maximum_boiler_temperature.process_key(data, "maximum_boiler_temperature") self.max_dhw_temperature.process_key(data, "max_dhw_temperature") self.model_id.process_key(data, "model_id") - self.sensors.update_from_dict(data) - self.switches.update_from_dict(data) + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) @dataclass class ClimateDeviceBinarySensors: """Climate-device binary_sensors class.""" - compressor_state: Optional[bool] = None - cooling_enabled: Optional[bool] = None - cooling_state: Optional[bool] = None - dhw_state: Optional[bool] = None - flame_state: Optional[bool] = None + compressor_state: bool | None = None + cooling_enabled: bool | None = None + cooling_state: bool | None = None + dhw_state: bool | None = None + flame_state: bool | None = None heating_state: bool = False - secondary_boiler_state: Optional[bool] = None + secondary_boiler_state: bool | None = None @dataclass class ClimateDeviceSensors: """Climate-device sensors class.""" - dhw_temperature: Optional[float] = None - domestic_hot_water_setpoint: Optional[float] = None - intended_boiler_temperature: Optional[float] = None - modulation_level: Optional[float] = None - outdoor_air_temperature: Optional[float] = None - return_temperature: Optional[float] = None - water_temperature: Optional[float] = None - water_pressure: Optional[float] = None + dhw_temperature: float | None = None + domestic_hot_water_setpoint: float | None = None + intended_boiler_temperature: float | None = None + modulation_level: float | None = None + outdoor_air_temperature: float | None = None + return_temperature: float | None = None + water_temperature: float | None = None + water_pressure: float | None = None @dataclass class ClimateDeviceSwitches: """Climate-device switches class.""" - dhw_cm_switch: Optional[bool] = None - cooling_ena_switch: Optional[bool] = None + dhw_cm_switch: bool | None = None + cooling_ena_switch: bool | None = None @dataclass @@ -413,10 +454,10 @@ class Plug(DeviceBase): super().__init__() zigbee_mac_address: str = "" available: bool = False - hardware: Optional[str] = None - model_id: Optional[str] = None - sensors: Optional[PlugSensors] = None - switches: Optional[PlugSwitches] = None + hardware: str | None = None + model_id: str | None = None + sensors: PlugSensors | None = None + switches: PlugSwitches | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" @@ -435,9 +476,9 @@ class PlugSensors: """Plug sensors class.""" electricity_consumed_interval: float = 0.0 - electricity_consumed: Optional[float] = None # Not present for Aqara Plug - electricity_produced: Optional[float] = None - electricity_produced_interval: Optional[float] = None + electricity_consumed: float | None = None # Not present for Aqara Plug + electricity_produced: float | None = None + electricity_produced_interval: float | None = None @dataclass @@ -445,13 +486,12 @@ class PlugSwitches: """Plug switches class.""" relay: bool - lock: Optional[bool] = None + lock: bool | None = None ################################################## class PlugwiseData: - """ - Overview of existing options: + """Overview of existing PlugwiseData options. - Gateway Adam - Climate device: OnOff or Opentherm @@ -485,7 +525,7 @@ class PlugwiseData: - Gateway Stretch (legacy) - Single devices (Zigbee) - - ??. + - ?? """ gateway: Gateway = Gateway() From 8e98702cba223e52d8a25f2d640d9ad3261c83a0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 12:52:18 +0100 Subject: [PATCH 39/49] Fixes --- plugwise/devices.py | 154 ++++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 85ef9566c..54ae0c1d6 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -44,13 +44,13 @@ class DeviceBase: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this DeviceBase object with data from a dictionary.""" - self.dev_class.process_key(data, "dev_class") - self.firmware.process_key(data, "firmware") - self.location.process_key(data, "location") - self.mac_address.process_key(data, "mac_address") - self.model.process_key(data, "model") - self.name.process_key(data, "name") - self.vendor.process_key(data, "vendor") + self.dev_class = process_key(data, "dev_class") + self.firmware = process_key(data, "firmware") + self.location = process_key(data, "location") + self.mac_address = process_key(data, "mac_address") + self.model = process_key(data, "model") + self.name = process_key(data, "name") + self.vendor = process_key(data, "vendor") @dataclass @@ -73,13 +73,13 @@ def update_from_dict(self, data: dict[str, Any]) -> None: super().update_from_dict(data) self.binary_sensors.update_from_dict(data) - self.gateway_modes.process_key(data, "gateway_mode") - self.hardware.process_key(data, "gateway_mode") - self.model_id.process_key(data, "gateway_mode") - self.regulation_modes.process_key(data, "gateway_mode") - self.select_gateway_mode.process_key(data, "gateway_mode") + self.gateway_modes = process_key(data, "gateway_mode") + self.hardware = process_key(data, "gateway_mode") + self.model_id = process_key(data, "gateway_mode") + self.regulation_modes = process_key(data, "gateway_mode") + self.select_gateway_mode = process_key(data, "gateway_mode") self.sensors.update_from_dict(data) - self.zigbee_mac_address.process_key(data, "gateway_mode") + self.zigbee_mac_address = process_key(data, "gateway_mode") @dataclass @@ -91,7 +91,7 @@ class GatewayBinarySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this GatewayBinarySensors object with data from a dictionary.""" - self.plugwise_notification.process_dict( + self.plugwise_notification = process_dict( data, "binary_sensors", "plugwise_notification" ) @@ -105,7 +105,7 @@ class Weather: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this GatewayBinarySensors object with data from a dictionary.""" - self.outdoor_temperature.process_dict(data, "sensors", "outdoor_temperature") + self.outdoor_temperature = process_dict(data, "sensors", "outdoor_temperature") @dataclass @@ -120,7 +120,7 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergyMeter object with data from a dictionary.""" super().update_from_dict(data) - self.available.process_key(data, "available") + self.available = process_key(data, "available") self.sensors.update_from_dict(data) @@ -159,81 +159,81 @@ class SmartEnergySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergySensors object with data from a dictionary.""" - self.electricity_consumed_off_peak_cumulative.process_dict( + self.electricity_consumed_off_peak_cumulative = process_dict( data, "sensors", "electricity_consumed_off_peak_cumulative" ) - self.electricity_consumed_off_peak_interval.process_dict( + self.electricity_consumed_off_peak_interval = process_dict( data, "sensors", "electricity_consumed_off_peak_interval" ) - self.electricity_consumed_off_peak_point.process_dict( + self.electricity_consumed_off_peak_point = process_dict( data, "sensors", "electricity_consumed_off_peak_point" ) - self.electricity_consumed_peak_cumulative.process_dict( + self.electricity_consumed_peak_cumulative = process_dict( data, "sensors", "electricity_consumed_peak_cumulative" ) - self.electricity_consumed_peak_interval.process_dict( + self.electricity_consumed_peak_interval = process_dict( data, "sensors", "electricity_consumed_peak_interval" ) - self.electricity_consumed_peak_point.process_dict( + self.electricity_consumed_peak_point = process_dict( data, "sensors", "electricity_consumed_peak_point" ) - self.electricity_consumed_point.process_dict( + self.electricity_consumed_point = process_dict( data, "sensors", "electricity_consumed_point" ) - self.electricity_phase_one_consumed.process_dict( + self.electricity_phase_one_consumed = process_dict( data, "sensors", "electricity_phase_one_consumed" ) - self.electricity_phase_one_produced.process_dict( + self.electricity_phase_one_produced = process_dict( data, "sensors", "electricity_phase_one_produced" ) - self.electricity_produced_off_peak_cumulative.process_dict( + self.electricity_produced_off_peak_cumulative = process_dict( data, "sensors", "electricity_produced_off_peak_cumulative" ) - self.electricity_produced_off_peak_interval.process_dict( + self.electricity_produced_off_peak_interval = process_dict( data, "sensors", "electricity_produced_off_peak_interval" ) - self.electricity_produced_off_peak_point.process_dict( + self.electricity_produced_off_peak_point = process_dict( data, "sensors", "electricity_produced_off_peak_point" ) - self.electricity_produced_peak_cumulative.process_dict( + self.electricity_produced_peak_cumulative = process_dict( data, "sensors", "electricity_produced_peak_cumulative" ) - self.electricity_produced_peak_interval.process_dict( + self.electricity_produced_peak_interval = process_dict( data, "sensors", "electricity_produced_peak_interval" ) - self.electricity_produced_peak_point.process_dict( + self.electricity_produced_peak_point = process_dict( data, "sensors", "electricity_produced_peak_point" ) - self.electricity_produced_point.process_dict( + self.electricity_produced_point = process_dict( data, "sensors", "electricity_produced_point" ) - self.net_electricity_cumulative.process_dict( + self.net_electricity_cumulative = process_dict( data, "sensors", "net_electricity_cumulative" ) - self.net_electricity_point.process_dict( + self.net_electricity_point = process_dict( data, "sensors", "net_electricity_point" ) - self.electricity_phase_three_consumed.process_dict( + self.electricity_phase_three_consumed = process_dict( data, "sensors", "electricity_phase_three_consumed" ) - self.electricity_phase_three_produced.process_dict( + self.electricity_phase_three_produced = process_dict( data, "sensors", "electricity_phase_three_produced" ) - self.electricity_phase_two_consumed.process_dict( + self.electricity_phase_two_consumed = process_dict( data, "sensors", "electricity_phase_two_consumed" ) - self.electricity_phase_two_produced.process_dict( + self.electricity_phase_two_produced = process_dict( data, "sensors", "electricity_phase_two_produced" ) - self.gas_consumed_cumulative.process_dict( + self.gas_consumed_cumulative = process_dict( data, "sensors", "gas_consumed_cumulative" ) - self.gas_consumed_interval.process_dict( + self.gas_consumed_interval = process_dict( data, "sensors", "gas_consumed_interval" ) - self.voltage_phase_one.process_dict(data, "sensors", "voltage_phase_one") - self.voltage_phase_three.process_dict(data, "sensors", "voltage_phase_three") - self.voltage_phase_two.process_dict(data, "sensors", "voltage_phase_two") + self.voltage_phase_one = process_dict(data, "sensors", "voltage_phase_one") + self.voltage_phase_three = process_dict(data, "sensors", "voltage_phase_three") + self.voltage_phase_two = process_dict(data, "sensors", "voltage_phase_two") @dataclass @@ -259,19 +259,19 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this climate Zone object with data from a dictionary.""" super().update_from_dict(data) - self.available_schedules.process_key(data, "available_schedules") - self.climate_mode.process_key(data, "climate_mode") - self.control_state.process_key(data, "control_state") - self.preset_modes.process_key(data, "preset_modes") - self.select_zone_profile.process_key(data, "select_zone_profile") - self.zone_profiles.process_key(data, "zone_profiles") - self.active_preset.process_key(data, "active_preset") - self.hardware.process_key(data, "hardware") - self.model_id.process_key(data, "model_id") - self.select_schedule.process_key(data, "select_schedule") + self.available_schedules = process_key(data, "available_schedules") + self.climate_mode = process_key(data, "climate_mode") + self.control_state = process_key(data, "control_state") + self.preset_modes = process_key(data, "preset_modes") + self.select_zone_profile = process_key(data, "select_zone_profile") + self.zone_profiles = process_key(data, "zone_profiles") + self.active_preset = process_key(data, "active_preset") + self.hardware = process_key(data, "hardware") + self.model_id = process_key(data, "model_id") + self.select_schedule = process_key(data, "select_schedule") self.sensors.update_from_dict(data) - self.thermostat.process_key(data, "thermostat") - self.thermostats.process_key(data, "thermostats") + self.thermostat = process_key(data, "thermostat") + self.thermostats = process_key(data, "thermostats") @dataclass @@ -285,9 +285,9 @@ class ZoneSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ZoneSensors object with data from a dictionary.""" - self.electricity_consumed.process_dict(data, "sensors", "electricity_consumed") - self.electricity_produced.process_dict(data, "sensors", "electricity_produced") - self.temperature.process_dict(data, "sensors", "temperature") + self.electricity_consumed = process_dict(data, "sensors", "electricity_consumed") + self.electricity_produced = process_dict(data, "sensors", "electricity_produced") + self.temperature = process_dict(data, "sensors", "temperature") @dataclass @@ -313,19 +313,19 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Thermostat object with data from a dictionary.""" super().update_from_dict(data) - self.available_schedules.process_key(data, "available_schedules") - self.control_state.process_key(data, "control_state") - self.preset_modes.process_key(data, "preset_modes") - self.active_preset.process_key(data, "active_preset") + self.available_schedules = process_key(data, "available_schedules") + self.control_state = process_key(data, "control_state") + self.preset_modes = process_key(data, "preset_modes") + self.active_preset = process_key(data, "active_preset") self.binary_sensors.update_from_dict(data) - self.climate_mode.process_key(data, "climate_mode") - self.hardware.process_key(data, "hardware") - self.model_id.process_key(data, "model_id") - self.select_schedule.process_key(data, "select_schedule") + self.climate_mode = process_key(data, "climate_mode") + self.hardware = process_key(data, "hardware") + self.model_id = process_key(data, "model_id") + self.select_schedule = process_key(data, "select_schedule") self.sensors.update_from_dict(data) - self.temperature_offset.process_key(data, "temperature_offset") - self.thermostat.process_key(data, "thermostat") - self.zigbee_mac_address.process_key(data, "zigbee_mac_address") + self.temperature_offset = process_key(data, "temperature_offset") + self.thermostat = process_key(data, "thermostat") + self.zigbee_mac_address = process_key(data, "zigbee_mac_address") @dataclass @@ -403,11 +403,11 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDevice object with data from a dictionary.""" super().update_from_dict(data) - self.available.process_key(data, "available") + self.available = process_key(data, "available") self.binary_sensors.update_from_dict(data) - self.maximum_boiler_temperature.process_key(data, "maximum_boiler_temperature") - self.max_dhw_temperature.process_key(data, "max_dhw_temperature") - self.model_id.process_key(data, "model_id") + self.maximum_boiler_temperature = process_key(data, "maximum_boiler_temperature") + self.max_dhw_temperature = process_key(data, "max_dhw_temperature") + self.model_id = process_key(data, "model_id") self.sensors.update_from_dict(data) self.switches.update_from_dict(data) @@ -463,10 +463,10 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" super().update_from_dict(data) - self.zigbee_mac_address.process_key(data, "zigbee_mac_address") - self.available.process_key(data, "available") - self.hardware.process_key(data, "hardware") - self.model_id.process_key(data, "model_id") + self.zigbee_mac_address = process_key(data, "zigbee_mac_address") + self.available = process_key(data, "available") + self.hardware = process_key(data, "hardware") + self.model_id = process_key(data, "model_id") self.sensors.update_from_dict(data) self.switches.update_from_dict(data) From ef9bf3a7ac49b4599d41355b61ca1d47b21bb9f9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 12:57:26 +0100 Subject: [PATCH 40/49] Fixes 2 --- plugwise/devices.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 54ae0c1d6..82976afb9 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -57,7 +57,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class Gateway(DeviceBase): """Plugwise Gateway class.""" - super().__init__() binary_sensors: GatewayBinarySensors | None = None gateway_modes: list[str] | None = None hardware: str | None = None @@ -68,17 +67,23 @@ class Gateway(DeviceBase): sensors: Weather | None = None zigbee_mac_address: str | None = None + def __init__(self) -> None: + """Init Gateway class and inherited functions.""" + super().__init__() + def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Gateway object with data from a dictionary.""" super().update_from_dict(data) - self.binary_sensors.update_from_dict(data) + if self.binary_sensors: + self.binary_sensors.update_from_dict(data) self.gateway_modes = process_key(data, "gateway_mode") self.hardware = process_key(data, "gateway_mode") self.model_id = process_key(data, "gateway_mode") self.regulation_modes = process_key(data, "gateway_mode") self.select_gateway_mode = process_key(data, "gateway_mode") - self.sensors.update_from_dict(data) + if self.sensors: + self.sensors.update_from_dict(data) self.zigbee_mac_address = process_key(data, "gateway_mode") @@ -86,7 +91,7 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class GatewayBinarySensors: """Gateway binary_sensors class.""" - plugwise_notification: bool = False + plugwise_notification: bool | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this GatewayBinarySensors object with data from a dictionary.""" @@ -285,8 +290,12 @@ class ZoneSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ZoneSensors object with data from a dictionary.""" - self.electricity_consumed = process_dict(data, "sensors", "electricity_consumed") - self.electricity_produced = process_dict(data, "sensors", "electricity_produced") + self.electricity_consumed = process_dict( + data, "sensors", "electricity_consumed" + ) + self.electricity_produced = process_dict( + data, "sensors", "electricity_produced" + ) self.temperature = process_dict(data, "sensors", "temperature") @@ -405,7 +414,9 @@ def update_from_dict(self, data: dict[str, Any]) -> None: super().update_from_dict(data) self.available = process_key(data, "available") self.binary_sensors.update_from_dict(data) - self.maximum_boiler_temperature = process_key(data, "maximum_boiler_temperature") + self.maximum_boiler_temperature = process_key( + data, "maximum_boiler_temperature" + ) self.max_dhw_temperature = process_key(data, "max_dhw_temperature") self.model_id = process_key(data, "model_id") self.sensors.update_from_dict(data) From 6f0f690c05eb671d8c44e84e7c849d05b0ada294 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 13:11:09 +0100 Subject: [PATCH 41/49] Try 3 --- plugwise/devices.py | 287 ++++++++++++++++++++++++++++++-------------- 1 file changed, 194 insertions(+), 93 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 82976afb9..c79a5b3fe 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -26,7 +26,7 @@ def process_dict(data: dict[str, Any], dict_type: str, key: str) -> Any | None: return None -@dataclass +@dataclass(kw_only=True) class DeviceBase: """Plugwise Device Base class. @@ -53,18 +53,18 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.vendor = process_key(data, "vendor") -@dataclass +@dataclass(kw_only=True) class Gateway(DeviceBase): """Plugwise Gateway class.""" - binary_sensors: GatewayBinarySensors | None = None + binary_sensors: GatewayBinarySensors gateway_modes: list[str] | None = None hardware: str | None = None model_id: str | None = None regulation_modes: list[str] | None = None select_gateway_mode: str | None = None select_regulation_mode: str | None = None - sensors: Weather | None = None + sensors: Weather zigbee_mac_address: str | None = None def __init__(self) -> None: @@ -75,19 +75,17 @@ def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Gateway object with data from a dictionary.""" super().update_from_dict(data) - if self.binary_sensors: - self.binary_sensors.update_from_dict(data) + self.binary_sensors.update_from_dict(data) self.gateway_modes = process_key(data, "gateway_mode") self.hardware = process_key(data, "gateway_mode") self.model_id = process_key(data, "gateway_mode") self.regulation_modes = process_key(data, "gateway_mode") self.select_gateway_mode = process_key(data, "gateway_mode") - if self.sensors: - self.sensors.update_from_dict(data) + self.sensors.update_from_dict(data) self.zigbee_mac_address = process_key(data, "gateway_mode") -@dataclass +@dataclass(kw_only=True) class GatewayBinarySensors: """Gateway binary_sensors class.""" @@ -101,7 +99,7 @@ def update_from_dict(self, data: dict[str, Any]) -> None: ) -@dataclass +@dataclass(kw_only=True) class Weather: """Gateway weather sensor class.""" @@ -113,44 +111,47 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.outdoor_temperature = process_dict(data, "sensors", "outdoor_temperature") -@dataclass +@dataclass(kw_only=True) class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" - super().__init__() + sensors: SmartEnergySensors available: bool | None = None - sensors: SmartEnergySensors | None = None + + def __init__(self) -> None: + """Init SmartEnergyMeter class and inherited functions.""" + super().__init__() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergyMeter object with data from a dictionary.""" super().update_from_dict(data) - self.available = process_key(data, "available") self.sensors.update_from_dict(data) + self.available = process_key(data, "available") -@dataclass +@dataclass(kw_only=True) class SmartEnergySensors: """DSMR Energy Meter sensors class (P1 v4).""" - electricity_consumed_off_peak_cumulative: float = 0.0 - electricity_consumed_off_peak_interval: int = 0 + electricity_consumed_off_peak_cumulative: float | None = None + electricity_consumed_off_peak_interval: int | None = None electricity_consumed_off_peak_point: int | None = None - electricity_consumed_peak_cumulative: float = 0.0 - electricity_consumed_peak_interval: int = 0 + electricity_consumed_peak_cumulative: float | None = None + electricity_consumed_peak_interval: int | None = None electricity_consumed_peak_point: int | None = None electricity_consumed_point: int | None = None - electricity_phase_one_consumed: int = 0 - electricity_phase_one_produced: int = 0 - electricity_produced_off_peak_cumulative: float = 0.0 - electricity_produced_off_peak_interval: int = 0 + electricity_phase_one_consumed: int | None = None + electricity_phase_one_produced: int | None = None + electricity_produced_off_peak_cumulative: float | None = None + electricity_produced_off_peak_interval: int | None = None electricity_produced_off_peak_point: int | None = None - electricity_produced_peak_cumulative: float = 0.0 - electricity_produced_peak_interval: int = 0 + electricity_produced_peak_cumulative: float | None = None + electricity_produced_peak_interval: int | None = None electricity_produced_peak_point: int | None = None electricity_produced_point: int | None = None - net_electricity_cumulative: float = 0.0 - net_electricity_point: int = 0 + net_electricity_cumulative: float | None = None + net_electricity_point: int | None = None electricity_phase_three_consumed: int | None = None electricity_phase_three_produced: int | None = None electricity_phase_two_consumed: int | None = None @@ -241,29 +242,33 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.voltage_phase_two = process_dict(data, "sensors", "voltage_phase_two") -@dataclass +@dataclass(kw_only=True) class Zone(DeviceBase): """Plugwise climate Zone data class.""" - super().__init__() - available_schedules: list[str] = [] - climate_mode: str = "heat" - control_state: str = "heating" - preset_modes: list[str] = [] - select_zone_profile: str = "off" - zone_profiles: list[str] = [] + sensors: ZoneSensors + available_schedules: list[str] | None = None + climate_mode: str | None = None + control_state: str | None = None + preset_modes: list[str] | None = None + select_zone_profile: str | None = None + zone_profiles: list[str] | None = None active_preset: str | None = None hardware: str | None = None model_id: str | None = None select_schedule: str | None = None - sensors: ZoneSensors | None = None thermostat: ThermostatDict | None = None thermostats: ThermostatsDict | None = None + def __init__(self) -> None: + """Init Zone class and inherited functions.""" + super().__init__() + def update_from_dict(self, data: dict[str, Any]) -> None: """Update this climate Zone object with data from a dictionary.""" super().update_from_dict(data) + self.sensors.update_from_dict(data) self.available_schedules = process_key(data, "available_schedules") self.climate_mode = process_key(data, "climate_mode") self.control_state = process_key(data, "control_state") @@ -274,12 +279,11 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.hardware = process_key(data, "hardware") self.model_id = process_key(data, "model_id") self.select_schedule = process_key(data, "select_schedule") - self.sensors.update_from_dict(data) self.thermostat = process_key(data, "thermostat") self.thermostats = process_key(data, "thermostats") -@dataclass +@dataclass(kw_only=True) class ZoneSensors: """Climate Zone sensors class.""" @@ -299,131 +303,166 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.temperature = process_dict(data, "sensors", "temperature") -@dataclass +@dataclass(kw_only=True) class Thermostat(DeviceBase): """Plugwise Thermostat class, covering Anna (legacy) standalone or wired to Adam, Emma Essential/Pro standalone, or Emma Pro, Jip, Lisa and Tom/Floor connected to Adam.""" - super().__init__() - available_schedules: list[str] = [] - control_state: str = "heating" - preset_modes: list[str] = [] + binary_sensors: WirelessThermostatBinarySensors + sensors: ThermostatSensors + available_schedules: list[str] | None = None + control_state: str | None = None + preset_modes: list[str] | None = None active_preset: str | None = None - binary_sensors: WirelessThermostatBinarySensors | None = None climate_mode: str | None = None hardware: str | None = None model_id: str | None = None select_schedule: str | None = None - sensors: ThermostatSensors | None = None temperature_offset: SetpointDict | None = None # not for legacy thermostat: ThermostatDict | None = None zigbee_mac_address: str | None = None + def __init__(self) -> None: + """Init Thermostat class and inherited functions.""" + super().__init__() + def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Thermostat object with data from a dictionary.""" super().update_from_dict(data) + self.binary_sensors.update_from_dict(data) + self.sensors.update_from_dict(data) self.available_schedules = process_key(data, "available_schedules") self.control_state = process_key(data, "control_state") self.preset_modes = process_key(data, "preset_modes") self.active_preset = process_key(data, "active_preset") - self.binary_sensors.update_from_dict(data) self.climate_mode = process_key(data, "climate_mode") self.hardware = process_key(data, "hardware") self.model_id = process_key(data, "model_id") self.select_schedule = process_key(data, "select_schedule") - self.sensors.update_from_dict(data) self.temperature_offset = process_key(data, "temperature_offset") self.thermostat = process_key(data, "thermostat") self.zigbee_mac_address = process_key(data, "zigbee_mac_address") -@dataclass +@dataclass(kw_only=True) class ThermostatSensors: """Thermostat sensors class.""" battery: int | None = None # not when AC powered, Lisa/Tom/Floor humidity: int | None = None # Emma and Jip illuminance: float | None = None # Anna - temperature: float = 0.0 setpoint: float | None = None # heat or cool setpoint_high: float | None = None # heat_cool setpoint_low: float | None = None # heat_cool + temperature: float | None = None temperature_difference: float | None = None # Tom/Floor valve_position: float | None = None # Tom/Floor + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" -@dataclass + self.battery = process_dict(data, "sensors", "battery") + self.humidity = process_dict(data, "sensors", "humidity") + self.illuminance = process_dict(data, "sensors", "illuminance") + self.setpoint = process_dict(data, "sensors", "battsetpointery") + self.setpoint_high = process_dict(data, "sensors", "setpoint_high") + self.setpoint_low = process_dict(data, "sensors", "setpoint_low") + self.temperature = process_dict(data, "sensors", "temperature") + self.temperature_difference = process_dict( + data, "sensors", "temperature_difference" + ) + self.valve_position = process_dict(data, "sensors", "valve_position") + + +@dataclass(kw_only=True) class WirelessThermostatBinarySensors: """Wireless thermostat sensors class.""" - low_battery: bool = False + low_battery: bool | None = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" + + self.electricity_consumed = process_dict( + data, "sensors", "electricity_consumed" + ) -@dataclass +@dataclass(kw_only=True) class SetpointDict: """Generic setpoint dict class. Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. """ - lower_bound: float = 0.0 - resolution: float = 0.0 - setpoint: float = 0.0 - upper_bound: float = 0.0 + lower_bound: float | None = None + resolution: float | None = None + setpoint: float | None = None + upper_bound: float | None = None + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" -@dataclass + self.electricity_consumed = process_dict( + data, "sensors", "electricity_consumed" + ) + + +@dataclass(kw_only=True) class ThermostatDict: """Thermostat dict class.""" - lower_bound: float = 0.0 - resolution: float = 0.0 - upper_bound: float = 0.0 + lower_bound: float | None = None + resolution: float | None = None + upper_bound: float | None = None setpoint: float | None = None # heat or cool setpoint_high: float | None = None # heat_cool setpoint_low: float | None = None # heat_cool -@dataclass +@dataclass(kw_only=True) class ThermostatsDict: """Thermostats dict class.""" - primary: list[str] = [] - secondary: list[str] = [] + primary: list[str] | None = None + secondary: list[str] | None = None -@dataclass +@dataclass(kw_only=True) class ClimateDevice(DeviceBase): """Climate-device class. Representing both OnOff and OpenTherm types. """ - super().__init__() + binary_sensors: ClimateDeviceBinarySensors + sensors: ClimateDeviceSensors + switches: ClimateDeviceSwitches available: bool | None = None - binary_sensors: ClimateDeviceBinarySensors | None = None maximum_boiler_temperature: SetpointDict | None = None max_dhw_temperature: SetpointDict | None = None model_id: str | None = None - sensors: ClimateDeviceSensors | None = None - switches: ClimateDeviceSwitches | None = None + + def __init__(self) -> None: + """Init ClimateDevice class and inherited functions.""" + super().__init__() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDevice object with data from a dictionary.""" super().update_from_dict(data) - self.available = process_key(data, "available") self.binary_sensors.update_from_dict(data) + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) + self.available = process_key(data, "available") self.maximum_boiler_temperature = process_key( data, "maximum_boiler_temperature" ) self.max_dhw_temperature = process_key(data, "max_dhw_temperature") self.model_id = process_key(data, "model_id") - self.sensors.update_from_dict(data) - self.switches.update_from_dict(data) -@dataclass +@dataclass(kw_only=True) class ClimateDeviceBinarySensors: """Climate-device binary_sensors class.""" @@ -432,11 +471,24 @@ class ClimateDeviceBinarySensors: cooling_state: bool | None = None dhw_state: bool | None = None flame_state: bool | None = None - heating_state: bool = False + heating_state: bool | None = None secondary_boiler_state: bool | None = None + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" + + self.compressor_state = process_dict(data, "binary_sensors", "compressor_state") + self.cooling_enabled = process_dict(data, "binary_sensors", "cooling_enabled") + self.cooling_state = process_dict(data, "binary_sensors", "cooling_state") + self.dhw_state = process_dict(data, "binary_sensors", "dhw_state") + self.flame_state = process_dict(data, "binary_sensors", "flame_state") + self.heating_state = process_dict(data, "binary_sensors", "heating_state") + self.secondary_boiler_state = process_dict( + data, "binary_sensors", "secondary_boiler_state" + ) + -@dataclass +@dataclass(kw_only=True) class ClimateDeviceSensors: """Climate-device sensors class.""" @@ -449,55 +501,104 @@ class ClimateDeviceSensors: water_temperature: float | None = None water_pressure: float | None = None + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" + + self.dhw_temperature = process_dict(data, "sensors", "dhw_temperature") + self.domestic_hot_water_setpoint = process_dict( + data, "sensors", "domestic_hot_water_setpoint" + ) + self.intended_boiler_temperature = process_dict( + data, "sensors", "intended_boiler_temperature" + ) + self.modulation_level = process_dict(data, "sensors", "modulation_level") + self.outdoor_air_temperature = process_dict( + data, "sensors", "outdoor_air_temperature" + ) + self.return_temperature = process_dict(data, "sensors", "return_temperature") + self.water_temperature = process_dict(data, "sensors", "water_temperature") + self.water_pressure = process_dict(data, "sensors", "water_pressure") + -@dataclass +@dataclass(kw_only=True) class ClimateDeviceSwitches: """Climate-device switches class.""" - dhw_cm_switch: bool | None = None cooling_ena_switch: bool | None = None + dhw_cm_switch: bool | None = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" + + self.cooling_ena_switch = process_dict(data, "switches", "cooling_ena_switch") + self.dhw_cm_switch = process_dict(data, "switches", "dhw_cm_switch") -@dataclass +@dataclass(kw_only=True) class Plug(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" - super().__init__() - zigbee_mac_address: str = "" - available: bool = False + sensors: PlugSensors + switches: PlugSwitches + available: bool | None = None hardware: str | None = None model_id: str | None = None - sensors: PlugSensors | None = None - switches: PlugSwitches | None = None + zigbee_mac_address: str | None = None + + def __init__(self) -> None: + """Init Plug class and inherited functions.""" + super().__init__() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" super().update_from_dict(data) + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) self.zigbee_mac_address = process_key(data, "zigbee_mac_address") self.available = process_key(data, "available") self.hardware = process_key(data, "hardware") self.model_id = process_key(data, "model_id") - self.sensors.update_from_dict(data) - self.switches.update_from_dict(data) -@dataclass +@dataclass(kw_only=True) class PlugSensors: """Plug sensors class.""" - electricity_consumed_interval: float = 0.0 electricity_consumed: float | None = None # Not present for Aqara Plug + electricity_consumed_interval: float | None = None electricity_produced: float | None = None electricity_produced_interval: float | None = None + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" -@dataclass + self.electricity_consumed = process_dict( + data, "sensors", "electricity_consumed" + ) + self.electricity_consumed_interval = process_dict( + data, "sensors", "electricity_consumed_interval" + ) + self.electricity_produced = process_dict( + data, "sensors", "electricity_produced" + ) + self.electricity_produced_interval = process_dict( + data, "sensors", "electricity_produced_interval" + ) + + +@dataclass(kw_only=True) class PlugSwitches: """Plug switches class.""" - relay: bool lock: bool | None = None + relay: bool | None = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this ZoneSensors object with data from a dictionary.""" + + self.lock = process_dict(data, "switches", "lock") + self.relay = process_dict(data, "switches", "relay") ################################################## @@ -541,15 +642,15 @@ class PlugwiseData: gateway: Gateway = Gateway() climate_device: ClimateDevice = ClimateDevice() - zones: list[Zone] = [] - thermostats: list[Thermostat] = [] - plugs: list[Plug] = [] + zones: list[Zone] | None = None + thermostats: list[Thermostat] | None = None + plugs: list[Plug] | None = None p1_dsmr: SmartEnergyMeter = SmartEnergyMeter() - def update_from_dict(self, data: dict[str, Any]) -> PlugwiseData: + def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API.""" - for device_id, device in data: + for device_id, device in data.items(): if device["device_class"] == "gateway": self.gateway.update_from_dict(device) if device["device_class"] == "heater_central": From e7618fa4eeee2727f7d8c809c30f7810ae48ead6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 19:06:18 +0100 Subject: [PATCH 42/49] Try 4 --- plugwise/devices.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index c79a5b3fe..37308a1ad 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -641,11 +641,35 @@ class PlugwiseData: """ gateway: Gateway = Gateway() - climate_device: ClimateDevice = ClimateDevice() + climate_device: ClimateDevice | None = None zones: list[Zone] | None = None thermostats: list[Thermostat] | None = None plugs: list[Plug] | None = None - p1_dsmr: SmartEnergyMeter = SmartEnergyMeter() + p1_dsmr: SmartEnergyMeter | None = None + + def __init__(self, smile) -> None: + """Initialize PlugwiseData class.""" + self.climate_device = None + self.zones = None + self.thermostats = None + self.plugs = None + self.p1_dsmr = None + self.smile = smile + + if self.smile.type == "Adam": + self.climate_device = ClimateDevice() + self.zones = list[Zone()] + self.thermostats = list[Thermostat()] + self.plugs = list[Plug()] + if self.smile.type == "Smile Anna": + self.climate_device = ClimateDevice() + if self.smile.type == "Smile Anna P1": + self.climate_device = ClimateDevice() + self.p1_dsmr = SmartEnergyMeter() + if self.smile.type == "Smile P1": + self.p1_dsmr = SmartEnergyMeter() + if self.smile.type == "Stretch": + self.plugs = list[Plug()] def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API.""" From 0c950e48cf3b8b3941413ce3bfa801526d12c921 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 22 Nov 2025 19:14:04 +0100 Subject: [PATCH 43/49] Try 5 --- plugwise/devices.py | 76 +++++++++++++++++++++++++++++----------- plugwise/legacy/smile.py | 3 ++ plugwise/smile.py | 3 ++ 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 37308a1ad..36a20eb7a 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from munch import Munch from typing import Any from .constants import ZONE_THERMOSTATS @@ -70,6 +71,8 @@ class Gateway(DeviceBase): def __init__(self) -> None: """Init Gateway class and inherited functions.""" super().__init__() + self.binary_sensors = GatewayBinarySensors() + self.sensors = Weather() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Gateway object with data from a dictionary.""" @@ -263,6 +266,7 @@ class Zone(DeviceBase): def __init__(self) -> None: """Init Zone class and inherited functions.""" super().__init__() + self.sensors = ZoneSensors() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this climate Zone object with data from a dictionary.""" @@ -324,6 +328,8 @@ class Thermostat(DeviceBase): def __init__(self) -> None: """Init Thermostat class and inherited functions.""" super().__init__() + self.binary_sensors = WirelessThermostatBinarySensors() + self.sensors = ThermostatSensors() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Thermostat object with data from a dictionary.""" @@ -446,6 +452,9 @@ class ClimateDevice(DeviceBase): def __init__(self) -> None: """Init ClimateDevice class and inherited functions.""" super().__init__() + self.binary_sensors = ClimateDeviceBinarySensors() + self.sensors = ClimateDeviceSensors() + self.switches = ClimateDeviceSwitches() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDevice object with data from a dictionary.""" @@ -548,6 +557,8 @@ class Plug(DeviceBase): def __init__(self) -> None: """Init Plug class and inherited functions.""" super().__init__() + self.sensors = PlugSensors() + self. switches = PlugSwitches() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" @@ -647,49 +658,74 @@ class PlugwiseData: plugs: list[Plug] | None = None p1_dsmr: SmartEnergyMeter | None = None - def __init__(self, smile) -> None: + def __init__(self, data: Any) -> None: """Initialize PlugwiseData class.""" self.climate_device = None - self.zones = None - self.thermostats = None - self.plugs = None + self.gateway = None self.p1_dsmr = None - self.smile = smile + self.plugs = None + self.thermostats = None + self.zones = None + + for device_id, device in data.items(): + self.gateway = Gateway() + if device["dev_class"] == "gateway": + self.gateway.update_from_dict(device) - if self.smile.type == "Adam": self.climate_device = ClimateDevice() + if device["dev_class"] == "heater_central": + self.climate_device.update_from_dict(device) + self.zones = list[Zone()] + if device["dev_class"] == "climate": + for zone in self.zones: + zone.update_from_dict(device) + self.zones.append(zone) + self.thermostats = list[Thermostat()] + if device["dev_class"] in ZONE_THERMOSTATS: + for thermostat in self.thermostats: + thermostat.update_from_dict(device) + self.thermostats.append(thermostat) + self.plugs = list[Plug()] - if self.smile.type == "Smile Anna": - self.climate_device = ClimateDevice() - if self.smile.type == "Smile Anna P1": - self.climate_device = ClimateDevice() - self.p1_dsmr = SmartEnergyMeter() - if self.smile.type == "Smile P1": + if device["dev_class"].endswith("_plug"): + for plug in self.plugs: + plug.update_from_dict(device) + self.plugs.append(plug) + self.p1_dsmr = SmartEnergyMeter() - if self.smile.type == "Stretch": - self.plugs = list[Plug()] + if device["dev_class"] == "smartmeter": + self.p1_dsmr.update_from_dict(device) def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API.""" for device_id, device in data.items(): - if device["device_class"] == "gateway": + if device["dev_class"] == "gateway": + self.gateway = Gateway() self.gateway.update_from_dict(device) - if device["device_class"] == "heater_central": + if device["dev_class"] == "heater_central": + self.climate_device = ClimateDevice() self.climate_device.update_from_dict(device) - if device["device_class"] == "climate": + if device["dev_class"] == "climate": + self.zones = list[Zone()] for zone in self.zones: if zone.location == device_id: zone.update_from_dict(device) - if device["device_class"] in ZONE_THERMOSTATS: + self.zones.append(zone) + if device["dev_class"] in ZONE_THERMOSTATS: + self.thermostats = list[Thermostat()] for thermostat in self.thermostats: if thermostat.location == device["location"]: thermostat.update_from_dict(device) - if device["device_class"].endswith("_plug"): + self.thermostats.append(thermostat) + if device["dev_class"].endswith("_plug"): + self.plugs = list[Plug()] for plug in self.plugs: if plug.location == device["location"]: plug.update_from_dict(device) - if device["device_class"] == "smartmeter": + self.plugs.append(plug) + if device["dev_class"] == "smartmeter": + self.p1_dsmr = SmartEnergyMeter() self.p1_dsmr.update_from_dict(device) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 54a21c15d..8918c323c 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -23,6 +23,7 @@ GwEntityData, ThermoLoc, ) +from plugwise.devices import PlugwiseData from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError from plugwise.legacy.data import SmileLegacyData @@ -52,6 +53,7 @@ def __init__( self._loc_data = _loc_data self._on_off_device = _on_off_device self._opentherm_device = _opentherm_device + self._plugwise_data: PlugwiseData | None = None self._request = _request self._stretch_v2 = _stretch_v2 self._target_smile = _target_smile @@ -123,6 +125,7 @@ async def async_update(self) -> dict[str, GwEntityData]: self._first_update = False self._previous_day_number = day_number + self._plugwise_data = PlugwiseData(self.gw_entities) return self.gw_entities ######################################################################################################## diff --git a/plugwise/smile.py b/plugwise/smile.py index 22010af5c..439c7ba0a 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -30,6 +30,7 @@ ThermoLoc, ) from plugwise.data import SmileData +from plugwise.devices import PlugwiseData from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError from defusedxml import ElementTree as etree @@ -84,6 +85,7 @@ def __init__( self._loc_data = _loc_data self._on_off_device = _on_off_device self._opentherm_device = _opentherm_device + self._plugwise_data: PlugwiseData | None = None self._request = _request self._schedule_old_states = _schedule_old_states self.smile = smile @@ -146,6 +148,7 @@ async def async_update(self) -> dict[str, GwEntityData]: except KeyError as err: raise DataMissingError("No Plugwise actual data received") from err + self._plugwise_data = PlugwiseData(self.gw_entities) return self.gw_entities ######################################################################################################## From 13216afd676dabb4ddbfc07ce028f4a768515284 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 23 Nov 2025 11:03:03 +0100 Subject: [PATCH 44/49] Corrections after review --- plugwise/devices.py | 125 ++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 68 deletions(-) diff --git a/plugwise/devices.py b/plugwise/devices.py index 36a20eb7a..a5a0fbe08 100644 --- a/plugwise/devices.py +++ b/plugwise/devices.py @@ -3,10 +3,9 @@ from __future__ import annotations from dataclasses import dataclass -from munch import Munch from typing import Any -from .constants import ZONE_THERMOSTATS +from plugwise.constants import ZONE_THERMOSTATS def process_key(data: dict[str, Any], key: str) -> Any | None: @@ -79,13 +78,13 @@ def update_from_dict(self, data: dict[str, Any]) -> None: super().update_from_dict(data) self.binary_sensors.update_from_dict(data) - self.gateway_modes = process_key(data, "gateway_mode") - self.hardware = process_key(data, "gateway_mode") - self.model_id = process_key(data, "gateway_mode") - self.regulation_modes = process_key(data, "gateway_mode") - self.select_gateway_mode = process_key(data, "gateway_mode") + self.gateway_modes = process_key(data, "gateway_modes") + self.hardware = process_key(data, "hardware") + self.model_id = process_key(data, "model_id") + self.regulation_modes = process_key(data, "regulation_modes") + self.select_gateway_mode = process_key(data, "select_gateway_mode") self.sensors.update_from_dict(data) - self.zigbee_mac_address = process_key(data, "gateway_mode") + self.zigbee_mac_address = process_key(data, "zigbee_mac_address") @dataclass(kw_only=True) @@ -109,7 +108,7 @@ class Weather: outdoor_temperature: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this GatewayBinarySensors object with data from a dictionary.""" + """Update this Weather object with data from a dictionary.""" self.outdoor_temperature = process_dict(data, "sensors", "outdoor_temperature") @@ -118,12 +117,14 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" + available: bool | None sensors: SmartEnergySensors - available: bool | None = None def __init__(self) -> None: """Init SmartEnergyMeter class and inherited functions.""" super().__init__() + self.available = None + self.sensors = SmartEnergySensors() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergyMeter object with data from a dictionary.""" @@ -365,12 +366,12 @@ class ThermostatSensors: valve_position: float | None = None # Tom/Floor def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this ThermostatSensors object with data from a dictionary.""" self.battery = process_dict(data, "sensors", "battery") self.humidity = process_dict(data, "sensors", "humidity") self.illuminance = process_dict(data, "sensors", "illuminance") - self.setpoint = process_dict(data, "sensors", "battsetpointery") + self.setpoint = process_dict(data, "sensors", "setpoint") self.setpoint_high = process_dict(data, "sensors", "setpoint_high") self.setpoint_low = process_dict(data, "sensors", "setpoint_low") self.temperature = process_dict(data, "sensors", "temperature") @@ -387,11 +388,9 @@ class WirelessThermostatBinarySensors: low_battery: bool | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this WirelessThermostatBinarySensors object with data from a dictionary.""" - self.electricity_consumed = process_dict( - data, "sensors", "electricity_consumed" - ) + self.low_battery = process_dict(data, "binary_sensors", "low_battery") @dataclass(kw_only=True) @@ -407,11 +406,12 @@ class SetpointDict: upper_bound: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this SetpointDict object with data from a dictionary.""" - self.electricity_consumed = process_dict( - data, "sensors", "electricity_consumed" - ) + self.lower_bound = process_key(data, "lower_bound") + self.resolution = process_key(data, "resolution") + self.setpoint = process_key(data, "setpoint") + self.upper_bound = process_key(data, "upper_bound") @dataclass(kw_only=True) @@ -484,7 +484,7 @@ class ClimateDeviceBinarySensors: secondary_boiler_state: bool | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this ClimateDeviceBinarySensors object with data from a dictionary.""" self.compressor_state = process_dict(data, "binary_sensors", "compressor_state") self.cooling_enabled = process_dict(data, "binary_sensors", "cooling_enabled") @@ -511,7 +511,7 @@ class ClimateDeviceSensors: water_pressure: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this ClimateDeviceSensors object with data from a dictionary.""" self.dhw_temperature = process_dict(data, "sensors", "dhw_temperature") self.domestic_hot_water_setpoint = process_dict( @@ -537,7 +537,7 @@ class ClimateDeviceSwitches: dhw_cm_switch: bool | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this ClimateDeviceSwitches object with data from a dictionary.""" self.cooling_ena_switch = process_dict(data, "switches", "cooling_ena_switch") self.dhw_cm_switch = process_dict(data, "switches", "dhw_cm_switch") @@ -558,7 +558,7 @@ def __init__(self) -> None: """Init Plug class and inherited functions.""" super().__init__() self.sensors = PlugSensors() - self. switches = PlugSwitches() + self.switches = PlugSwitches() def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" @@ -582,7 +582,7 @@ class PlugSensors: electricity_produced_interval: float | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this PlugSensors object with data from a dictionary.""" self.electricity_consumed = process_dict( data, "sensors", "electricity_consumed" @@ -606,7 +606,7 @@ class PlugSwitches: relay: bool | None = None def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this ZoneSensors object with data from a dictionary.""" + """Update this PlugSwitches object with data from a dictionary.""" self.lock = process_dict(data, "switches", "lock") self.relay = process_dict(data, "switches", "relay") @@ -652,80 +652,69 @@ class PlugwiseData: """ gateway: Gateway = Gateway() - climate_device: ClimateDevice | None = None - zones: list[Zone] | None = None - thermostats: list[Thermostat] | None = None - plugs: list[Plug] | None = None - p1_dsmr: SmartEnergyMeter | None = None + climate_device: ClimateDevice | None + zones: list[Zone] | None + thermostats: list[Thermostat] | None + plugs: list[Plug] | None + p1_dsmr: SmartEnergyMeter | None def __init__(self, data: Any) -> None: """Initialize PlugwiseData class.""" self.climate_device = None - self.gateway = None self.p1_dsmr = None self.plugs = None self.thermostats = None self.zones = None - for device_id, device in data.items(): - self.gateway = Gateway() + for _, device in data.items(): if device["dev_class"] == "gateway": self.gateway.update_from_dict(device) - - self.climate_device = ClimateDevice() if device["dev_class"] == "heater_central": + if self.climate_device is None: + self.climate_device = ClimateDevice() self.climate_device.update_from_dict(device) - - self.zones = list[Zone()] if device["dev_class"] == "climate": - for zone in self.zones: - zone.update_from_dict(device) - self.zones.append(zone) - - self.thermostats = list[Thermostat()] + if self.zones is None: + self.zones = [] + zone = Zone() + zone.update_from_dict(device) + self.zones.append(zone) if device["dev_class"] in ZONE_THERMOSTATS: - for thermostat in self.thermostats: - thermostat.update_from_dict(device) - self.thermostats.append(thermostat) - - self.plugs = list[Plug()] + if self.thermostats is None: + self.thermostats = [] + thermostat = Thermostat() + thermostat.update_from_dict(device) + self.thermostats.append(thermostat) if device["dev_class"].endswith("_plug"): - for plug in self.plugs: - plug.update_from_dict(device) - self.plugs.append(plug) - - self.p1_dsmr = SmartEnergyMeter() + if self.plugs is None: + self.plugs = [] + plug = Plug() + plug.update_from_dict(device) + self.plugs.append(plug) if device["dev_class"] == "smartmeter": - self.p1_dsmr.update_from_dict(device) + if self.p1_dsmr is None: + self.p1_dsmr = SmartEnergyMeter() + self.p1_dsmr.update_from_dict(device) def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API.""" for device_id, device in data.items(): if device["dev_class"] == "gateway": - self.gateway = Gateway() self.gateway.update_from_dict(device) - if device["dev_class"] == "heater_central": - self.climate_device = ClimateDevice() + if self.climate_device and device["dev_class"] == "heater_central": self.climate_device.update_from_dict(device) - if device["dev_class"] == "climate": - self.zones = list[Zone()] + if self.zones and device["dev_class"] == "climate": for zone in self.zones: if zone.location == device_id: zone.update_from_dict(device) - self.zones.append(zone) - if device["dev_class"] in ZONE_THERMOSTATS: - self.thermostats = list[Thermostat()] + if self.thermostats and device["dev_class"] in ZONE_THERMOSTATS: for thermostat in self.thermostats: if thermostat.location == device["location"]: thermostat.update_from_dict(device) - self.thermostats.append(thermostat) - if device["dev_class"].endswith("_plug"): - self.plugs = list[Plug()] + if self.plugs and device["dev_class"].endswith("_plug"): for plug in self.plugs: if plug.location == device["location"]: plug.update_from_dict(device) - self.plugs.append(plug) - if device["dev_class"] == "smartmeter": - self.p1_dsmr = SmartEnergyMeter() + if self.p1_dsmr and device["dev_class"] == "smartmeter": self.p1_dsmr.update_from_dict(device) From 9119f2fb7777c4dae6ebfd2b9f3d27dc39c5bdf3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 23 Nov 2025 11:42:22 +0100 Subject: [PATCH 45/49] Add models.py, delete devices.py --- plugwise/legacy/smile.py | 2 +- plugwise/{devices.py => models.py} | 2 +- plugwise/smile.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename plugwise/{devices.py => models.py} (99%) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 8918c323c..dc616751f 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -23,9 +23,9 @@ GwEntityData, ThermoLoc, ) -from plugwise.devices import PlugwiseData from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError from plugwise.legacy.data import SmileLegacyData +from plugwise.models import PlugwiseData from munch import Munch diff --git a/plugwise/devices.py b/plugwise/models.py similarity index 99% rename from plugwise/devices.py rename to plugwise/models.py index a5a0fbe08..574bcaf09 100644 --- a/plugwise/devices.py +++ b/plugwise/models.py @@ -1,4 +1,4 @@ -"""Plugwise Device classes.""" +"""Plugwise Device model classes.""" from __future__ import annotations diff --git a/plugwise/smile.py b/plugwise/smile.py index 439c7ba0a..618f5b55f 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -30,8 +30,8 @@ ThermoLoc, ) from plugwise.data import SmileData -from plugwise.devices import PlugwiseData from plugwise.exceptions import ConnectionFailedError, DataMissingError, PlugwiseError +from plugwise.models import PlugwiseData from defusedxml import ElementTree as etree From 9fb95835d33ccae8da04fe75aa50f6b35e584f33 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 23 Nov 2025 13:49:05 +0100 Subject: [PATCH 46/49] Improve --- plugwise/models.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/plugwise/models.py b/plugwise/models.py index 574bcaf09..3151fd91f 100644 --- a/plugwise/models.py +++ b/plugwise/models.py @@ -400,38 +400,30 @@ class SetpointDict: Used for temperature_offset, max_dhw_temperature,maximum_boiler_temperature. """ - lower_bound: float | None = None - resolution: float | None = None - setpoint: float | None = None - upper_bound: float | None = None - - def update_from_dict(self, data: dict[str, Any]) -> None: - """Update this SetpointDict object with data from a dictionary.""" - - self.lower_bound = process_key(data, "lower_bound") - self.resolution = process_key(data, "resolution") - self.setpoint = process_key(data, "setpoint") - self.upper_bound = process_key(data, "upper_bound") + lower_bound: float + resolution: float + setpoint: float + upper_bound: float @dataclass(kw_only=True) class ThermostatDict: """Thermostat dict class.""" - lower_bound: float | None = None - resolution: float | None = None - upper_bound: float | None = None - setpoint: float | None = None # heat or cool - setpoint_high: float | None = None # heat_cool - setpoint_low: float | None = None # heat_cool + lower_bound: float + resolution: float + upper_bound: float + setpoint: float | None # heat or cool + setpoint_high: float | None # heat_cool + setpoint_low: float | None # heat_cool @dataclass(kw_only=True) class ThermostatsDict: """Thermostats dict class.""" - primary: list[str] | None = None - secondary: list[str] | None = None + primary: list[str] + secondary: list[str] @dataclass(kw_only=True) From ffdd537dca26dff1d0fa1e6a42ad6f17a6e0b0c5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 23 Nov 2025 13:55:14 +0100 Subject: [PATCH 47/49] Simplify --- plugwise/models.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/plugwise/models.py b/plugwise/models.py index 3151fd91f..b48d4cb7c 100644 --- a/plugwise/models.py +++ b/plugwise/models.py @@ -33,22 +33,28 @@ class DeviceBase: Every device will have most of these data points. """ + available: bool | None = None dev_class: str | None = None firmware: str | None = None + hardware: str | None = None location: str | None = None mac_address: str | None = None model: str | None = None + model_id: str | None = None name: str | None = None vendor: str | None = None def update_from_dict(self, data: dict[str, Any]) -> None: """Update this DeviceBase object with data from a dictionary.""" + self.available = process_key(data, "available") self.dev_class = process_key(data, "dev_class") self.firmware = process_key(data, "firmware") + self.hardware = process_key(data, "hardware") self.location = process_key(data, "location") self.mac_address = process_key(data, "mac_address") self.model = process_key(data, "model") + self.model_id = process_key(data, "model_id") self.name = process_key(data, "name") self.vendor = process_key(data, "vendor") @@ -59,8 +65,6 @@ class Gateway(DeviceBase): binary_sensors: GatewayBinarySensors gateway_modes: list[str] | None = None - hardware: str | None = None - model_id: str | None = None regulation_modes: list[str] | None = None select_gateway_mode: str | None = None select_regulation_mode: str | None = None @@ -79,8 +83,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: super().update_from_dict(data) self.binary_sensors.update_from_dict(data) self.gateway_modes = process_key(data, "gateway_modes") - self.hardware = process_key(data, "hardware") - self.model_id = process_key(data, "model_id") self.regulation_modes = process_key(data, "regulation_modes") self.select_gateway_mode = process_key(data, "select_gateway_mode") self.sensors.update_from_dict(data) @@ -117,13 +119,11 @@ def update_from_dict(self, data: dict[str, Any]) -> None: class SmartEnergyMeter(DeviceBase): """DSMR Energy Meter data class.""" - available: bool | None sensors: SmartEnergySensors def __init__(self) -> None: """Init SmartEnergyMeter class and inherited functions.""" super().__init__() - self.available = None self.sensors = SmartEnergySensors() def update_from_dict(self, data: dict[str, Any]) -> None: @@ -131,7 +131,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: super().update_from_dict(data) self.sensors.update_from_dict(data) - self.available = process_key(data, "available") @dataclass(kw_only=True) @@ -258,8 +257,6 @@ class Zone(DeviceBase): select_zone_profile: str | None = None zone_profiles: list[str] | None = None active_preset: str | None = None - hardware: str | None = None - model_id: str | None = None select_schedule: str | None = None thermostat: ThermostatDict | None = None thermostats: ThermostatsDict | None = None @@ -281,8 +278,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.select_zone_profile = process_key(data, "select_zone_profile") self.zone_profiles = process_key(data, "zone_profiles") self.active_preset = process_key(data, "active_preset") - self.hardware = process_key(data, "hardware") - self.model_id = process_key(data, "model_id") self.select_schedule = process_key(data, "select_schedule") self.thermostat = process_key(data, "thermostat") self.thermostats = process_key(data, "thermostats") @@ -319,8 +314,6 @@ class Thermostat(DeviceBase): preset_modes: list[str] | None = None active_preset: str | None = None climate_mode: str | None = None - hardware: str | None = None - model_id: str | None = None select_schedule: str | None = None temperature_offset: SetpointDict | None = None # not for legacy thermostat: ThermostatDict | None = None @@ -343,8 +336,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.preset_modes = process_key(data, "preset_modes") self.active_preset = process_key(data, "active_preset") self.climate_mode = process_key(data, "climate_mode") - self.hardware = process_key(data, "hardware") - self.model_id = process_key(data, "model_id") self.select_schedule = process_key(data, "select_schedule") self.temperature_offset = process_key(data, "temperature_offset") self.thermostat = process_key(data, "thermostat") @@ -436,7 +427,6 @@ class ClimateDevice(DeviceBase): binary_sensors: ClimateDeviceBinarySensors sensors: ClimateDeviceSensors switches: ClimateDeviceSwitches - available: bool | None = None maximum_boiler_temperature: SetpointDict | None = None max_dhw_temperature: SetpointDict | None = None model_id: str | None = None @@ -455,7 +445,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.binary_sensors.update_from_dict(data) self.sensors.update_from_dict(data) self.switches.update_from_dict(data) - self.available = process_key(data, "available") self.maximum_boiler_temperature = process_key( data, "maximum_boiler_temperature" ) @@ -541,9 +530,6 @@ class Plug(DeviceBase): sensors: PlugSensors switches: PlugSwitches - available: bool | None = None - hardware: str | None = None - model_id: str | None = None zigbee_mac_address: str | None = None def __init__(self) -> None: @@ -559,9 +545,6 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.sensors.update_from_dict(data) self.switches.update_from_dict(data) self.zigbee_mac_address = process_key(data, "zigbee_mac_address") - self.available = process_key(data, "available") - self.hardware = process_key(data, "hardware") - self.model_id = process_key(data, "model_id") @dataclass(kw_only=True) From 760d92a17ee3716ab0669eb113644ef985b51be9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 23 Nov 2025 14:00:59 +0100 Subject: [PATCH 48/49] Formatting --- plugwise/models.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/plugwise/models.py b/plugwise/models.py index b48d4cb7c..ff14d51df 100644 --- a/plugwise/models.py +++ b/plugwise/models.py @@ -10,7 +10,6 @@ def process_key(data: dict[str, Any], key: str) -> Any | None: """Return the key value from the data dict, when present.""" - if key in data: return data[key] @@ -19,7 +18,6 @@ def process_key(data: dict[str, Any], key: str) -> Any | None: def process_dict(data: dict[str, Any], dict_type: str, key: str) -> Any | None: """Return the key value from the data dict, when present.""" - if dict_type in data and key in data[dict_type]: return data[dict_type][key] @@ -46,7 +44,6 @@ class DeviceBase: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this DeviceBase object with data from a dictionary.""" - self.available = process_key(data, "available") self.dev_class = process_key(data, "dev_class") self.firmware = process_key(data, "firmware") @@ -79,7 +76,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Gateway object with data from a dictionary.""" - super().update_from_dict(data) self.binary_sensors.update_from_dict(data) self.gateway_modes = process_key(data, "gateway_modes") @@ -97,7 +93,6 @@ class GatewayBinarySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this GatewayBinarySensors object with data from a dictionary.""" - self.plugwise_notification = process_dict( data, "binary_sensors", "plugwise_notification" ) @@ -111,7 +106,6 @@ class Weather: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Weather object with data from a dictionary.""" - self.outdoor_temperature = process_dict(data, "sensors", "outdoor_temperature") @@ -128,7 +122,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergyMeter object with data from a dictionary.""" - super().update_from_dict(data) self.sensors.update_from_dict(data) @@ -167,7 +160,6 @@ class SmartEnergySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this SmartEnergySensors object with data from a dictionary.""" - self.electricity_consumed_off_peak_cumulative = process_dict( data, "sensors", "electricity_consumed_off_peak_cumulative" ) @@ -268,7 +260,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this climate Zone object with data from a dictionary.""" - super().update_from_dict(data) self.sensors.update_from_dict(data) self.available_schedules = process_key(data, "available_schedules") @@ -293,7 +284,6 @@ class ZoneSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ZoneSensors object with data from a dictionary.""" - self.electricity_consumed = process_dict( data, "sensors", "electricity_consumed" ) @@ -327,7 +317,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Thermostat object with data from a dictionary.""" - super().update_from_dict(data) self.binary_sensors.update_from_dict(data) self.sensors.update_from_dict(data) @@ -358,7 +347,6 @@ class ThermostatSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ThermostatSensors object with data from a dictionary.""" - self.battery = process_dict(data, "sensors", "battery") self.humidity = process_dict(data, "sensors", "humidity") self.illuminance = process_dict(data, "sensors", "illuminance") @@ -380,7 +368,6 @@ class WirelessThermostatBinarySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this WirelessThermostatBinarySensors object with data from a dictionary.""" - self.low_battery = process_dict(data, "binary_sensors", "low_battery") @@ -440,7 +427,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDevice object with data from a dictionary.""" - super().update_from_dict(data) self.binary_sensors.update_from_dict(data) self.sensors.update_from_dict(data) @@ -466,7 +452,6 @@ class ClimateDeviceBinarySensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDeviceBinarySensors object with data from a dictionary.""" - self.compressor_state = process_dict(data, "binary_sensors", "compressor_state") self.cooling_enabled = process_dict(data, "binary_sensors", "cooling_enabled") self.cooling_state = process_dict(data, "binary_sensors", "cooling_state") @@ -493,7 +478,6 @@ class ClimateDeviceSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDeviceSensors object with data from a dictionary.""" - self.dhw_temperature = process_dict(data, "sensors", "dhw_temperature") self.domestic_hot_water_setpoint = process_dict( data, "sensors", "domestic_hot_water_setpoint" @@ -519,7 +503,6 @@ class ClimateDeviceSwitches: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this ClimateDeviceSwitches object with data from a dictionary.""" - self.cooling_ena_switch = process_dict(data, "switches", "cooling_ena_switch") self.dhw_cm_switch = process_dict(data, "switches", "dhw_cm_switch") @@ -540,7 +523,6 @@ def __init__(self) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this Plug object with data from a dictionary.""" - super().update_from_dict(data) self.sensors.update_from_dict(data) self.switches.update_from_dict(data) @@ -558,7 +540,6 @@ class PlugSensors: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this PlugSensors object with data from a dictionary.""" - self.electricity_consumed = process_dict( data, "sensors", "electricity_consumed" ) @@ -582,7 +563,6 @@ class PlugSwitches: def update_from_dict(self, data: dict[str, Any]) -> None: """Update this PlugSwitches object with data from a dictionary.""" - self.lock = process_dict(data, "switches", "lock") self.relay = process_dict(data, "switches", "relay") @@ -673,7 +653,6 @@ def __init__(self, data: Any) -> None: def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API.""" - for device_id, device in data.items(): if device["dev_class"] == "gateway": self.gateway.update_from_dict(device) From ca5a123c7076d12212c1dfaeb163014760d29fe1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 24 Nov 2025 19:20:54 +0100 Subject: [PATCH 49/49] Add support for Groups --- plugwise/models.py | 94 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 15 deletions(-) diff --git a/plugwise/models.py b/plugwise/models.py index ff14d51df..23a8e787b 100644 --- a/plugwise/models.py +++ b/plugwise/models.py @@ -507,6 +507,60 @@ def update_from_dict(self, data: dict[str, Any]) -> None: self.dhw_cm_switch = process_dict(data, "switches", "dhw_cm_switch") +@dataclass(kw_only=True) +class Group(DeviceBase): + """Group class covering switch-groups, pump-groups, for Adam and Stretch.""" + + members: list[str] | None = None + sensors: GroupSensors + switches: GroupSwitch + + def __init__(self) -> None: + """Init Group class and inherited functions.""" + super().__init__() + self.sensors = GroupSensors() + self.switches = GroupSwitch() + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this Group object with data from a dictionary.""" + super().update_from_dict(data) + self.members = process_key(data, "members") + self.sensors.update_from_dict(data) + self.switches.update_from_dict(data) + + +@dataclass(kw_only=True) +class GroupSensors: + """Group sensors class.""" + + electricity_consumed: float | None = None + electricity_produced: float | None = None + temperature: float | None = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this GroupSensors object with data from a dictionary.""" + self.electricity_consumed = process_dict( + data, "sensors", "electricity_consumed" + ) + self.electricity_produced = process_dict( + data, "sensors", "electricity_produced" + ) + self.temperature = process_dict(data, "sensors", "temperature") + + +@dataclass(kw_only=True) +class GroupSwitch: + """Group switch class.""" + + lock: bool | None = None + relay: bool | None = None + + def update_from_dict(self, data: dict[str, Any]) -> None: + """Update this GroupSwitch object with data from a dictionary.""" + self.lock = process_dict(data, "switches", "lock") + self.relay = process_dict(data, "switches", "relay") + + @dataclass(kw_only=True) class Plug(DeviceBase): """Plug data class covering Plugwise Adam/Stretch and Aqara Plugs, and generic ZigBee type Switches.""" @@ -574,6 +628,7 @@ class PlugwiseData: - Gateway Adam - Climate device: OnOff or Opentherm - Zones (1 to many) with thermostatic and energy sensors summary, with thermostat setpoint- and mode-, preset- & schedule-setter + - Groups - switching type - Single devices (appliances) assigned to a Zone, or not - Anna (wired thermostat) - Emma Pro wired (wired thermostat) @@ -603,43 +658,40 @@ class PlugwiseData: - Gateway Stretch (legacy) - Single devices (Zigbee) + - Groups: switching and reporting types - ?? """ gateway: Gateway = Gateway() climate_device: ClimateDevice | None - zones: list[Zone] | None - thermostats: list[Thermostat] | None + groups: list[Group] | None plugs: list[Plug] | None p1_dsmr: SmartEnergyMeter | None + thermostats: list[Thermostat] | None + zones: list[Zone] | None def __init__(self, data: Any) -> None: """Initialize PlugwiseData class.""" self.climate_device = None - self.p1_dsmr = None + self.groups = None self.plugs = None + self.p1_dsmr = None self.thermostats = None self.zones = None for _, device in data.items(): if device["dev_class"] == "gateway": self.gateway.update_from_dict(device) + if device["dev_class"] in ("pumping", "report", "switching"): + if self.groups is None: + self.groups = [] + group = Group() + group.update_from_dict(device) + self.groups.append(group) if device["dev_class"] == "heater_central": if self.climate_device is None: self.climate_device = ClimateDevice() self.climate_device.update_from_dict(device) - if device["dev_class"] == "climate": - if self.zones is None: - self.zones = [] - zone = Zone() - zone.update_from_dict(device) - self.zones.append(zone) - if device["dev_class"] in ZONE_THERMOSTATS: - if self.thermostats is None: - self.thermostats = [] - thermostat = Thermostat() - thermostat.update_from_dict(device) - self.thermostats.append(thermostat) if device["dev_class"].endswith("_plug"): if self.plugs is None: self.plugs = [] @@ -650,6 +702,18 @@ def __init__(self, data: Any) -> None: if self.p1_dsmr is None: self.p1_dsmr = SmartEnergyMeter() self.p1_dsmr.update_from_dict(device) + if device["dev_class"] in ZONE_THERMOSTATS: + if self.thermostats is None: + self.thermostats = [] + thermostat = Thermostat() + thermostat.update_from_dict(device) + self.thermostats.append(thermostat) + if device["dev_class"] == "climate": + if self.zones is None: + self.zones = [] + zone = Zone() + zone.update_from_dict(device) + self.zones.append(zone) def update_from_dict(self, data: dict[str, Any]) -> None: """Update the status object with data received from the Plugwise API."""