Skip to content

Commit

Permalink
Merge pull request #251 from ocrease/Issue78
Browse files Browse the repository at this point in the history
Configurable HVAC Modes for NeoStat HC
  • Loading branch information
ocrease authored Jan 30, 2025
2 parents 32ac2d3 + 1ec9b19 commit 4fbc1b7
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 92 deletions.
9 changes: 9 additions & 0 deletions custom_components/heatmiserneo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
82 changes: 44 additions & 38 deletions custom_components/heatmiserneo/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,6 +47,8 @@
HEATMISER_TYPE_IDS_THERMOSTAT,
SERVICE_HOLD_OFF,
SERVICE_HOLD_ON,
AvailableMode,
GlobalSystemType,
)
from .entity import HeatmiserNeoEntity, HeatmiserNeoEntityDescription

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
110 changes: 60 additions & 50 deletions custom_components/heatmiserneo/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -126,75 +133,78 @@ 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
) -> dict[str, str]:
"""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.
return self.async_create_entry(
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,
}
)

Expand Down
9 changes: 9 additions & 0 deletions custom_components/heatmiserneo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
5 changes: 3 additions & 2 deletions custom_components/heatmiserneo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
SERVICE_DELETE_PROFILE,
SERVICE_GET_PROFILE_DEFINITIONS,
SERVICE_RENAME_PROFILE,
GlobalSystemType,
)
from .coordinator import HeatmiserNeoCoordinator
from .entity import (
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions custom_components/heatmiserneo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
}
},
"options": {
"abort": {
"no_devices_supported": "No devices have configurable HVAC modes"
},
"step": {
"init": {
"title": "Configure HVAC Modes",
Expand All @@ -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": {}
Expand Down
8 changes: 8 additions & 0 deletions custom_components/heatmiserneo/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
}
},
"options": {
"abort": {
"no_devices_supported": "No devices have configurable HVAC modes"
},
"step": {
"init": {
"title": "Configure HVAC Modes",
Expand All @@ -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": {}
Expand Down
Loading

0 comments on commit 4fbc1b7

Please sign in to comment.