Skip to content

Commit 7690bf2

Browse files
authored
Merge pull request #228 from plugwise/Fix_improve
Code improvements
2 parents 838a777 + 64222eb commit 7690bf2

File tree

6 files changed

+120
-121
lines changed

6 files changed

+120
-121
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
# v0.25.2: Bugfix, code improvements
4+
- Fix a set_temperature() and heat_cool related bug
5+
36
# v0.25.1: Remove sensor-keys from output when cooling is present
47

58
# v0.25.0: Improve compatibility with HA Core climate platform

plugwise/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Plugwise module."""
22

3-
__version__ = "0.25.1"
3+
__version__ = "0.25.2"
44

55
from plugwise.smile import Smile
66
from plugwise.stick import Stick

plugwise/constants.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,20 @@
391391
NONE: Final = "None"
392392
FAKE_APPL: Final = "aaaa0000aaaa0000aaaa0000aaaa00aa"
393393
FAKE_LOC: Final = "0000aaaa0000aaaa0000aaaa0000aa00"
394+
LEGACY_SMILES: Final[tuple[str, ...]] = (
395+
"smile_thermo_v1",
396+
"smile_v2",
397+
"stretch_v2",
398+
"stretch_v3",
399+
)
394400
LIMITS: Final[tuple[str, ...]] = (
395401
"setpoint",
396402
"lower_bound",
397403
"upper_bound",
398404
"resolution",
399405
)
400-
MAX_SETPOINT: Final[float] = 40.0
401-
MIN_SETPOINT: Final[float] = 0.0
406+
MAX_SETPOINT: Final[float] = 30.0
407+
MIN_SETPOINT: Final[float] = 4.0
402408
SPECIAL_FORMAT: Final[tuple[str, ...]] = (ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS)
403409
SWITCH_GROUP_TYPES: Final[tuple[str, ...]] = ("switching", "report")
404410
ZONE_THERMOSTATS: Final[tuple[str, ...]] = (

plugwise/helper.py

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ def update_helper(
9191

9292
def check_model(name: str | None, vendor_name: str | None) -> str | None:
9393
"""Model checking before using version_to_model."""
94-
if vendor_name == "Plugwise":
95-
if (model := version_to_model(name)) != "Unknown":
96-
return model
94+
if vendor_name == "Plugwise" and ((model := version_to_model(name)) != "Unknown"):
95+
return model
96+
9797
return name
9898

9999

@@ -160,15 +160,13 @@ def power_data_local_format(
160160
attrs: dict[str, str], key_string: str, val: str
161161
) -> float | int | bool:
162162
"""Format power data."""
163-
attrs_uom = getattr(attrs, ATTR_UNIT_OF_MEASUREMENT)
164-
f_val = format_measure(val, attrs_uom)
165-
# Format only P1_MEASUREMENT POWER_WATT values, do not move to util-format_meaure function!
166-
if attrs_uom == POWER_WATT:
167-
f_val = int(round(float(val)))
163+
# Special formatting of P1_MEASUREMENT POWER_WATT values, do not move to util-format_measure() function!
168164
if all(item in key_string for item in ("electricity", "cumulative")):
169-
f_val = format_measure(val, ENERGY_KILO_WATT_HOUR)
165+
return format_measure(val, ENERGY_KILO_WATT_HOUR)
166+
if (attrs_uom := getattr(attrs, ATTR_UNIT_OF_MEASUREMENT)) == POWER_WATT:
167+
return int(round(float(val)))
170168

171-
return f_val
169+
return format_measure(val, attrs_uom)
172170

173171

174172
def power_data_energy_diff(
@@ -384,12 +382,12 @@ def _locations_specials(self, loc: Munch, location: str) -> Munch:
384382
Correct location info in special cases.
385383
"""
386384
if loc.name == "Home":
387-
self._home_location = loc.id
385+
self._home_location = loc.loc_id
388386

389387
# Replace location-name for P1 legacy, can contain privacy-related info
390388
if self._smile_legacy and self.smile_type == "power":
391389
loc.name = "Home"
392-
self._home_location = loc.id
390+
self._home_location = loc.loc_id
393391

394392
return loc
395393

@@ -405,7 +403,7 @@ def _all_locations(self) -> None:
405403

