55
66import asyncio
77import datetime as dt
8+ from typing import Any , cast
89
910# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
1011from aiohttp import BasicAuth , ClientError , ClientResponse , ClientSession , ClientTimeout
4546 SWITCHES ,
4647 TEMP_CELSIUS ,
4748 THERMOSTAT_CLASSES ,
49+ TOGGLES ,
4850 UOM ,
4951 ApplianceData ,
5052 DeviceData ,
@@ -99,9 +101,8 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None:
99101 return name
100102
101103
102- def _get_actuator_functionalities (xml : etree ) -> DeviceData :
104+ def _get_actuator_functionalities (xml : etree , data : dict [ str , Any ] ) -> None :
103105 """Helper-function for _get_appliance_data()."""
104- data : DeviceData = {}
105106 for item in ACTIVE_ACTUATORS :
106107 temp_dict : dict [str , float ] = {}
107108 for key in LIMITS :
@@ -113,9 +114,7 @@ def _get_actuator_functionalities(xml: etree) -> DeviceData:
113114 temp_dict .update ({key : format_measure (function .text , TEMP_CELSIUS )})
114115
115116 if temp_dict :
116- data [item ] = temp_dict # type: ignore [literal-required]
117-
118- return data
117+ data [item ] = temp_dict
119118
120119
121120def schedules_temps (
@@ -848,9 +847,9 @@ def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, str]:
848847 def _appliance_measurements (
849848 self ,
850849 appliance : etree ,
851- data : DeviceData ,
850+ data : dict [ str , Any ] ,
852851 measurements : dict [str , DATA | UOM ],
853- ) -> DeviceData :
852+ ) -> None :
854853 """Helper-function for _get_appliance_data() - collect appliance measurement data."""
855854 for measurement , attrs in measurements .items ():
856855 p_locator = f'.//logs/point_log[type="{ measurement } "]/period/measurement'
@@ -869,28 +868,28 @@ def _appliance_measurements(
869868 if new_name := getattr (attrs , ATTR_NAME , None ):
870869 measurement = new_name
871870
872- data [measurement ] = appl_p_loc .text # type: ignore [literal-required]
871+ data [measurement ] = appl_p_loc .text
873872 # measurements with states "on" or "off" that need to be passed directly
874873 if measurement not in ["dhw_mode" , "regulation_mode" ]:
875- data [measurement ] = format_measure (appl_p_loc .text , getattr (attrs , ATTR_UNIT_OF_MEASUREMENT )) # type: ignore [literal-required]
874+ data [measurement ] = format_measure (
875+ appl_p_loc .text , getattr (attrs , ATTR_UNIT_OF_MEASUREMENT )
876+ )
876877
877878 # Anna: save cooling-related measurements for later use
878879 # Use the local outdoor temperature as reference for turning cooling on/off
879880 if measurement == "cooling_activation_outdoor_temperature" :
880- self ._cooling_activation_outdoor_temp = data [measurement ] # type: ignore [literal-required]
881+ self ._cooling_activation_outdoor_temp = data [measurement ]
881882 if measurement == "cooling_deactivation_threshold" :
882- self ._cooling_deactivation_threshold = data [measurement ] # type: ignore [literal-required]
883+ self ._cooling_deactivation_threshold = data [measurement ]
883884 if measurement == "outdoor_air_temperature" :
884- self ._outdoor_temp = data [measurement ] # type: ignore [literal-required]
885+ self ._outdoor_temp = data [measurement ]
885886
886887 i_locator = f'.//logs/interval_log[type="{ measurement } "]/period/measurement'
887888 if (appl_i_loc := appliance .find (i_locator )) is not None :
888889 name = f"{ measurement } _interval"
889- data [name ] = format_measure (appl_i_loc .text , ENERGY_WATT_HOUR ) # type: ignore [literal-required]
890-
891- return data
890+ data [name ] = format_measure (appl_i_loc .text , ENERGY_WATT_HOUR )
892891
893- def _wireless_availablity (self , appliance : etree , data : DeviceData ) -> None :
892+ def _wireless_availablity (self , appliance : etree , data : dict [ str , Any ] ) -> None :
894893 """Helper-function for _get_appliance_data().
895894 Collect the availablity-status for wireless connected devices.
896895 """
@@ -913,10 +912,10 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
913912 Collect the appliance-data based on device id.
914913 Determined from APPLIANCES, for legacy from DOMAIN_OBJECTS.
915914 """
916- data : DeviceData = {}
915+ data : dict [ str , Any ] = {}
917916 # P1 legacy has no APPLIANCES, also not present in DOMAIN_OBJECTS
918917 if self ._smile_legacy and self .smile_type == "power" :
919- return data
918+ return cast ( DeviceData , data )
920919
921920 measurements = DEVICE_MEASUREMENTS
922921 if d_id == self ._heater_id :
@@ -926,11 +925,13 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
926925 appliance := self ._appliances .find (f'./appliance[@id="{ d_id } "]' )
927926 ) is not None :
928927
929- data = self ._appliance_measurements (appliance , data , measurements )
930- data .update (self ._get_lock_state (appliance ))
931- if (appl_type := appliance .find ("type" )) is not None :
932- if appl_type .text in ACTUATOR_CLASSES :
933- data .update (_get_actuator_functionalities (appliance ))
928+ self ._appliance_measurements (appliance , data , measurements )
929+ self ._get_lock_state (appliance , data )
930+ for toggle , name in TOGGLES .items ():
931+ self ._get_toggle_state (appliance , toggle , name , data )
932+
933+ if appliance .find ("type" ).text in ACTUATOR_CLASSES :
934+ _get_actuator_functionalities (appliance , data )
934935
935936 # Collect availability-status for wireless connected devices to Adam
936937 self ._wireless_availablity (appliance , data )
@@ -960,26 +961,31 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
960961 if "temperature" in data :
961962 data .pop ("heating_state" , None )
962963
963- if d_id == self ._heater_id :
964- # Adam
965- if self .smile_name == "Smile Anna" :
966- # Use elga_status_code or cooling_enabled to set _cooling_enabled to True
967- if self ._cooling_present :
968- # Elga:
969- if "elga_status_code" in data :
970- self ._cooling_enabled = data ["elga_status_code" ] in [8 , 9 ]
971- self ._cooling_active = data ["elga_status_code" ] == 8
972- data .pop ("elga_status_code" , None )
973- # Loria/Thermastate: look at cooling_state, not at cooling_enabled, not available on R32!
974- elif "cooling_ena_switch" in data :
975- self ._cooling_enabled = data ["cooling_ena_switch" ]
976- self ._cooling_active = data ["cooling_state" ]
964+ if (
965+ d_id == self ._heater_id
966+ and self .smile_name == "Smile Anna"
967+ and self ._cooling_present
968+ ):
969+ # Use elga_status_code or cooling_enabled to set _cooling_enabled to True
970+ if "elga_status_code" in data :
971+ self ._cooling_enabled = data ["elga_status_code" ] in [8 , 9 ]
972+ self ._cooling_active = data ["elga_status_code" ] == 8
973+ data .pop ("elga_status_code" , None )
974+ # Loria/Thermastate: look at cooling_state, not at cooling_enabled, not available on R32!
975+ # Anna + Elga >= 4.3.7: the Elga cooling-enabled state is shown but there is no cooling-switch
976+ for item in ("cooling_ena_switch" , "cooling_enabled" ):
977+ if item in data :
978+ self ._cooling_enabled = data [item ]
979+ self ._cooling_active = data ["cooling_state" ]
980+
981+ if all (item in data for item in ("cooling_ena_switch" , "cooling_enabled" )):
982+ data .pop ("cooling_enabled" )
977983
978984 # Don't show cooling_state when no cooling present
979985 if not self ._cooling_present and "cooling_state" in data :
980986 data .pop ("cooling_state" )
981987
982- return data
988+ return cast ( DeviceData , data )
983989
984990 def _rank_thermostat (
985991 self ,
@@ -1333,11 +1339,10 @@ def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
13331339
13341340 return val
13351341
1336- def _get_lock_state (self , xml : etree ) -> DeviceData :
1342+ def _get_lock_state (self , xml : etree , data : dict [ str , Any ] ) -> None :
13371343 """Helper-function for _get_appliance_data().
13381344 Adam & Stretches: obtain the relay-switch lock state.
13391345 """
1340- data : DeviceData = {}
13411346 actuator = "actuator_functionalities"
13421347 func_type = "relay_functionality"
13431348 if self ._stretch_v2 :
@@ -1348,7 +1353,19 @@ def _get_lock_state(self, xml: etree) -> DeviceData:
13481353 if (found := xml .find (locator )) is not None :
13491354 data ["lock" ] = found .text == "true"
13501355
1351- return data
1356+ def _get_toggle_state (
1357+ self , xml : etree , toggle : str , name : str , data : dict [str , Any ]
1358+ ) -> None :
1359+ """Helper-function for _get_appliance_data().
1360+ Obtain the toggle state of 'toggle'.
1361+ """
1362+ if xml .find ("type" ).text == "heater_central" :
1363+ locator = "./actuator_functionalities/toggle_functionality"
1364+ if found := xml .findall (locator ):
1365+ for item in found :
1366+ if (toggle_type := item .find ("type" )) is not None :
1367+ if toggle_type .text == toggle :
1368+ data .update ({name : item .find ("state" ).text == "on" })
13521369
13531370 def _update_device_with_dicts (
13541371 self ,
0 commit comments