diff --git a/custom_components/luxtronik/binary_sensor.py b/custom_components/luxtronik/binary_sensor.py index 61641de..179ad42 100644 --- a/custom_components/luxtronik/binary_sensor.py +++ b/custom_components/luxtronik/binary_sensor.py @@ -27,11 +27,14 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikBinarySensorEntity( - hass, entry, coordinator, description, description.device_key - ) - for description in BINARY_SENSORS - if coordinator.entity_active(description) + ( + LuxtronikBinarySensorEntity( + hass, entry, coordinator, description, description.device_key + ) + for description in BINARY_SENSORS + if coordinator.entity_active(description) + ), + True, ) diff --git a/custom_components/luxtronik/climate.py b/custom_components/luxtronik/climate.py index 0246545..6a5a31f 100644 --- a/custom_components/luxtronik/climate.py +++ b/custom_components/luxtronik/climate.py @@ -155,9 +155,12 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikThermostat(hass, entry, coordinator, description) - for description in THERMOSTATS - if coordinator.entity_active(description) + ( + LuxtronikThermostat(hass, entry, coordinator, description) + for description in THERMOSTATS + if coordinator.entity_active(description) + ), + True, ) @@ -179,6 +182,7 @@ def as_dict(self) -> dict[str, Any]: class LuxtronikThermostat(LuxtronikEntity, ClimateEntity, RestoreEntity): """The thermostat class for Luxtronik thermostats.""" + # region Attributes entity_description: LuxtronikClimateDescription _last_hvac_mode_before_preset: str | None = None @@ -194,6 +198,7 @@ class LuxtronikThermostat(LuxtronikEntity, ClimateEntity, RestoreEntity): _attr_preset_mode: str | None = None _attr_current_lux_operation = LuxOperationMode.no_request + # endregion Attributes def __init__( self, @@ -265,11 +270,11 @@ def _handle_coordinator_update( correction_factor = get_sensor_data( data, self.entity_description.luxtronik_key_correction_factor.value, False ) - #LOGGER.info(f"self._attr_target_temperature={self._attr_target_temperature}") - #LOGGER.info(f"self._attr_current_temperature={self._attr_current_temperature}") - #LOGGER.info(f"correction_factor={correction_factor}") - #LOGGER.info(f"lux_action={lux_action}") - #LOGGER.info(f"_attr_hvac_action={self._attr_hvac_action}") + # LOGGER.info(f"self._attr_target_temperature={self._attr_target_temperature}") + # LOGGER.info(f"self._attr_current_temperature={self._attr_current_temperature}") + # LOGGER.info(f"correction_factor={correction_factor}") + # LOGGER.info(f"lux_action={lux_action}") + # LOGGER.info(f"_attr_hvac_action={self._attr_hvac_action}") if ( self._attr_target_temperature is not None and self._attr_current_temperature is not None # noqa: W503 @@ -284,10 +289,10 @@ def _handle_coordinator_update( self.entity_description.luxtronik_key_correction_target.value ) correction_current = get_sensor_data(data, key_correction_target) - #LOGGER.info(f"correction_current={correction_current}") - #LOGGER.info(f"correction={correction}") + # LOGGER.info(f"correction_current={correction_current}") + # LOGGER.info(f"correction={correction}") if correction_current is None or correction_current != correction: - #LOGGER.info(f'key_correction_target={key_correction_target.split(".")[1]}') + # LOGGER.info(f'key_correction_target={key_correction_target.split(".")[1]}') _ = self.coordinator.write( key_correction_target.split(".")[1], correction ) # mypy: allow-unused-coroutine @@ -307,6 +312,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" + self._attr_hvac_mode = hvac_mode lux_mode = [ k for k, v in self.entity_description.hvac_mode_mapping.items() @@ -316,7 +322,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode == PRESET_COMFORT: + self._attr_preset_mode = preset_mode + if preset_mode in [PRESET_COMFORT]: lux_mode = LuxMode.automatic elif preset_mode != PRESET_NONE: lux_mode = [k for k, v in HVAC_PRESET_MAPPING.items() if v == preset_mode][ @@ -333,12 +340,14 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" + self._attr_is_aux_heat = True if self._last_hvac_mode_before_preset is None: self._last_hvac_mode_before_preset = self._attr_hvac_mode await self._async_set_lux_mode(LuxMode.second_heatsource.value) async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" + self._attr_is_aux_heat = False if (self._last_hvac_mode_before_preset is None) or ( not self._last_hvac_mode_before_preset in HVAC_PRESET_MAPPING ): diff --git a/custom_components/luxtronik/common.py b/custom_components/luxtronik/common.py index 2e23aa1..6e22cb5 100644 --- a/custom_components/luxtronik/common.py +++ b/custom_components/luxtronik/common.py @@ -30,7 +30,7 @@ def get_sensor_data( sensors: LuxtronikCoordinatorData, luxtronik_key: str | LP | LC | LV, - warn_unset = True, + warn_unset=True, ) -> Any: """Get sensor data.""" if luxtronik_key is None or "." not in luxtronik_key: @@ -39,6 +39,8 @@ def get_sensor_data( "Function get_sensor_data luxtronik_key %s is None", luxtronik_key ) return None + elif "{" in luxtronik_key: + return None key = luxtronik_key.split(".") group: str = key[0] sensor_id: str = key[1] diff --git a/custom_components/luxtronik/config_flow.py b/custom_components/luxtronik/config_flow.py index e19e436..cdb0e29 100644 --- a/custom_components/luxtronik/config_flow.py +++ b/custom_components/luxtronik/config_flow.py @@ -91,7 +91,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: class LuxtronikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Luxtronik heatpump controller config flow.""" - VERSION = 5 + VERSION = 7 _hassio_discovery = None _discovery_host = None _discovery_port = None @@ -307,7 +307,7 @@ def async_get_options_flow( return LuxtronikOptionsFlowHandler(config_entry) -class LuxtronikOptionsFlowHandler(config_entries.OptionsFlow): +class LuxtronikOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): """Handle a Luxtronik options flow.""" _sensor_prefix = DOMAIN @@ -318,9 +318,7 @@ def __init__(self, config_entry): def _get_value(self, key: str, default=None): """Return a value from Luxtronik.""" - return self.config_entry.options.get( - key, self.config_entry.data.get(key, default) - ) + return self.options.get(key, self.config_entry.data.get(key, default)) # def _get_options_schema(self): # """Return a schema for Luxtronik configuration options.""" @@ -354,7 +352,7 @@ async def async_step_user(self, user_input=None) -> FlowResult: self.hass.config_entries.async_update_entry( self.config_entry, data=self.config_entry.data | user_input, - options=self.config_entry.options, + options=self.options, ) # Store empty options because we have store it in data: return self.async_create_entry(title="", data={}) diff --git a/custom_components/luxtronik/const.py b/custom_components/luxtronik/const.py index c082d25..f3084d5 100644 --- a/custom_components/luxtronik/const.py +++ b/custom_components/luxtronik/const.py @@ -6,7 +6,7 @@ import logging from typing import Final -from homeassistant.backports.enum import StrEnum +from enum import StrEnum from homeassistant.const import Platform from homeassistant.helpers.typing import StateType diff --git a/custom_components/luxtronik/lux_helper.py b/custom_components/luxtronik/lux_helper.py index 9bf8cb1..89ff69f 100644 --- a/custom_components/luxtronik/lux_helper.py +++ b/custom_components/luxtronik/lux_helper.py @@ -143,6 +143,13 @@ def get_manufacturer_firmware_url_by_model(model: str) -> str: def _is_socket_closed(sock: socket.socket) -> bool: + try: + if sock.fileno() < 0: + return True + except Exception as err: # pylint: disable=broad-except + LOGGER.exception( + "Unexpected exception when checking if a socket is closed", exc_info=err + ) try: # this will try to read bytes without blocking and also without removing them from buffer (peek only) last_timeout = sock.gettimeout() @@ -189,6 +196,9 @@ def __init__( def __del__(self): """Luxtronik helper descructor.""" + self._disconnect() + + def _disconnect(self): if self._socket is not None: if not _is_socket_closed(self._socket): self._socket.close() @@ -215,7 +225,17 @@ def _read_write(self, write=False): socket.SOCK_STREAM, ) if is_none or _is_socket_closed(self._socket): - self._socket.connect((self._host, self._port)) + try: + self._socket.connect((self._host, self._port)) + except OSError as err: + if err.errno == 9: # Bad file descr. + self._disconnect() + return + elif err.errno == 106: + self._socket.close() + self._socket.connect((self._host, self._port)) + else: + raise err self._socket.settimeout(self._socket_timeout) LOGGER.info( "Connected to Luxtronik heatpump %s:%s with timeout %ss", @@ -312,6 +332,9 @@ def _read_visibilities(self): self._max_data_length, ) return + elif length <= 0: + # Force reconnect for the next readout + self._disconnect() LOGGER.debug("Length %s", length) for _ in range(0, length): try: diff --git a/custom_components/luxtronik/manifest.json b/custom_components/luxtronik/manifest.json index 3795716..82313a2 100755 --- a/custom_components/luxtronik/manifest.json +++ b/custom_components/luxtronik/manifest.json @@ -9,7 +9,7 @@ "after_dependencies": [], "codeowners": ["@BenPru"], "iot_class": "local_polling", - "version": "2023.10.29", + "version": "2023.10.30", "homeassistant": "2023.1.0", "dhcp": [ { "macaddress": "000E8C*" }, diff --git a/custom_components/luxtronik/number.py b/custom_components/luxtronik/number.py index 654228f..b77b0d6 100644 --- a/custom_components/luxtronik/number.py +++ b/custom_components/luxtronik/number.py @@ -36,11 +36,14 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikNumberEntity( - hass, entry, coordinator, description, description.device_key - ) - for description in NUMBER_SENSORS - if coordinator.entity_active(description) + ( + LuxtronikNumberEntity( + hass, entry, coordinator, description, description.device_key + ) + for description in NUMBER_SENSORS + if coordinator.entity_active(description) + ), + True, ) diff --git a/custom_components/luxtronik/sensor.py b/custom_components/luxtronik/sensor.py index d4fbd85..6a80a98 100644 --- a/custom_components/luxtronik/sensor.py +++ b/custom_components/luxtronik/sensor.py @@ -45,27 +45,36 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikSensorEntity( - hass, entry, coordinator, description, description.device_key - ) - for description in SENSORS - if coordinator.entity_active(description) + ( + LuxtronikSensorEntity( + hass, entry, coordinator, description, description.device_key + ) + for description in SENSORS + if coordinator.entity_active(description) + ), + True, ) async_add_entities( - LuxtronikStatusSensorEntity( - hass, entry, coordinator, description, description.device_key - ) - for description in SENSORS_STATUS - if coordinator.entity_active(description) + ( + LuxtronikStatusSensorEntity( + hass, entry, coordinator, description, description.device_key + ) + for description in SENSORS_STATUS + if coordinator.entity_active(description) + ), + True, ) async_add_entities( - LuxtronikIndexSensor( - hass, entry, coordinator, description, description.device_key - ) - for description in SENSORS_INDEX - if coordinator.entity_active(description) + ( + LuxtronikIndexSensor( + hass, entry, coordinator, description, description.device_key + ) + for description in SENSORS_INDEX + if coordinator.entity_active(description) + ), + True, ) diff --git a/custom_components/luxtronik/switch.py b/custom_components/luxtronik/switch.py index aad046c..5a6fe5a 100644 --- a/custom_components/luxtronik/switch.py +++ b/custom_components/luxtronik/switch.py @@ -29,11 +29,14 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikSwitchEntity( - hass, entry, coordinator, description, description.device_key - ) - for description in SWITCHES - if coordinator.entity_active(description) + ( + LuxtronikSwitchEntity( + hass, entry, coordinator, description, description.device_key + ) + for description in SWITCHES + if coordinator.entity_active(description) + ), + True, ) diff --git a/custom_components/luxtronik/update.py b/custom_components/luxtronik/update.py index 9ea0090..b672824 100644 --- a/custom_components/luxtronik/update.py +++ b/custom_components/luxtronik/update.py @@ -66,7 +66,7 @@ async def async_setup_entry( ) entities = [update_entity] - async_add_entities(entities) + async_add_entities(entities, True) class LuxtronikUpdateEntity(LuxtronikEntity, UpdateEntity): diff --git a/custom_components/luxtronik/water_heater.py b/custom_components/luxtronik/water_heater.py index ed04300..3b9d863 100644 --- a/custom_components/luxtronik/water_heater.py +++ b/custom_components/luxtronik/water_heater.py @@ -81,9 +81,12 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() async_add_entities( - LuxtronikWaterHeater(hass, config_entry, coordinator, description) - for description in WATER_HEATERS - if coordinator.entity_active(description) + ( + LuxtronikWaterHeater(hass, config_entry, coordinator, description) + for description in WATER_HEATERS + if coordinator.entity_active(description) + ), + True, )