diff --git a/custom_components/heatmiserneo/__init__.py b/custom_components/heatmiserneo/__init__.py index 5b6a13f..08ab0fa 100644 --- a/custom_components/heatmiserneo/__init__.py +++ b/custom_components/heatmiserneo/__init__.py @@ -60,9 +60,18 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_options)) + return True +async def async_update_options( + hass: HomeAssistant, entry: HeatmiserNeoConfigEntry +) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry( hass: HomeAssistant, entry: HeatmiserNeoConfigEntry ) -> bool: diff --git a/custom_components/heatmiserneo/climate.py b/custom_components/heatmiserneo/climate.py index ad70e8f..96dcff8 100644 --- a/custom_components/heatmiserneo/climate.py +++ b/custom_components/heatmiserneo/climate.py @@ -5,7 +5,6 @@ from collections import OrderedDict from dataclasses import dataclass from datetime import timedelta -from functools import cached_property import logging from neohubapi.neohub import HCMode, NeoHub, NeoStat @@ -48,6 +47,8 @@ HEATMISER_TYPE_IDS_THERMOSTAT, SERVICE_HOLD_OFF, SERVICE_HOLD_ON, + AvailableMode, + GlobalSystemType, ) from .entity import HeatmiserNeoEntity, HeatmiserNeoEntityDescription @@ -173,26 +174,61 @@ def __init__( self._attr_max_temp = neostat.max_temperature_limit self._attr_min_temp = neostat.min_temperature_limit self._attr_preset_modes = [PRESET_HOME, PRESET_BOOST, PRESET_AWAY] - self._attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO] + + supported_features = ( + ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.PRESET_MODE + ) hvac_modes = [] + + heating = False + cooling = False if hasattr(neostat, "standby"): hvac_modes.append(HVACMode.OFF) # The following devices support Heating modes if self.data.device_type in HEATMISER_TYPE_IDS_HC: - if self.system_data.GLOBAL_SYSTEM_TYPE == "HeatOnly": + if self.system_data.GLOBAL_SYSTEM_TYPE == GlobalSystemType.HEAT_ONLY: hvac_modes.append(HVACMode.HEAT) - elif self.system_data.GLOBAL_SYSTEM_TYPE == "CoolOnly": + elif self.system_data.GLOBAL_SYSTEM_TYPE == GlobalSystemType.COOL_ONLY: hvac_modes.append(HVACMode.COOL) else: - hvac_modes.append(HVACMode.HEAT) - hvac_modes.append(HVACMode.COOL) - hvac_modes.append(HVACMode.HEAT_COOL) - hvac_modes.append(HVACMode.FAN_ONLY) + if AvailableMode.HEAT in neostat.available_modes: + hvac_modes.append(HVACMode.HEAT) + heating = True + if AvailableMode.COOL in neostat.available_modes: + hvac_modes.append(HVACMode.COOL) + cooling = True + if AvailableMode.AUTO in neostat.available_modes: + hvac_modes.append(HVACMode.HEAT_COOL) + heating = True + cooling = True + + if AvailableMode.VENT in neostat.available_modes: + supported_features = supported_features | ClimateEntityFeature.FAN_MODE + hvac_modes.append(HVACMode.FAN_ONLY) + self._attr_fan_modes = [ + FAN_OFF, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, + ] else: hvac_modes.append(HVACMode.HEAT) + if heating and cooling: + supported_features = ( + supported_features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + else: + supported_features = ( + supported_features | ClimateEntityFeature.TARGET_TEMPERATURE + ) + self._attr_hvac_modes = hvac_modes + self._attr_supported_features = supported_features async def async_set_hvac_mode(self, hvac_mode): """Set hvac mode.""" @@ -407,36 +443,6 @@ async def unset_hold(self): return result - @cached_property - def supported_features(self): - """Return the list of supported features.""" - # Do this based on device type - - # All thermostats should have on and off - supported_features = ( - ClimateEntityFeature.TURN_ON - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.PRESET_MODE - ) - - if self.data.device_type in HEATMISER_TYPE_IDS_HC: - # neoStat-HC - if self.system_data.GLOBAL_SYSTEM_TYPE not in ["HeatOnly", "CoolOnly"]: - supported_features = ( - supported_features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) - else: - supported_features = ( - supported_features | ClimateEntityFeature.TARGET_TEMPERATURE - ) - supported_features = supported_features | ClimateEntityFeature.FAN_MODE - else: - supported_features = ( - supported_features | ClimateEntityFeature.TARGET_TEMPERATURE - ) - - return supported_features - @property def target_temperature(self): """Return the temperature we try to reach.""" diff --git a/custom_components/heatmiserneo/config_flow.py b/custom_components/heatmiserneo/config_flow.py index 4cf9e36..01551b2 100644 --- a/custom_components/heatmiserneo/config_flow.py +++ b/custom_components/heatmiserneo/config_flow.py @@ -9,27 +9,34 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.climate import HVACMode from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get, -) -from .const import CONF_HVAC_MODES, DEFAULT_HOST, DEFAULT_PORT, DOMAIN, AvailableMode +from . import HeatmiserNeoConfigEntry +from .const import ( + CONF_HVAC_MODES, + DEFAULT_HOST, + DEFAULT_PORT, + DOMAIN, + HEATMISER_TYPE_IDS_HC, + AvailableMode, + GlobalSystemType, +) _LOGGER = logging.getLogger(__name__) -modes = { - AvailableMode.AUTO: HVACMode.HEAT_COOL, - AvailableMode.COOL: HVACMode.COOL, - AvailableMode.HEAT: HVACMode.HEAT, - AvailableMode.VENT: HVACMode.FAN_ONLY, +AVAILABLE_MODE_MAPPING = { + AvailableMode.AUTO: "Auto", + AvailableMode.COOL: "Cool", + AvailableMode.HEAT: "Heat", + AvailableMode.VENT: "Fan Only", } -default_modes = [HVACMode.HEAT] + +SCHEMA_ATTR_DEVICE = "device" +SCHEMA_ATTR_HVAC_MODES = "hvac_modes" +SCHEMA_ATTR_MORE = "more" @config_entries.HANDLERS.register("heatmiserneo") @@ -126,14 +133,10 @@ def async_get_options_flow(config_entry): class OptionsFlowHandler(config_entries.OptionsFlow): """Handles options flow for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: HeatmiserNeoConfigEntry) -> None: """Initialize options flow.""" - self.config_entry = config_entry - self.config = ( - deepcopy(config_entry.options[CONF_HVAC_MODES]) - if CONF_HVAC_MODES in self.config_entry.options - else {} - ) + # self.config_entry = config_entry + self.config = deepcopy(config_entry.options.get(CONF_HVAC_MODES, {})) async def async_step_init( self, user_input: dict[str, str] | None = None @@ -141,44 +144,38 @@ async def async_step_init( """Manage the options for the custom component.""" errors: dict[str, str] = {} - # Grab all devices from the entity registry so we can populate the - # dropdown list that will allow a user to configure a device. - entity_registry = async_get(self.hass) - devices = async_entries_for_config_entry( - entity_registry, self.config_entry.entry_id + devices, _ = self.config_entry.runtime_data.coordinator.data + system_data = self.config_entry.runtime_data.coordinator.system_data + + devices = sorted( + [ + k + for k, v in devices.items() + if v.device_type in HEATMISER_TYPE_IDS_HC and not v.time_clock_mode + ] ) - stats = { - e.unique_id: e.capabilities - for e in devices - if e.entity_id.startswith("climate.") - } + + if len(devices) == 0: + # return await self.async_step_none() + return self.async_abort(reason="no_devices_supported") if user_input is not None: _LOGGER.debug("user_input: %s", user_input) _LOGGER.debug("original config: %s", self.config) # Remove any devices where hvac_modes have been unset. - remove_devices = [ - unique_id - for unique_id in stats - if unique_id == user_input["device"] - if len(user_input["hvac_modes"]) == 0 - ] - for unique_id in remove_devices: - if unique_id in self.config: - self.config.pop(unique_id) - - if len(user_input["hvac_modes"]) != 0: - if not errors: - # Add the new device config. - self.config[user_input["device"]] = user_input["hvac_modes"] + name = user_input[SCHEMA_ATTR_DEVICE] + if len(user_input[SCHEMA_ATTR_HVAC_MODES]) == 0: + self.config.pop(name) + elif not errors: + self.config[name] = user_input[SCHEMA_ATTR_HVAC_MODES] _LOGGER.debug("updated config: %s", self.config) if not errors: # If user selected the 'more' tickbox, show this form again # so they can configure additional devices. - if user_input.get("more", False): + if user_input.get(SCHEMA_ATTR_MORE, False): return await self.async_step_init() # Value of data will be set on the options property of the config_entry instance. @@ -186,15 +183,28 @@ async def async_step_init( title="", data={CONF_HVAC_MODES: self.config} ) + system_modes = [] + if system_data.GLOBAL_SYSTEM_TYPE == GlobalSystemType.HEAT_ONLY: + system_modes.append(AvailableMode.HEAT) + elif system_data.GLOBAL_SYSTEM_TYPE == GlobalSystemType.COOL_ONLY: + system_modes.append(AvailableMode.COOL) + else: + system_modes.append(AvailableMode.HEAT) + system_modes.append(AvailableMode.COOL) + system_modes.append(AvailableMode.AUTO) + system_modes.append(AvailableMode.VENT) + + mode_options = { + k: v for k, v in AVAILABLE_MODE_MAPPING.items() if k in system_modes + } + options_schema = vol.Schema( { - vol.Optional("device", default=list(stats.keys())): vol.In( - stats.keys() - ), + vol.Optional(SCHEMA_ATTR_DEVICE, default=devices): vol.In(devices), vol.Optional( - "hvac_modes", default=list(default_modes) - ): cv.multi_select(modes), - vol.Optional("more"): cv.boolean, + SCHEMA_ATTR_HVAC_MODES, default=list(mode_options) + ): cv.multi_select(mode_options), + vol.Optional(SCHEMA_ATTR_MORE): cv.boolean, } ) diff --git a/custom_components/heatmiserneo/const.py b/custom_components/heatmiserneo/const.py index f374058..49e6be3 100644 --- a/custom_components/heatmiserneo/const.py +++ b/custom_components/heatmiserneo/const.py @@ -144,6 +144,15 @@ class AvailableMode(str, enum.Enum): AUTO = "auto" +class GlobalSystemType(str, enum.Enum): + """Global System Types for NeoStat HC.""" + + HEAT_ONLY = "HeatOnly" + COOL_ONLY = "CoolOnly" + HEAT_COOL = "HeatOrCool" + INDEPENDENT = "Independent" + + class ModeSelectOption(str, enum.Enum): """Operating mode options for NeoPlugs and NeoStats in timer mode.""" diff --git a/custom_components/heatmiserneo/sensor.py b/custom_components/heatmiserneo/sensor.py index 357816b..8a3daef 100644 --- a/custom_components/heatmiserneo/sensor.py +++ b/custom_components/heatmiserneo/sensor.py @@ -85,6 +85,7 @@ SERVICE_DELETE_PROFILE, SERVICE_GET_PROFILE_DEFINITIONS, SERVICE_RENAME_PROFILE, + GlobalSystemType, ) from .coordinator import HeatmiserNeoCoordinator from .entity import ( @@ -616,7 +617,7 @@ class HeatmiserNeoHubSensorEntityDescription( device.device_type in HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC or ( device.device_type in HEATMISER_TYPE_IDS_HC - and sys_data.GLOBAL_SYSTEM_TYPE != "CoolOnly" + and sys_data.GLOBAL_SYSTEM_TYPE != GlobalSystemType.COOL_ONLY ) ) and not device.time_clock_mode @@ -633,7 +634,7 @@ class HeatmiserNeoHubSensorEntityDescription( setup_filter_fn=lambda device, sys_data: ( device.device_type in HEATMISER_TYPE_IDS_HC and not device.time_clock_mode - and sys_data.GLOBAL_SYSTEM_TYPE != "HeatOnly" + and sys_data.GLOBAL_SYSTEM_TYPE != GlobalSystemType.HEAT_ONLY ), unit_of_measurement_fn=lambda _, sys_data: ( HEATMISER_TEMPERATURE_UNIT_HA_UNIT.get(sys_data.CORF, None) diff --git a/custom_components/heatmiserneo/strings.json b/custom_components/heatmiserneo/strings.json index 67d6511..f05b27d 100644 --- a/custom_components/heatmiserneo/strings.json +++ b/custom_components/heatmiserneo/strings.json @@ -19,6 +19,9 @@ } }, "options": { + "abort": { + "no_devices_supported": "No devices have configurable HVAC modes" + }, "step": { "init": { "title": "Configure HVAC Modes", @@ -28,6 +31,11 @@ "more": "Configure another device" }, "description": "Select a device and configure the HVAC modes" + }, + "none": { + "title": "Configure HVAC Modes", + "data": {}, + "description": "No devices have configurable HVAC modes" } }, "error": {} diff --git a/custom_components/heatmiserneo/translations/en.json b/custom_components/heatmiserneo/translations/en.json index 858cae8..786a3f9 100644 --- a/custom_components/heatmiserneo/translations/en.json +++ b/custom_components/heatmiserneo/translations/en.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "no_devices_supported": "No devices have configurable HVAC modes" + }, "step": { "init": { "title": "Configure HVAC Modes", @@ -32,6 +35,11 @@ "more": "Configure another device" }, "description": "Select a device and configure the HVAC modes" + }, + "none": { + "title": "Configure HVAC Modes", + "data": {}, + "description": "No devices have configurable HVAC modes" } }, "error": {} diff --git a/docs/changelog.md b/docs/changelog.md index c4e19ff..8c2ec67 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,45 +1,62 @@ # Change Log +## 20250129 + +- Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/251 by @ocrease, fixes issues with HVAC Mode configuration for NeoStat HC + ## 20250126 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/248 by @ocrease, adds upsert mode to profile services. ## 20250124 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/247 by @ocrease, Improves NeoStatHC compatibility. ## 20250120 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/243 by @ocrease, changes to Zeroconf and Optimum start times. - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/246 by @ocrease, Adds service for programming thermostats. ## 20250116 + - Major release, version 3.0.0 now available. Massive thanks to @ocrease. This is a huge release, Please see previous notes for all changes. ## 20241230 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/235 by @ocrease, beta 13 fixes. ## 20241227 -- Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/232 by @ocrease, documentation update. + +- Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/232 by @ocrease, documentation update. ## 20241224 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/231 by @ocrease, documentation improvements. ## 20241220 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/230 by @ocrease, combines away sensors, tweaks repeater button. ## 20241219 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/227 by @ocrease, adds service for holiday/away mode control. - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/228 by @ocrease, fixes naming of remove button. - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/229 by @ocrease, Add documentation using GitHub pages. ## 20241218 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/226 by @ocrease, fixes profile issues in beta9 ## 20241217 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/225 by @ocrease, addresses issues with profiles not being available and other bugs in beta8. ## 20241212 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/224 by @ocrease, Bumps NeoHub API version and fixes issues in beta7. ## 20241212 + - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/223 by @ocrease, Bumps NeoHub API version and adds support for repeater and profile features. ## 20241206 @@ -67,7 +84,7 @@ - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/213 by @ocrease, improves overall quality. - Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/214 by @ocrease, fixing sensor type. -- Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/216 by @ocrease, improves robustness when devices are no longer avalible. +- Merged https://github.com/MindrustUK/Heatmiser-for-home-assistant/pull/216 by @ocrease, improves robustness when devices are no longer available. ## 20241105 diff --git a/docs/experimental.md b/docs/experimental.md index c75cb1d..05a0d8e 100644 --- a/docs/experimental.md +++ b/docs/experimental.md @@ -7,3 +7,7 @@ There are various features that have been developed but due to lack of devices t - NeoAir - NeoStatHC + +# NeoStat HC + +NeoStat HC thermostats can be configured to heat and/or cool. By default they also always have a fan mode. If the physical installation of the device does not support some modes, you can use the Configure option on the hub entry to override the available modes for each device. This only applies to NeoStat HC thermostats.