Skip to content

Commit 72847fc

Browse files
committed
Merge all code into smile.py
1 parent cb71d44 commit 72847fc

File tree

4 files changed

+1411
-1498
lines changed

4 files changed

+1411
-1498
lines changed

plugwise/data.py

Lines changed: 0 additions & 343 deletions
Original file line numberDiff line numberDiff line change
@@ -1,346 +1,3 @@
1-
"""Use of this source code is governed by the MIT license found in the LICENSE file.
21

3-
Plugwise Smile protocol data-collection helpers.
4-
"""
52

6-
from __future__ import annotations
73

8-
import re
9-
10-
from plugwise.constants import (
11-
ADAM,
12-
ANNA,
13-
MAX_SETPOINT,
14-
MIN_SETPOINT,
15-
NONE,
16-
OFF,
17-
ActuatorData,
18-
GwEntityData,
19-
)
20-
from plugwise.helper import SmileHelper
21-
from plugwise.util import remove_empty_platform_dicts
22-
23-
24-
class SmileData(SmileHelper):
25-
"""The Plugwise Smile main class."""
26-
27-
def __init__(self) -> None:
28-
"""Init."""
29-
SmileHelper.__init__(self)
30-
31-
def _all_entity_data(self) -> None:
32-
"""Helper-function for get_all_gateway_entities().
33-
34-
Collect data for each entity and add to self.gw_data and self.gw_entities.
35-
"""
36-
self._update_gw_entities()
37-
if self.smile(ADAM):
38-
self._update_zones()
39-
self.gw_entities.update(self._zones)
40-
41-
self.gw_data.update(
42-
{
43-
"gateway_id": self.gateway_id,
44-
"item_count": self._count,
45-
"notifications": self._notifications,
46-
"reboot": True,
47-
"smile_name": self.smile_name,
48-
}
49-
)
50-
if self._is_thermostat:
51-
self.gw_data.update(
52-
{"heater_id": self._heater_id, "cooling_present": self._cooling_present}
53-
)
54-
55-
def _update_zones(self) -> None:
56-
"""Helper-function for _all_entity_data() and async_update().
57-
58-
Collect data for each zone/location and add to self._zones.
59-
"""
60-
for location_id, zone in self._zones.items():
61-
data = self._get_location_data(location_id)
62-
zone.update(data)
63-
64-
def _update_gw_entities(self) -> None:
65-
"""Helper-function for _all_entities_data() and async_update().
66-
67-
Collect data for each entity and add to self.gw_entities.
68-
"""
69-
mac_list: list[str] = []
70-
for entity_id, entity in self.gw_entities.items():
71-
data = self._get_entity_data(entity_id)
72-
if entity_id == self.gateway_id:
73-
mac_list = self._detect_low_batteries()
74-
self._add_or_update_notifications(entity_id, entity, data)
75-
76-
entity.update(data)
77-
is_battery_low = (
78-
mac_list
79-
and "low_battery" in entity["binary_sensors"]
80-
and entity["zigbee_mac_address"] in mac_list
81-
and entity["dev_class"]
82-
in (
83-
"thermo_sensor",
84-
"thermostatic_radiator_valve",
85-
"zone_thermometer",
86-
"zone_thermostat",
87-
)
88-
)
89-
if is_battery_low:
90-
entity["binary_sensors"]["low_battery"] = True
91-
92-
self._update_for_cooling(entity)
93-
94-
remove_empty_platform_dicts(entity)
95-
96-
def _detect_low_batteries(self) -> list[str]:
97-
"""Helper-function updating the low-battery binary_sensor status from a Battery-is-low message."""
98-
mac_address_list: list[str] = []
99-
mac_pattern = re.compile(r"(?:[0-9A-F]{2}){8}")
100-
matches = ["Battery", "below"]
101-
if self._notifications:
102-
for msg_id, notification in list(self._notifications.items()):
103-
mac_address: str | None = None
104-
message: str | None = notification.get("message")
105-
warning: str | None = notification.get("warning")
106-
notify = message or warning
107-
if (
108-
notify is not None
109-
and all(x in notify for x in matches)
110-
and (mac_addresses := mac_pattern.findall(notify))
111-
):
112-
mac_address = mac_addresses[0] # re.findall() outputs a list
113-
114-
if mac_address is not None:
115-
mac_address_list.append(mac_address)
116-
if message is not None: # only block message-type notifications
117-
self._notifications.pop(msg_id)
118-
119-
return mac_address_list
120-
121-
def _add_or_update_notifications(
122-
self, entity_id: str, entity: GwEntityData, data: GwEntityData
123-
) -> None:
124-
"""Helper-function adding or updating the Plugwise notifications."""
125-
if (
126-
entity_id == self.gateway_id
127-
and (self._is_thermostat or self.smile_type == "power")
128-
) or (
129-
"binary_sensors" in entity
130-
and "plugwise_notification" in entity["binary_sensors"]
131-
):
132-
data["binary_sensors"]["plugwise_notification"] = bool(self._notifications)
133-
self._count += 1
134-
135-
def _update_for_cooling(self, entity: GwEntityData) -> None:
136-
"""Helper-function for adding/updating various cooling-related values."""
137-
# For Anna and heating + cooling, replace setpoint with setpoint_high/_low
138-
if (
139-
self.smile(ANNA)
140-
and self._cooling_present
141-
and entity["dev_class"] == "thermostat"
142-
):
143-
thermostat = entity["thermostat"]
144-
sensors = entity["sensors"]
145-
temp_dict: ActuatorData = {
146-
"setpoint_low": thermostat["setpoint"],
147-
"setpoint_high": MAX_SETPOINT,
148-
}
149-
if self._cooling_enabled:
150-
temp_dict = {
151-
"setpoint_low": MIN_SETPOINT,
152-
"setpoint_high": thermostat["setpoint"],
153-
}
154-
thermostat.pop("setpoint")
155-
temp_dict.update(thermostat)
156-
entity["thermostat"] = temp_dict
157-
if "setpoint" in sensors:
158-
sensors.pop("setpoint")
159-
sensors["setpoint_low"] = temp_dict["setpoint_low"]
160-
sensors["setpoint_high"] = temp_dict["setpoint_high"]
161-
self._count += 2 # add 4, remove 2
162-
163-
def _get_location_data(self, loc_id: str) -> GwEntityData:
164-
"""Helper-function for _all_entity_data() and async_update().
165-
166-
Provide entity-data, based on Location ID (= loc_id).
167-
"""
168-
zone = self._zones[loc_id]
169-
data = self._get_zone_data(loc_id)
170-
data["control_state"] = "idle"
171-
self._count += 1
172-
if (ctrl_state := self._control_state(data, loc_id)) and str(ctrl_state) in (
173-
"cooling",
174-
"heating",
175-
"preheating",
176-
):
177-
data["control_state"] = str(ctrl_state)
178-
179-
data["sensors"].pop("setpoint") # remove, only used in _control_state()
180-
self._count -= 1
181-
182-
# Thermostat data (presets, temperatures etc)
183-
self._climate_data(loc_id, zone, data)
184-
185-
return data
186-
187-
def _get_entity_data(self, entity_id: str) -> GwEntityData:
188-
"""Helper-function for _update_gw_entities() and async_update().
189-
190-
Provide entity-data, based on appliance_id (= entity_id).
191-
"""
192-
entity = self.gw_entities[entity_id]
193-
data = self._get_measurement_data(entity_id)
194-
195-
# Check availability of wired-connected entities
196-
# Smartmeter
197-
self._check_availability(
198-
entity, "smartmeter", data, "P1 does not seem to be connected"
199-
)
200-
# OpenTherm entity
201-
if entity["name"] != "OnOff":
202-
self._check_availability(
203-
entity, "heater_central", data, "no OpenTherm communication"
204-
)
205-
206-
# Switching groups data
207-
self._entity_switching_group(entity, data)
208-
# Adam data
209-
if self.smile(ADAM):
210-
self._get_adam_data(entity, data)
211-
212-
# Thermostat data for Anna (presets, temperatures etc)
213-
if self.smile(ANNA) and entity["dev_class"] == "thermostat":
214-
self._climate_data(entity_id, entity, data)
215-
self._get_anna_control_state(data)
216-
217-
return data
218-
219-
def _check_availability(
220-
self, entity: GwEntityData, dev_class: str, data: GwEntityData, message: str
221-
) -> None:
222-
"""Helper-function for _get_entity_data().
223-
224-
Provide availability status for the wired-connected devices.
225-
"""
226-
if entity["dev_class"] == dev_class:
227-
data["available"] = True
228-
self._count += 1
229-
for item in self._notifications.values():
230-
for msg in item.values():
231-
if message in msg:
232-
data["available"] = False
233-
234-
def _get_adam_data(self, entity: GwEntityData, data: GwEntityData) -> None:
235-
"""Helper-function for _get_entity_data().
236-
237-
Determine Adam heating-status for on-off heating via valves,
238-
available regulations_modes and thermostat control_states,
239-
and add missing cooling_enabled when required.
240-
"""
241-
if entity["dev_class"] == "heater_central":
242-
# Indicate heating_state based on valves being open in case of city-provided heating
243-
if self._on_off_device and isinstance(self._heating_valves(), int):
244-
data["binary_sensors"]["heating_state"] = self._heating_valves() != 0
245-
# Add cooling_enabled binary_sensor
246-
if "binary_sensors" in data:
247-
if (
248-
"cooling_enabled" not in data["binary_sensors"]
249-
and self._cooling_present
250-
):
251-
data["binary_sensors"]["cooling_enabled"] = self._cooling_enabled
252-
253-
# Show the allowed regulation_modes and gateway_modes
254-
if entity["dev_class"] == "gateway":
255-
if self._reg_allowed_modes:
256-
data["regulation_modes"] = self._reg_allowed_modes
257-
self._count += 1
258-
if self._gw_allowed_modes:
259-
data["gateway_modes"] = self._gw_allowed_modes
260-
self._count += 1
261-
262-
def _climate_data(
263-
self, location_id: str, entity: GwEntityData, data: GwEntityData
264-
) -> None:
265-
"""Helper-function for _get_entity_data().
266-
267-
Determine climate-control entity data.
268-
"""
269-
loc_id = location_id
270-
if entity.get("location") is not None:
271-
loc_id = entity["location"]
272-
273-
# Presets
274-
data["preset_modes"] = None
275-
data["active_preset"] = None
276-
self._count += 2
277-
if presets := self._presets(loc_id):
278-
data["preset_modes"] = list(presets)
279-
data["active_preset"] = self._preset(loc_id)
280-
281-
# Schedule
282-
avail_schedules, sel_schedule = self._schedules(loc_id)
283-
if avail_schedules != [NONE]:
284-
data["available_schedules"] = avail_schedules
285-
data["select_schedule"] = sel_schedule
286-
self._count += 2
287-
288-
# Set HA climate HVACMode: auto, heat, heat_cool, cool and off
289-
data["climate_mode"] = "auto"
290-
self._count += 1
291-
if sel_schedule in (NONE, OFF):
292-
data["climate_mode"] = "heat"
293-
if self._cooling_present:
294-
data["climate_mode"] = (
295-
"cool" if self.check_reg_mode("cooling") else "heat_cool"
296-
)
297-
298-
if self.check_reg_mode("off"):
299-
data["climate_mode"] = "off"
300-
301-
if NONE not in avail_schedules:
302-
self._get_schedule_states_with_off(
303-
loc_id, avail_schedules, sel_schedule, data
304-
)
305-
306-
def check_reg_mode(self, mode: str) -> bool:
307-
"""Helper-function for device_data_climate()."""
308-
gateway = self.gw_entities[self.gateway_id]
309-
return (
310-
"regulation_modes" in gateway and gateway["select_regulation_mode"] == mode
311-
)
312-
313-
def _get_anna_control_state(self, data: GwEntityData) -> None:
314-
"""Set the thermostat control_state based on the opentherm/onoff device state."""
315-
data["control_state"] = "idle"
316-
self._count += 1
317-
for entity in self.gw_entities.values():
318-
if entity["dev_class"] != "heater_central":
319-
continue
320-
321-
binary_sensors = entity["binary_sensors"]
322-
if binary_sensors["heating_state"]:
323-
data["control_state"] = "heating"
324-
if binary_sensors.get("cooling_state"):
325-
data["control_state"] = "cooling"
326-
327-
def _get_schedule_states_with_off(
328-
self, location: str, schedules: list[str], selected: str, data: GwEntityData
329-
) -> None:
330-
"""Collect schedules with states for each thermostat.
331-
332-
Also, replace NONE by OFF when none of the schedules are active.
333-
"""
334-
loc_schedule_states: dict[str, str] = {}
335-
for schedule in schedules:
336-
loc_schedule_states[schedule] = "off"
337-
if schedule == selected and data["climate_mode"] == "auto":
338-
loc_schedule_states[schedule] = "on"
339-
self._schedule_old_states[location] = loc_schedule_states
340-
341-
all_off = True
342-
for state in self._schedule_old_states[location].values():
343-
if state == "on":
344-
all_off = False
345-
if all_off:
346-
data["select_schedule"] = OFF

0 commit comments

Comments
 (0)