Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable HVAC Modes for NeoStat HC #251

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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