406404
for location in locations:
407405
loc.name = location.find("name").text
408-
loc.id = location.attrib["id"]
406+
loc.loc_id = location.attrib["id"]
409407
# Filter the valid single location for P1 legacy: services not empty
410408
locator = "./services"
411409
if (
@@ -418,7 +416,7 @@ def _all_locations(self) -> None:
418416
# Specials
419417
loc = self._locations_specials(loc, location)
420418

421-
self._loc_data[loc.id] = {"name": loc.name}
419+
self._loc_data[loc.loc_id] = {"name": loc.name}
422420

423421
return
424422

@@ -598,7 +596,7 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
598596

599597
return appl
600598

601-
def _p1_smartmeter_info_finder(self, appl: Munch) -> Munch:
599+
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
602600
"""Collect P1 DSMR Smartmeter info."""
603601
loc_id = next(iter(self._loc_data.keys()))
604602
appl.dev_id = self.gateway_id
@@ -628,44 +626,40 @@ def _p1_smartmeter_info_finder(self, appl: Munch) -> Munch:
628626
if value is not None or key == "location":
629627
self._appl_data[appl.dev_id].update({key: value}) # type: ignore[misc]
630628

631-
return appl
632-
633629
def _create_legacy_gateway(self) -> None:
634630
"""Create the (missing) gateway devices for legacy Anna, P1 and Stretch.
635631
636632
Use the home_location or FAKE_APPL as device id.
637633
"""
638-
if self._smile_legacy:
639-
self.gateway_id = self._home_location
640-
if self.smile_type == "power":
641-
self.gateway_id = FAKE_APPL
642-
643-
self._appl_data[self.gateway_id] = {"dev_class": "gateway"}
644-
for key, value in {
645-
"firmware": self.smile_fw_version,
646-
"location": self._home_location,
647-
"mac_address": self.smile_mac_address,
648-
"model": self.smile_model,
649-
"name": self.smile_name,
650-
"zigbee_mac_address": self.smile_zigbee_mac_address,
651-
"vendor": "Plugwise",
652-
}.items():
653-
if value is not None:
654-
self._appl_data[self.gateway_id].update({key: value}) # type: ignore[misc]
634+
self.gateway_id = self._home_location
635+
if self.smile_type == "power":
636+
self.gateway_id = FAKE_APPL
655637

656-
if self.smile_type == "power":
657-
# For legacy P1 collect the connected SmartMeter info
658-
appl = Munch()
659-
appl = self._p1_smartmeter_info_finder(appl)
638+
self._appl_data[self.gateway_id] = {"dev_class": "gateway"}
639+
for key, value in {
640+
"firmware": self.smile_fw_version,
641+
"location": self._home_location,
642+
"mac_address": self.smile_mac_address,
643+
"model": self.smile_model,
644+
"name": self.smile_name,
645+
"zigbee_mac_address": self.smile_zigbee_mac_address,
646+
"vendor": "Plugwise",
647+
}.items():
648+
if value is not None:
649+
self._appl_data[self.gateway_id].update({key: value}) # type: ignore[misc]
660650

661651
def _all_appliances(self) -> None:
662652
"""Collect all appliances with relevant info."""
663653
self._all_locations()
664654

665-
self._create_legacy_gateway()
666-
# Legacy P1 has no more devices
667-
if self._smile_legacy and self.smile_type == "power":
668-
return
655+
if self._smile_legacy:
656+
self._create_legacy_gateway()
657+
# For legacy P1 collect the connected SmartMeter info
658+
if self.smile_type == "power":
659+
appl = Munch()
660+
self._p1_smartmeter_info_finder(appl)
661+
# Legacy P1 has no more devices
662+
return
669663

670664
for appliance in self._appliances.findall("./appliance"):
671665
appl = Munch()
@@ -728,12 +722,12 @@ def _all_appliances(self) -> None:
728722

729723
# For non-legacy P1 collect the connected SmartMeter info
730724
if self.smile_type == "power":
731-
appl = self._p1_smartmeter_info_finder(appl)
725+
self._p1_smartmeter_info_finder(appl)
732726
# P1: for gateway and smartmeter switch device_id - part 2
733727
for item in self._appl_data:
734728
if item != self.gateway_id:
735729
self.gateway_id = item
736-
# Leave for-loop to avoid a 2nd switch
730+
# Leave for-loop to avoid a 2nd device_id switch
737731
break
738732

739733
def _match_locations(self) -> dict[str, ThermoLoc]:
@@ -744,7 +738,7 @@ def _match_locations(self) -> dict[str, ThermoLoc]:
744738

745739
self._all_appliances()
746740
for location_id, location_details in self._loc_data.items():
747-
for _, appliance_details in self._appl_data.items():
741+
for appliance_details in list(self._appl_data.values()):
748742
if appliance_details["location"] == location_id:
749743
location_details.update(
750744
{"master": None, "master_prio": 0, "slaves": set()}
@@ -949,6 +943,7 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
949943
):
950944
if data.get("c_heating_state") and not data.get("heating_state"):
951945
data["heating_state"] = True
946+
# For Adam + OnOff cooling heating_state = True means cooling is active
952947
if self._cooling_present:
953948
self._cooling_active = True
954949

plugwise/smile.py

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DEFAULT_TIMEOUT,
2020
DEFAULT_USERNAME,
2121
DOMAIN_OBJECTS,
22+
LEGACY_SMILES,
2223
LOCATIONS,
2324
LOGGER,
2425
MAX_SETPOINT,
@@ -53,13 +54,7 @@ class SmileData(SmileHelper):
5354

5455
def update_for_cooling(self, devices: dict[str, DeviceData]) -> None:
5556
"""Helper-function for adding/updating various cooling-related values."""
56-
for _, device in devices.items():
57-
# For Anna or Adama + cooling, modify cooling_state based on provided info by Plugwise
58-
if device["dev_class"] == "heater_central" and self._cooling_present:
59-
device["binary_sensors"]["cooling_state"] = False
60-
if self._cooling_active:
61-
device["binary_sensors"]["cooling_state"] = True
62-
57+
for device in list(devices.values()):
6358
# For Adam + on/off cooling, modify heating_state and cooling_state
6459
# based on provided info by Plugwise
6560
if (
@@ -76,8 +71,8 @@ def update_for_cooling(self, devices: dict[str, DeviceData]) -> None:
7671
if device["dev_class"] not in ZONE_THERMOSTATS:
7772
continue
7873

74+
# For heating + cooling, replace setpoint with setpoint_high/_low
7975
if self._cooling_present:
80-
# Replace setpoint with setpoint_high/_low
8176
thermostat = device["thermostat"]
8277
sensors = device["sensors"]
8378
max_setpoint = MAX_SETPOINT
@@ -152,7 +147,7 @@ def get_all_devices(self) -> None:
152147
self._cooling_present = False
153148
if search.find(locator_1) is not None:
154149
self._cooling_present = True
155-
# Alternative method for the Anna with Elga, or alternative method for the Anna with Loria/Thermastage
150+
# Alternative method for the Anna with Elga
156151
elif search.find(locator_2) is not None:
157152
self._cooling_present = True
158153

@@ -188,11 +183,14 @@ def _device_data_adam(
188183
"""Helper-function for _get_device_data().
189184
Determine Adam device data.
190185
"""
191-
if self.smile_name == "Adam":
192-
# Indicate heating_state based on valves being open in case of city-provided heating
193-
if details.get("dev_class") == "heater_central":
194-
if self._on_off_device and self._heating_valves() is not None:
195-
device_data["heating_state"] = self._heating_valves() != 0
186+
# Indicate heating_state based on valves being open in case of city-provided heating
187+
if (
188+
self.smile_name == "Adam"
189+
and details.get("dev_class") == "heater_central"
190+
and self._on_off_device
191+
and self._heating_valves() is not None
192+
):
193+
device_data["heating_state"] = self._heating_valves() != 0
196194

197195
return device_data
198196

@@ -210,7 +208,6 @@ def _device_data_climate(
210208
if presets := self._presets(loc_id):
211209
presets_list = list(presets)
212210
device_data["preset_modes"] = presets_list
213-
214211
device_data["active_preset"] = self._preset(loc_id)
215212

216213
# Schedule
@@ -258,16 +255,16 @@ def _check_availability(
258255
# OpenTherm device
259256
if details["dev_class"] == "heater_central" and details["name"] != "OnOff":
260257
device_data["available"] = True
261-
for _, data in self._notifications.items():
262-
for _, msg in data.items():
258+
for data in list(self._notifications.values()):
259+
for msg in list(data.values()):
263260
if "no OpenTherm communication" in msg:
264261
device_data["available"] = False
265262

266263
# Smartmeter
267264
if details["dev_class"] == "smartmeter":
268265
device_data["available"] = True
269-
for _, data in self._notifications.items():
270-
for _, msg in data.items():
266+
for data in list(self._notifications.values()):
267+
for msg in list(data.values()):
271268
if "P1 does not seem to be connected to a smart meter" in msg:
272269
device_data["available"] = False
273270

@@ -299,31 +296,30 @@ def _get_device_data(self, dev_id: str) -> DeviceData:
299296
device_data.pop("thermostat")
300297

301298
# Generic
302-
if details["dev_class"] == "gateway" or dev_id == self.gateway_id:
303-
if self.smile_type == "thermostat":
304-
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
305-
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
306-
outdoor_temperature = self._object_value(
307-
self._home_location, "outdoor_temperature"
308-
)
309-
if outdoor_temperature is not None:
310-
device_data["outdoor_temperature"] = outdoor_temperature
299+
if self.smile_type == "thermostat" and details["dev_class"] == "gateway":
300+
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
301+
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
302+
outdoor_temperature = self._object_value(
303+
self._home_location, "outdoor_temperature"
304+
)
305+
if outdoor_temperature is not None:
306+
device_data["outdoor_temperature"] = outdoor_temperature
311307

312-
# Show the allowed regulation modes
313-
if self._reg_allowed_modes:
314-
device_data["regulation_modes"] = self._reg_allowed_modes
308+
# Show the allowed regulation modes
309+
if self._reg_allowed_modes:
310+
device_data["regulation_modes"] = self._reg_allowed_modes
315311

316312
# Show the allowed dhw_modes
317-
if details["dev_class"] == "heater_central":
318-
if self._dhw_allowed_modes:
319-
device_data["dhw_modes"] = self._dhw_allowed_modes
313+
if details["dev_class"] == "heater_central" and self._dhw_allowed_modes:
314+
device_data["dhw_modes"] = self._dhw_allowed_modes
320315

321-
if details["dev_class"] == "smartmeter":
322-
# Get P1 data from LOCATIONS
323-
if (
324-
power_data := self._power_data_from_location(details["location"])
325-
) is not None:
326-
device_data.update(power_data)
316+
# Get P1 data from LOCATIONS
317+
if (
318+
details["dev_class"] == "smartmeter"
319+
and (power_data := self._power_data_from_location(details["location"]))
320+
is not None
321+
):
322+
device_data.update(power_data)
327323

328324
# Check availability of non-legacy wired-connected devices
329325
if not self._smile_legacy:
@@ -388,14 +384,13 @@ async def connect(self) -> bool:
388384
models.append(model.text)
389385

390386
dsmrmain = result.find("./module/protocols/dsmrmain")
391-
if "Plugwise" not in names:
392-
if dsmrmain is None: # pragma: no cover
393-
LOGGER.error(
394-
"Connected but expected text not returned, we got %s. Please create \
395-
an issue on http://github.com/plugwise/python-plugwise",
396-
result,
397-
)
398-
raise ResponseError
387+
if "Plugwise" not in names and dsmrmain is None: # pragma: no cover
388+
LOGGER.error(
389+
"Connected but expected text not returned, we got %s. Please create \
390+
an issue on http://github.com/plugwise/python-plugwise",
391+
result,
392+
)
393+
raise ResponseError
399394

400395
# Check if Anna is connected to an Adam
401396
if "159.2" in models:
@@ -499,7 +494,7 @@ async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
499494
self.smile_type = SMILES[target_smile].smile_type
500495
self.smile_version = (self.smile_fw_version, ver)
501496

502-
if target_smile in ("smile_thermo_v1", "smile_v2", "stretch_v3", "stretch_v2"):
497+
if target_smile in LEGACY_SMILES:
503498
self._smile_legacy = True
504499

505500
if self.smile_type == "stretch":

0 commit comments

Comments
 (0)