diff --git a/custom_components/heatmiserneo/const.py b/custom_components/heatmiserneo/const.py index 862210f..dced246 100644 --- a/custom_components/heatmiserneo/const.py +++ b/custom_components/heatmiserneo/const.py @@ -27,6 +27,16 @@ SERVICE_HOLD_ON = "hold_on" SERVICE_HOLD_OFF = "hold_off" SERVICE_TIMER_HOLD_ON = "timer_hold_on" +SERVICE_GET_DEVICE_PROFILE_DEFINITION = "get_device_profile_definition" +SERVICE_GET_PROFILE_DEFINITIONS = "get_profile_definitions" +SERVICE_CREATE_PROFILE_ONE = "create_profile_one" +SERVICE_CREATE_PROFILE_TWO = "create_profile_two" +SERVICE_CREATE_PROFILE_SEVEN = "create_profile_seven" +SERVICE_CREATE_TIMER_PROFILE_ONE = "create_timer_profile_one" +SERVICE_CREATE_TIMER_PROFILE_TWO = "create_timer_profile_two" +SERVICE_CREATE_TIMER_PROFILE_SEVEN = "create_timer_profile_seven" +SERVICE_RENAME_PROFILE = "rename_profile" +SERVICE_DELETE_PROFILE = "delete_profile" SERVICE_HUB_AWAY = "set_away_mode" ATTR_HOLD_DURATION = "hold_duration" ATTR_HOLD_STATE = "hold_state" @@ -34,6 +44,38 @@ ATTR_AWAY_STATE = "away" # ATTR_AWAY_START = "start" ATTR_AWAY_END = "end" +ATTR_NAME_OLD = "old_name" +ATTR_NAME_NEW = "new_name" +ATTR_FRIENDLY_MODE = "friendly_mode" +ATTR_UPDATE = "update" +ATTR_MONDAY_TIMES = "monday_times" +ATTR_MONDAY_TEMPERATURES = "monday_temperatures" +ATTR_TUESDAY_TIMES = "tuesday_times" +ATTR_TUESDAY_TEMPERATURES = "tuesday_temperatures" +ATTR_WEDNESDAY_TIMES = "wednesday_times" +ATTR_WEDNESDAY_TEMPERATURES = "wednesday_temperatures" +ATTR_THURSDAY_TIMES = "thursday_times" +ATTR_THURSDAY_TEMPERATURES = "thursday_temperatures" +ATTR_FRIDAY_TIMES = "friday_times" +ATTR_FRIDAY_TEMPERATURES = "friday_temperatures" +ATTR_SATURDAY_TIMES = "saturday_times" +ATTR_SATURDAY_TEMPERATURES = "saturday_temperatures" +ATTR_SUNDAY_TIMES = "sunday_times" +ATTR_SUNDAY_TEMPERATURES = "sunday_temperatures" +ATTR_MONDAY_ON_TIMES = "monday_on_times" +ATTR_MONDAY_OFF_TIMES = "monday_off_times" +ATTR_TUESDAY_ON_TIMES = "tuesday_on_times" +ATTR_TUESDAY_OFF_TIMES = "tuesday_off_times" +ATTR_WEDNESDAY_ON_TIMES = "wednesday_on_times" +ATTR_WEDNESDAY_OFF_TIMES = "wednesday_off_times" +ATTR_THURSDAY_ON_TIMES = "thursday_on_times" +ATTR_THURSDAY_OFF_TIMES = "thursday_off_times" +ATTR_FRIDAY_ON_TIMES = "friday_on_times" +ATTR_FRIDAY_OFF_TIMES = "friday_off_times" +ATTR_SATURDAY_ON_TIMES = "saturday_on_times" +ATTR_SATURDAY_OFF_TIMES = "saturday_off_times" +ATTR_SUNDAY_ON_TIMES = "sunday_on_times" +ATTR_SUNDAY_OFF_TIMES = "sunday_off_times" PROFILE_0 = "PROFILE_0" diff --git a/custom_components/heatmiserneo/diagnostics.py b/custom_components/heatmiserneo/diagnostics.py index e2d24a8..e069a66 100644 --- a/custom_components/heatmiserneo/diagnostics.py +++ b/custom_components/heatmiserneo/diagnostics.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from . import HeatmiserNeoConfigEntry +from .helpers import to_dict _LOGGER = logging.getLogger(__name__) @@ -62,10 +63,10 @@ async def async_get_config_entry_diagnostics( "devices": devices.result if devices else None, "device_list": device_list, "zones": zones, - "profiles": _to_dict(profiles), - "profiles_0": _to_dict(profiles_0), - "timer_profiles": _to_dict(timer_profiles), - "timer_profiles_0": _to_dict(timer_profiles_0), + "profiles": to_dict(profiles), + "profiles_0": to_dict(profiles_0), + "timer_profiles": to_dict(timer_profiles), + "timer_profiles_0": to_dict(timer_profiles_0), "raw_live_data": raw_live_data, } @@ -93,15 +94,3 @@ async def retrieve_zone_device_list(zone: str, hub: NeoHub): response = await hub._send({"GET_DEVICE_LIST": zone}) # noqa: SLF001 _LOGGER.debug("Response for GET_DEVICE_LIST on zone %s: %s", zone, response) return dict(vars(response)).get(zone, {}) - - -def _to_dict(item): - match item: - case dict(): - return {key: _to_dict(value) for key, value in item.items()} - case list() | tuple(): - return [_to_dict(x) for x in item] - case object(__dict__=_): - return {key: _to_dict(value) for key, value in vars(item).items()} - case _: - return item diff --git a/custom_components/heatmiserneo/entity.py b/custom_components/heatmiserneo/entity.py index 2045d5b..0554494 100644 --- a/custom_components/heatmiserneo/entity.py +++ b/custom_components/heatmiserneo/entity.py @@ -41,7 +41,10 @@ class HeatmiserNeoEntityDescription(EntityDescription): icon_fn: Callable[[NeoStat], str | None] | None = None # extra_attrs: list[str] | None = None custom_functions: ( - dict[str, Callable[[type[HeatmiserNeoEntity], ServiceCall], Awaitable[None]]] + dict[ + str, + Callable[[type[HeatmiserNeoEntity], ServiceCall], Awaitable[Any | None]], + ] | None ) = None @@ -57,7 +60,10 @@ class HeatmiserNeoHubEntityDescription(EntityDescription): icon_fn: Callable[[NeoStat], str | None] | None = None # extra_attrs: list[str] | None = None custom_functions: ( - dict[str, Callable[[type[HeatmiserNeoEntity], ServiceCall], Awaitable[None]]] + dict[ + str, + Callable[[type[HeatmiserNeoEntity], ServiceCall], Awaitable[Any | None]], + ] | None ) = None @@ -163,12 +169,13 @@ def entity_registry_enabled_default(self) -> bool: return self.entity_description.enabled_by_default_fn(self) return super().entity_registry_enabled_default - async def call_custom_action(self, service_call: ServiceCall) -> None: + async def call_custom_action(self, service_call: ServiceCall) -> Any | None: """Call a custom action specified in the entity description.""" - await self.entity_description.custom_functions.get(service_call.service)( - self, service_call - ) + result = await self.entity_description.custom_functions.get( + service_call.service + )(self, service_call) self.coordinator.async_update_listeners() + return result async def async_cancel_away_or_holiday(self) -> None: """Cancel away/holiday mode.""" @@ -270,17 +277,18 @@ def entity_registry_enabled_default(self) -> bool: return self.entity_description.enabled_by_default_fn(self) return super().entity_registry_enabled_default - async def call_custom_action(self, service_call: ServiceCall) -> None: + async def call_custom_action(self, service_call: ServiceCall) -> Any | None: """Call a custom action specified in the entity description.""" - await self.entity_description.custom_functions.get(service_call.service)( - self, service_call - ) + result = await self.entity_description.custom_functions.get( + service_call.service + )(self, service_call) self.coordinator.async_update_listeners() + return result async def call_custom_action( entity: HeatmiserNeoEntity, service_call: ServiceCall -) -> None: +) -> Any | None: """Call a custom action specified in the entity description.""" if ( not entity.entity_description.custom_functions @@ -291,8 +299,8 @@ async def call_custom_action( raise HomeAssistantError( f"Entity {entity.entity_id} does not support service" ) - return - await entity.call_custom_action(service_call) + return None + return await entity.call_custom_action(service_call) def _device_supports_away(dev: NeoStat) -> bool: diff --git a/custom_components/heatmiserneo/helpers.py b/custom_components/heatmiserneo/helpers.py index 1915dca..791068a 100644 --- a/custom_components/heatmiserneo/helpers.py +++ b/custom_components/heatmiserneo/helpers.py @@ -220,3 +220,123 @@ def profile_level( ## so that is the next level current_level = previous_level return current_level + + +def to_dict(item): + """Convert an arbitrary object to a dict.""" + match item: + case dict(): + return {key: to_dict(value) for key, value in item.items()} + case list() | tuple(): + return [to_dict(x) for x in item] + case object(__dict__=_): + return {key: to_dict(value) for key, value in vars(item).items()} + case _: + return item + + +def get_profile_definition( + profile_id: int, + coordinator: HeatmiserNeoCoordinator, + friendly_mode: bool = False, + device_id: int = 0, +): + """Set override with custom duration.""" + profile_format = coordinator.system_data.FORMAT + profile = None + p0 = False + timer = False + if profile_id == 0: + profile = coordinator.profiles_0.get(device_id) + p0 = True + else: + profile = coordinator.profiles.get(profile_id) + if not profile: + if profile_format == ScheduleFormat.ZERO: + profile_format = coordinator.system_data.ALT_TIMER_FORMAT + + if profile_id == 0: + profile = coordinator.timer_profiles_0.get(device_id) + else: + profile = coordinator.timer_profiles.get(profile_id) + if profile: + timer = True + if not profile: + return None + profile_dict = to_dict(profile) + + levels = None + info = None + if p0: + info = profile_dict.get("profiles")[0] + del info["device"] + else: + info = profile_dict.get("info") + + result = {"id": profile_id, "name": "PROFILE_0" if p0 else profile_dict["name"]} + + if friendly_mode: + result["format"] = profile_format + result["type"] = "timer" if timer else "heating" + + if timer: + if friendly_mode: + levels = { + wd: [ + {"time_on": e[0], "time_off": e[1]} + for e in sorted(lv.values(), key=lambda x: x[0]) + if e[0] != "24:00" + ] + for wd, lv in info.items() + } + + result["info"] = levels + else: + on_times = { + wd + "_on_times": [ + e[0] + for e in sorted(lv.values(), key=lambda x: x[0]) + if e[0] != "24:00" + ] + for wd, lv in info.items() + } + off_times = { + wd + "_off_times": [ + e[1] + for e in sorted(lv.values(), key=lambda x: x[0]) + if e[0] != "24:00" + ] + for wd, lv in info.items() + } + times = on_times | off_times + times = dict(sorted(times.items(), reverse=True)) + + result = result | times + elif friendly_mode: + levels = { + wd: [ + {"time": e[0], "temperature": e[1]} + for e in sorted(lv.values(), key=lambda x: x[0]) + if e[0] != "24:00" + ] + for wd, lv in info.items() + } + result["info"] = levels + else: + times = { + wd + "_times": [ + e[0] for e in sorted(lv.values(), key=lambda x: x[0]) if e[0] != "24:00" + ] + for wd, lv in info.items() + } + temperatures = { + wd + "_temperatures": [ + e[1] for e in sorted(lv.values(), key=lambda x: x[0]) if e[0] != "24:00" + ] + for wd, lv in info.items() + } + levels = times | temperatures + levels = dict(sorted(levels.items(), reverse=True)) + result = result | levels + + return result diff --git a/custom_components/heatmiserneo/select.py b/custom_components/heatmiserneo/select.py index 1b3b568..8312be5 100644 --- a/custom_components/heatmiserneo/select.py +++ b/custom_components/heatmiserneo/select.py @@ -15,7 +15,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,6 +23,7 @@ from . import HeatmiserNeoConfigEntry, hold_duration_validation from .const import ( + ATTR_FRIENDLY_MODE, ATTR_HOLD_DURATION, ATTR_HOLD_STATE, DEFAULT_PLUG_HOLD_DURATION, @@ -32,6 +33,7 @@ HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC, HEATMISER_TYPE_IDS_TIMER, PROFILE_0, + SERVICE_GET_DEVICE_PROFILE_DEFINITION, SERVICE_TIMER_HOLD_ON, ModeSelectOption, ) @@ -44,6 +46,7 @@ call_custom_action, profile_sensor_enabled_by_default, ) +from .helpers import get_profile_definition _LOGGER = logging.getLogger(__name__) @@ -319,6 +322,20 @@ async def async_base_set_profile( entity.data.active_profile = profile_id +async def _async_get_profile_definition( + entity: HeatmiserNeoSelectEntity, service_call: ServiceCall +): + """Set override with custom duration.""" + coordinator = entity.coordinator + data = entity.data + profile_id = data.active_profile + + friendly_mode = service_call.data.get(ATTR_FRIENDLY_MODE, False) + return get_profile_definition( + int(profile_id), coordinator, friendly_mode, data.device_id + ) + + TIMER_SET_MODE = { ModeSelectOption.AUTO: set_timer_auto, ModeSelectOption.OVERRIDE_ON: lambda entity: set_timer_override(entity, True), @@ -398,8 +415,6 @@ async def async_base_set_profile( HeatmiserNeoSelectEntityDescription( key="heatmiser_neo_active_profile", options_fn=lambda entity: _profile_names(entity.coordinator), - # entity_category=EntityCategory.CONFIG, - # entity_registry_enabled_default=False, setup_filter_fn=lambda device, _: ( device.device_type in HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC and not device.time_clock_mode @@ -411,12 +426,13 @@ async def async_base_set_profile( name="Active Profile", # translation_key="preheat_time", enabled_by_default_fn=profile_sensor_enabled_by_default, + custom_functions={ + SERVICE_GET_DEVICE_PROFILE_DEFINITION: _async_get_profile_definition + }, ), HeatmiserNeoSelectEntityDescription( key="heatmiser_neo_active_timer_profile", options_fn=lambda entity: _timer_profile_names(entity.coordinator), - # entity_category=EntityCategory.CONFIG, - # entity_registry_enabled_default=False, setup_filter_fn=lambda device, _: ( device.device_type in HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC and device.time_clock_mode @@ -428,6 +444,9 @@ async def async_base_set_profile( name="Active Profile", # translation_key="preheat_time", enabled_by_default_fn=profile_sensor_enabled_by_default, + custom_functions={ + SERVICE_GET_DEVICE_PROFILE_DEFINITION: _async_get_profile_definition + }, ), ) @@ -531,6 +550,14 @@ async def async_setup_entry( }, call_custom_action, ) + platform.async_register_entity_service( + SERVICE_GET_DEVICE_PROFILE_DEFINITION, + { + vol.Optional(ATTR_FRIENDLY_MODE, default=False): cv.boolean, + }, + call_custom_action, + supports_response=SupportsResponse.ONLY, + ) class HeatmiserNeoSelectEntity(HeatmiserNeoEntity, SelectEntity): diff --git a/custom_components/heatmiserneo/sensor.py b/custom_components/heatmiserneo/sensor.py index dc3b804..b88537a 100644 --- a/custom_components/heatmiserneo/sensor.py +++ b/custom_components/heatmiserneo/sensor.py @@ -5,10 +5,12 @@ from collections.abc import Callable from dataclasses import dataclass import datetime +import json import logging from typing import Any -from neohubapi.neohub import NeoHub, NeoStat +from neohubapi.neohub import NeoHub, NeoStat, ScheduleFormat +import voluptuous as vol from homeassistant.components.climate import ( FAN_AUTO, @@ -23,18 +25,63 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import EntityCategory, UnitOfTime -from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_NAME, EntityCategory, UnitOfTime +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util from . import HeatmiserNeoConfigEntry from .const import ( + ATTR_FRIDAY_OFF_TIMES, + ATTR_FRIDAY_ON_TIMES, + ATTR_FRIDAY_TEMPERATURES, + ATTR_FRIDAY_TIMES, + ATTR_FRIENDLY_MODE, + ATTR_MONDAY_OFF_TIMES, + ATTR_MONDAY_ON_TIMES, + ATTR_MONDAY_TEMPERATURES, + ATTR_MONDAY_TIMES, + ATTR_NAME_NEW, + ATTR_NAME_OLD, + ATTR_SATURDAY_OFF_TIMES, + ATTR_SATURDAY_ON_TIMES, + ATTR_SATURDAY_TEMPERATURES, + ATTR_SATURDAY_TIMES, + ATTR_SUNDAY_OFF_TIMES, + ATTR_SUNDAY_ON_TIMES, + ATTR_SUNDAY_TEMPERATURES, + ATTR_SUNDAY_TIMES, + ATTR_THURSDAY_OFF_TIMES, + ATTR_THURSDAY_ON_TIMES, + ATTR_THURSDAY_TEMPERATURES, + ATTR_THURSDAY_TIMES, + ATTR_TUESDAY_OFF_TIMES, + ATTR_TUESDAY_ON_TIMES, + ATTR_TUESDAY_TEMPERATURES, + ATTR_TUESDAY_TIMES, + ATTR_UPDATE, + ATTR_WEDNESDAY_OFF_TIMES, + ATTR_WEDNESDAY_ON_TIMES, + ATTR_WEDNESDAY_TEMPERATURES, + ATTR_WEDNESDAY_TIMES, HEATMISER_FAN_SPEED_HA_FAN_MODE, HEATMISER_TEMPERATURE_UNIT_HA_UNIT, HEATMISER_TYPE_IDS_HC, HEATMISER_TYPE_IDS_HOLD, HEATMISER_TYPE_IDS_THERMOSTAT, HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC, + SERVICE_CREATE_PROFILE_ONE, + SERVICE_CREATE_PROFILE_SEVEN, + SERVICE_CREATE_PROFILE_TWO, + SERVICE_CREATE_TIMER_PROFILE_ONE, + SERVICE_CREATE_TIMER_PROFILE_SEVEN, + SERVICE_CREATE_TIMER_PROFILE_TWO, + SERVICE_DELETE_PROFILE, + SERVICE_GET_PROFILE_DEFINITIONS, + SERVICE_RENAME_PROFILE, ) from .coordinator import HeatmiserNeoCoordinator from .entity import ( @@ -42,14 +89,55 @@ HeatmiserNeoEntityDescription, HeatmiserNeoHubEntity, HeatmiserNeoHubEntityDescription, + call_custom_action, profile_sensor_enabled_by_default, ) -from .helpers import profile_level +from .helpers import get_profile_definition, profile_level _LOGGER = logging.getLogger(__name__) HOLIDAY_FORMAT = "%a %b %d %H:%M:%S %Y\n" +HEATING_LEVELS_4 = {0: "wake", 1: "leave", 2: "return", 3: "sleep"} + +HEATING_LEVELS_6 = { + 0: "wake", + 1: "level1", + 2: "level2", + 3: "level3", + 4: "level4", + 5: "sleep", +} + +TIMER_LEVELS_4 = {0: "time1", 1: "time2", 2: "time3", 3: "time4"} + +SCHEDULE_WEEKDAYS = { + ScheduleFormat.ONE: ["sunday"], + ScheduleFormat.TWO: ["sunday", "monday"], + ScheduleFormat.SEVEN: [ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + ], +} + + +def time_str(value: Any) -> str: + """Input validator for time string in profile services.""" + try: + time_val = dt_util.parse_time(value) + except TypeError as err: + raise vol.Invalid("Not a parseable type") from err + + if time_val is None: + raise vol.Invalid(f"Invalid time specified: {value}") + + return time_val.strftime("%H:%M") + async def async_setup_entry( hass: HomeAssistant, @@ -82,6 +170,329 @@ async def async_setup_entry( if description.setup_filter_fn(neodevice, system_data) ) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RENAME_PROFILE, + { + vol.Required(ATTR_NAME_OLD): cv.string, + vol.Required(ATTR_NAME_NEW): cv.string, + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_DELETE_PROFILE, + {vol.Required(ATTR_NAME): cv.string}, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_PROFILE_ONE, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_SUNDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_PROFILE_TWO, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_MONDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_MONDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_SUNDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_PROFILE_SEVEN, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_MONDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_MONDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_TUESDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_TUESDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_WEDNESDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_WEDNESDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_THURSDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_THURSDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_FRIDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_FRIDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_SATURDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SATURDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + vol.Required(ATTR_SUNDAY_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_TEMPERATURES): vol.All( + cv.ensure_list, [vol.Coerce(float)] + ), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_TIMER_PROFILE_ONE, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_SUNDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_TIMER_PROFILE_TWO, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_MONDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_MONDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_CREATE_TIMER_PROFILE_SEVEN, + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_UPDATE, default=False): cv.boolean, + vol.Required(ATTR_MONDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_MONDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_TUESDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_TUESDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_WEDNESDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_WEDNESDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_THURSDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_THURSDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_FRIDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_FRIDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SATURDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SATURDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_ON_TIMES): vol.All(cv.ensure_list, [time_str]), + vol.Required(ATTR_SUNDAY_OFF_TIMES): vol.All(cv.ensure_list, [time_str]), + }, + call_custom_action, + ) + platform.async_register_entity_service( + SERVICE_GET_PROFILE_DEFINITIONS, + { + vol.Optional(ATTR_FRIENDLY_MODE, default=False): cv.boolean, + }, + call_custom_action, + supports_response=SupportsResponse.ONLY, + ) + + +async def async_rename_profile( + entity: HeatmiserNeoHubEntity, service_call: ServiceCall +): + """Rename a profile.""" + coordinator = entity.coordinator + old_name = service_call.data[ATTR_NAME_OLD] + new_name = service_call.data[ATTR_NAME_NEW] + profile_id, timer = _check_profile_name(old_name, coordinator) + conflicting_profile_id, _ = _check_profile_name(new_name, coordinator) + if not profile_id: + raise HomeAssistantError(f"Old name '{old_name}' does not exist") + if conflicting_profile_id: + raise HomeAssistantError(f"New name '{new_name}' already in use") + + message = {"PROFILE_TITLE": [old_name, new_name]} + reply = {"result": "profile renamed"} + await entity.coordinator.hub._send(message, reply) # noqa: SLF001 + if timer: + entity.coordinator.timer_profiles[profile_id].name = new_name + else: + entity.coordinator.profiles[profile_id].name = new_name + + +async def async_delete_profile( + entity: HeatmiserNeoHubEntity, service_call: ServiceCall +): + """Rename a profile.""" + coordinator = entity.coordinator + profile_name = service_call.data[ATTR_NAME] + profile_id, timer = _check_profile_name(profile_name, coordinator) + if not profile_id: + raise HomeAssistantError(f"Profile '{profile_name}' does not exist") + + message = {"CLEAR_PROFILE": profile_name} + reply = {"result": "profile removed"} + await entity.coordinator.hub._send(message, reply) # noqa: SLF001 + if timer: + del entity.coordinator.timer_profiles[profile_id] + else: + del entity.coordinator.profiles[profile_id] + + +async def async_get_profile_definitions( + entity: HeatmiserNeoHubEntity, service_call: ServiceCall +): + """Get definitions of all profiles.""" + friendly_mode = service_call.data.get(ATTR_FRIENDLY_MODE, False) + + heating = { + p.name: get_profile_definition(k, entity.coordinator, friendly_mode) + for k, p in entity.coordinator.profiles.items() + } + timers = { + p.name: get_profile_definition(k, entity.coordinator, friendly_mode) + for k, p in entity.coordinator.timer_profiles.items() + } + + return {"heating_profiles": heating, "timer_profiles": timers} + + +async def async_create_profile( + entity: HeatmiserNeoEntity, + service_call: ServiceCall, + requested_format: ScheduleFormat, + timer: bool = False, +): + """Create or update a profile.""" + _LOGGER.debug("Create profile - service_call=%s", service_call) + coordinator = entity.coordinator + profile_format = coordinator.system_data.FORMAT + if timer and profile_format is ScheduleFormat.ZERO: + profile_format = coordinator.system_data.ALT_TIMER_FORMAT + + if profile_format is ScheduleFormat.ZERO: + raise HomeAssistantError( + "Hub is in non programmable mode. Can't create profiles" + ) + + if requested_format is not profile_format: + raise HomeAssistantError( + f"Requested profile format ({requested_format}) does not match hub format ({profile_format})" + ) + + is_update = service_call.data.get(ATTR_UPDATE, False) + profile_name = service_call.data[ATTR_NAME] + profile_id, timer_profile = _check_profile_name(profile_name, coordinator) + + if not profile_id: + if is_update: + raise HomeAssistantError( + f"Could not find existing profile with name '{profile_name}'" + ) + elif not is_update: + raise HomeAssistantError(f"A profile with name '{profile_name}' already exists") + elif timer != timer_profile: + raise HomeAssistantError( + f"A {"heating" if timer else "timer"} profile with name '{profile_name}' already exists" + ) + + heating_levels = 4 if timer else coordinator.system_data.HEATING_LEVELS + + weekdays = SCHEDULE_WEEKDAYS[profile_format] + weekday_levels = { + wd: _convert_to_profile_info(service_call, wd, heating_levels, timer) + for wd in weekdays + } + + msg_details = {} + if is_update: + msg_details["ID"] = profile_id + msg_details["info"] = weekday_levels + msg_details["name"] = profile_name + + msg = {"STORE_PROFILE": msg_details} + reply = {"result": "profile created"} + + _LOGGER.debug("Create profile - msg=%s", json.dumps(msg)) + await entity.coordinator.hub._send(msg, reply) # noqa: SLF001 + + await coordinator.async_request_refresh() + + +def _convert_to_profile_info( + service_call: ServiceCall, weekday: str, levels: int = 4, timer: bool = False +) -> dict: + list1 = None + list2 = None + empty1 = "24:00" + empty2 = None + if timer: + on_key = weekday + "_on_times" + off_key = weekday + "_off_times" + list1 = service_call.data.get(on_key, []) + list2 = service_call.data.get(off_key, []) + empty2 = empty1 + + if len(list1) != len(list2): + raise HomeAssistantError( + f"On Times and Off Times lists for {weekday} must have same length" + ) + else: + times_key = weekday + "_times" + temperatures_key = weekday + "_temperatures" + + list1 = service_call.data.get(times_key, []) + list2 = service_call.data.get(temperatures_key, []) + empty2 = 5 + + if len(list1) != len(list2): + raise HomeAssistantError( + f"Times and Temperatures lists for {weekday} must have same length" + ) + + if len(list1) > levels: + raise HomeAssistantError( + f"Too many levels defined for {weekday}. Hub only supports {levels} levels" + ) + + return { + _convert_level_index(timer, levels, i): [list1[i], list2[i]] + if i < len(list1) + else [empty1, empty2] + for i in range(levels) + } + + +def _convert_level_index(timer: bool, configured_levels: int, level_idx: int) -> str: + if timer: + return TIMER_LEVELS_4[level_idx] + if configured_levels == 4: + return HEATING_LEVELS_4[level_idx] + return HEATING_LEVELS_6[level_idx] + + +def _check_profile_name(profile_name: str, coordinator: HeatmiserNeoCoordinator): + ids = [ + k + for k, p in coordinator.timer_profiles.items() + if p.name.casefold() == profile_name.casefold() + ] + if len(ids) == 1: + return ids[0], True + + ids = [ + k + for k, p in coordinator.profiles.items() + if p.name.casefold() == profile_name.casefold() + ] + + return ids[0] if len(ids) == 1 else None, False + @dataclass(frozen=True, kw_only=True) class HeatmiserNeoSensorEntityDescription( @@ -276,6 +687,55 @@ class HeatmiserNeoHubSensorEntityDescription( name="Away End", value_fn=lambda coordinator: _holiday_end(coordinator), ), + HeatmiserNeoHubSensorEntityDescription( + key="heatmiser_neohub_profile_format", + device_class=SensorDeviceClass.ENUM, + options=[e._name_.lower() for e in ScheduleFormat], + value_fn=lambda coordinator: coordinator.system_data.FORMAT._name_.lower() + if coordinator.system_data.FORMAT + else None, + translation_key="hub_profile_format", + custom_functions={ + SERVICE_RENAME_PROFILE: async_rename_profile, + SERVICE_DELETE_PROFILE: async_delete_profile, + SERVICE_GET_PROFILE_DEFINITIONS: async_get_profile_definitions, + SERVICE_CREATE_PROFILE_ONE: lambda e, s: async_create_profile( + e, s, ScheduleFormat.ONE + ), + SERVICE_CREATE_PROFILE_TWO: lambda e, s: async_create_profile( + e, s, ScheduleFormat.TWO + ), + SERVICE_CREATE_PROFILE_SEVEN: lambda e, s: async_create_profile( + e, s, ScheduleFormat.SEVEN + ), + SERVICE_CREATE_TIMER_PROFILE_ONE: lambda e, s: async_create_profile( + e, s, ScheduleFormat.ONE, True + ), + SERVICE_CREATE_TIMER_PROFILE_TWO: lambda e, s: async_create_profile( + e, s, ScheduleFormat.TWO, True + ), + SERVICE_CREATE_TIMER_PROFILE_SEVEN: lambda e, s: async_create_profile( + e, s, ScheduleFormat.SEVEN, True + ), + }, + ), + HeatmiserNeoHubSensorEntityDescription( + key="heatmiser_neohub_alt_timer_profile_format", + device_class=SensorDeviceClass.ENUM, + options=[e._name_.lower() for e in ScheduleFormat if e != ScheduleFormat.ZERO], + value_fn=lambda coordinator: coordinator.system_data.ALT_TIMER_FORMAT._name_.lower() + if coordinator.system_data.ALT_TIMER_FORMAT + and coordinator.system_data.FORMAT == ScheduleFormat.ZERO + else None, + translation_key="hub_profile_alt_timer_format", + ), + HeatmiserNeoHubSensorEntityDescription( + key="heatmiser_neohub_heating_levels", + device_class=SensorDeviceClass.ENUM, + options=[4, 6], + value_fn=lambda coordinator: coordinator.system_data.HEATING_LEVELS, + translation_key="hub_profile_heating_levels", + ), ) diff --git a/custom_components/heatmiserneo/services.yaml b/custom_components/heatmiserneo/services.yaml index eabb694..9ef33ae 100644 --- a/custom_components/heatmiserneo/services.yaml +++ b/custom_components/heatmiserneo/services.yaml @@ -74,3 +74,566 @@ set_away_mode: example: "2024-01-01 00:00:00" selector: datetime: +get_device_profile_definition: + name: Get Device Profile Definition + description: Gets the current profile definition from a device + target: + device: + integration: heatmiserneo + fields: + friendly_mode: + name: Friendly Mode + description: Enable to return the profile in a format easier to consume by humans + default: false + example: true + selector: + boolean: +get_profile_definitions: + name: Get Profile Definitions + description: Gets all the profiles from the hub + target: + device: + integration: heatmiserneo + fields: + friendly_mode: + name: Friendly Mode + description: Enable to return the profile in a format easier to consume by humans + default: false + example: true + selector: + boolean: +rename_profile: + name: Rename Profile + description: Updates the name of an existing profile + target: + device: + integration: heatmiserneo + fields: + old_name: + name: Old Name + description: Old name must match an existing profile + required: true + example: Old Profile + selector: + text: + new_name: + name: New Name + description: New name for the profile + required: true + example: New Profile + selector: + text: +delete_profile: + name: Delete Profile + description: Delete a profile + target: + device: + integration: heatmiserneo + fields: + name: + name: Name + description: The name of the profile to delee + required: true + example: Profile Name + selector: + text: +create_profile_one: + name: Create Profile - 24hr + description: Creates or update a profile which is the same every day + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + sunday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + sunday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true +create_profile_two: + name: Create Profile - 5 Day / 2 Day + description: Creates or update a profile for weekdays and weekends + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + monday_levels: + collapsed: true + fields: + monday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + monday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true + sunday_levels: + collapsed: true + fields: + sunday_times: + name: Times + description: Profile level time. Seconds are disregarded + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + sunday_temperatures: + name: Temperatures + description: Profile level temperature + required: true + example: "[19.5, 16]" + selector: + text: + multiple: true +create_profile_seven: + name: Create Profile - 7 Day + description: Creates or update a profile for each day of the week + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + monday_levels: + collapsed: true + fields: + monday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + monday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true + tuesday_levels: + collapsed: true + fields: + tuesday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + tuesday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true + wednesday_levels: + collapsed: true + fields: + wednesday_times: + name: Times + description: Profile level time. Seconds are disregarded + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + wednesday_temperatures: + name: Temperatures + description: Profile level temperature + required: true + example: "[19.5, 16]" + selector: + text: + multiple: true + thursday_levels: + collapsed: true + fields: + thursday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + thursday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true + friday_levels: + collapsed: true + fields: + friday_times: + name: Times + description: Profile level time. Seconds are disregarded + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + friday_temperatures: + name: Temperatures + description: Profile level temperature + required: true + example: "[19.5, 16]" + selector: + text: + multiple: true + saturday_levels: + collapsed: true + fields: + saturday_times: + name: Times + description: Profile level time in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + saturday_temperatures: + name: Temperatures + description: Profile level temperature in 0.5 degree steps + required: true + example: "[19.5, 17, 20.5, 16]" + selector: + text: + multiple: true + sunday_levels: + collapsed: true + fields: + sunday_times: + name: Times + description: Profile level time. Seconds are disregarded + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + sunday_temperatures: + name: Temperatures + description: Profile level temperature + required: true + example: "[19.5, 16]" + selector: + text: + multiple: true +create_timer_profile_one: + name: Create Timer Profile - 24hr + description: Creates or update a timer profile which is the same every day + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + sunday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + sunday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true +create_timer_profile_two: + name: Create Timer Profile - 5 Day / 2 Day + description: Creates or update a timer profile for weekdays and weekends + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + monday_levels: + collapsed: true + fields: + monday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + monday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true + sunday_levels: + collapsed: true + fields: + sunday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + sunday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 01:00]" + selector: + text: + multiple: true +create_timer_profile_seven: + name: Create Timer Profile - 7 Day + description: Creates or update a timer profile for each day of the week + target: + device: + integration: heatmiserneo + fields: + name: + name: Profile Name + description: Profile Name. If update is false, it must not be in use already + required: true + example: New Profile + selector: + text: + update: + name: Update + description: Set to true to update an existing profile + required: false + example: false + selector: + boolean: + monday_levels: + collapsed: true + fields: + monday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + monday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true + tuesday_levels: + collapsed: true + fields: + tuesday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + tuesday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true + wednesday_levels: + collapsed: true + fields: + wednesday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + wednesday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 01:00]" + selector: + text: + multiple: true + thursday_levels: + collapsed: true + fields: + thursday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + thursday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true + friday_levels: + collapsed: true + fields: + friday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + friday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 01:00]" + selector: + text: + multiple: true + saturday_levels: + collapsed: true + fields: + saturday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 09:00, 17:00, 22:00]" + selector: + text: + multiple: true + saturday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 10:30, 19:00, 01:00]" + selector: + text: + multiple: true + sunday_levels: + collapsed: true + fields: + sunday_on_times: + name: Switch On Times + description: Times to switch on in HH:MM format + required: true + example: "[06:45, 22:00]" + selector: + text: + multiple: true + sunday_off_times: + name: Switch Off Times + description: Times to switch off in HH:MM format + required: true + example: "[07:45, 01:00]" + selector: + text: + multiple: true diff --git a/custom_components/heatmiserneo/strings.json b/custom_components/heatmiserneo/strings.json index e81c567..df4c162 100644 --- a/custom_components/heatmiserneo/strings.json +++ b/custom_components/heatmiserneo/strings.json @@ -115,6 +115,108 @@ "tz1400": "UTC+14:00" } } + }, + "sensor": { + "hub_profile_format": { + "name": "Profile Format", + "state": { + "zero": "Non Programmable", + "one": "24 HR", + "two": "5/2 Days", + "seven": "7 Day" + } + }, + "hub_profile_alt_timer_format": { + "name": "Profile Alt Timer Format", + "state": { + "one": "24 HR", + "two": "5/2 Days", + "seven": "7 Day" + } + }, + "hub_profile_heating_levels": { + "name": "Profile Heating Levels" + } + } + }, + "services": { + "create_profile_two": { + "name": "Create Profile - 5 Day / 2 Day", + "description": "Creates or update a profile for weekdays and weekends", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_profile_seven": { + "name": "Create Profile - 7 Day", + "description": "Creates or update a profile for each day of the week", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "tuesday_levels": { + "name": "Tuesday Levels" + }, + "wednesday_levels": { + "name": "Wednesday Levels" + }, + "thursday_levels": { + "name": "Thursday Levels" + }, + "friday_levels": { + "name": "Friday Levels" + }, + "saturday_levels": { + "name": "Saturday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_timer_profile_two": { + "name": "Create Timer Profile - 5 Day / 2 Day", + "description": "Creates or update a timer profile for weekdays and weekends", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_timer_profile_seven": { + "name": "Create Timer Profile - 7 Day", + "description": "Creates or update a timer profile for each day of the week", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "tuesday_levels": { + "name": "Tuesday Levels" + }, + "wednesday_levels": { + "name": "Wednesday Levels" + }, + "thursday_levels": { + "name": "Thursday Levels" + }, + "friday_levels": { + "name": "Friday Levels" + }, + "saturday_levels": { + "name": "Saturday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } } } } diff --git a/custom_components/heatmiserneo/translations/en.json b/custom_components/heatmiserneo/translations/en.json index 215be81..b8b8656 100644 --- a/custom_components/heatmiserneo/translations/en.json +++ b/custom_components/heatmiserneo/translations/en.json @@ -119,6 +119,108 @@ "tz1400": "UTC+14:00" } } + }, + "sensor": { + "hub_profile_format": { + "name": "Profile Format", + "state": { + "zero": "Non Programmable", + "one": "24 HR", + "two": "5/2 Days", + "seven": "7 Day" + } + }, + "hub_profile_alt_timer_format": { + "name": "Profile Alt Timer Format", + "state": { + "one": "24 HR", + "two": "5/2 Days", + "seven": "7 Day" + } + }, + "hub_profile_heating_levels": { + "name": "Profile Heating Levels" + } + } + }, + "services": { + "create_profile_two": { + "name": "Create Profile - 5 Day / 2 Day", + "description": "Creates or update a profile for weekdays and weekends", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_profile_seven": { + "name": "Create Profile - 7 Day", + "description": "Creates or update a profile for each day of the week", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "tuesday_levels": { + "name": "Tuesday Levels" + }, + "wednesday_levels": { + "name": "Wednesday Levels" + }, + "thursday_levels": { + "name": "Thursday Levels" + }, + "friday_levels": { + "name": "Friday Levels" + }, + "saturday_levels": { + "name": "Saturday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_timer_profile_two": { + "name": "Create Timer Profile - 5 Day / 2 Day", + "description": "Creates or update a timer profile for weekdays and weekends", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } + }, + "create_timer_profile_seven": { + "name": "Create Timer Profile - 7 Day", + "description": "Creates or update a timer profile for each day of the week", + "sections": { + "monday_levels": { + "name": "Monday Levels" + }, + "tuesday_levels": { + "name": "Tuesday Levels" + }, + "wednesday_levels": { + "name": "Wednesday Levels" + }, + "thursday_levels": { + "name": "Thursday Levels" + }, + "friday_levels": { + "name": "Friday Levels" + }, + "saturday_levels": { + "name": "Saturday Levels" + }, + "sunday_levels": { + "name": "Sunday Levels" + } + } } } } diff --git a/docs/hub.md b/docs/hub.md index 872d1e5..8777427 100644 --- a/docs/hub.md +++ b/docs/hub.md @@ -15,6 +15,13 @@ The NeoHub device contains system wide entities and configuration parameters - Away - whether the hub is away - Away End - if the hub is away and this date is set, then the hub will automatically turn off away mode at this time +- Profile Format - The profile format in use + - Non Programmable - Heating profiles are not used + - 24HR mode - the same profile levels every day + - 5/2 Day mode - Different levels for weekdays and weekends + - 7 Day mode - Different levels every day +- Profile Alt Timer Format - Only populated if the main Profile Format is Non Programmable. This format would be used by timer devices +- Profile Heating Levels - Specifies the number of levels on heating profiles. It can be 4 or 6. Timer profiles are unaffected by this, they always have 4 levels ## Diagnostic Entities diff --git a/docs/services.md b/docs/services.md index 1555505..deeb372 100644 --- a/docs/services.md +++ b/docs/services.md @@ -92,3 +92,151 @@ data: target: entity_id: binary_sensor.neohub_192_168_1_10_away ``` + +## Profile Services + +### Rename Profile + +Change the name of an existing profile using the `heatmiserneo.rename_profile` action. You should target the NeoHub device itself or the Profile Format entity of the hub. + +``` +action: heatmiserneo.rename_profile +data: + old_name: Old Profile + new_name: New Profile +target: + entity_id: sensor.neohub_192_168_1_10_profile_format +``` + +### Delete Profile + +Delete an existing profile using the `heatmiserneo.delete_profile` action. You should target the NeoHub device itself or the Profile Format entity of the hub. Note, if any devices are using the profile, they will be moved to PROFILE_0 (eg profile managed on the device itself). + +``` +action: heatmiserneo.delete_profile +data: + name: Profile Name +target: + entity_id: sensor.neohub_192_168_1_10_profile_format +``` + +### Create/Update Profile + +This action allows creating or updating a heating profile. There are three versions of it, depending on the profile format being used in the hub: + +- For 24HR format (same levels every day) use `heatmiserneo.create_profile_one` +- For 5/2 Day format (different levels for weekdays vs weekends) use `heatmiserneo.create_profile_two` +- For 7 Day format (different levels every day) use `heatmiserneo.create_profile_seven` + +These have the following parameters in common: + +- Name - The name of the profile to create or update +- Update - If set to `false` (the default) the service will be in create mode and the supplied name must not exist already. If set to `true`, the supplied name must exist and it must be for a heating profile +- Times and Temperatures - Supply the times and temperatures for a particular weekday + - Times must be supplied in `HH:MM` 24h format + - Temperatures can be in 0.5 degree increments + - For 24HR mode, supply `sunday_times` and `sunday_temperatures` + - For 5/2 Day mode, additionally supply `monday_times` and `monday_temperatures` + - For 7 Day mode, supply times and temperatures for every day of the week + - The maximum number of levels allowed is dependent on the hub configuration. It will be either 4 or 6. The sensor `sensor.neohub_192_168_1_10_profile_heating_levels` has the current configuration. You can supply less levels but not more + +You should target the NeoHub device itself or the Profile Format entity of the hub. + +``` +action: heatmiserneo.create_profile_two +data: + name: Existing Profile + update: true + monday_times: + - "06:45" + - "09:00" + - "17:00" + - "22:00" + monday_temperatures: + - 19.5 + - 17 + - 20.5 + - 16 + sunday_times: + - "06:45" + - "22:00" + sunday_temperatures: + - 19.5 + - 16 +target: + entity_id: sensor.neohub_192_168_1_10_profile_format +``` + +### Create/Update Timer Profile + +This action allows creating or updating a timer profile. There are three versions of it, depending on the profile format being used in the hub: + +- For 24HR format (same levels every day) use `heatmiserneo.create_timer_profile_one` +- For 5/2 Day format (different levels for weekdays vs weekends) use `heatmiserneo.create_timer_profile_two` +- For 7 Day format (different levels every day) use `heatmiserneo.create_timer_profile_seven` + +These have the following parameters in common: + +- Name - The name of the profile to create or update +- Update - If set to `false` (the default) the service will be in create mode and the supplied name must not exist already. If set to `true`, the supplied name must exist and it must be for a heating profile +- On Times and Off Times - Supply the times to turn on and off + - Times must be supplied in `HH:MM` 24h format + - For 24HR mode, supply `sunday_on_times` and `sunday_off_times` + - For 5/2 Day mode, additionally supply `monday_on_times` and `monday_off_times`. Monday times will be used for weekdays and sunday times for weekends + - For 7 Day mode, supply times and temperatures for every day of the week + - Unlike heating profiles, the maximum number of timer levels is always 4. You can supply less levels but not more + +You should target the NeoHub device itself or the Profile Format entity of the hub. + +``` +action: heatmiserneo.create_timer_profile_two +data: + name: Existing Profile + update: true + monday_on_times: + - "06:45" + - "09:00" + - "17:00" + - "22:00" + monday_off_times: + - "07:45" + - "10:30" + - "19:00" + - "01:00" + sunday_on_times: + - "06:45" + - "22:00" + sunday_off_times: + - "07:45" + - "01:00" +target: + entity_id: sensor.neohub_192_168_1_10_profile_format +``` + +### Get Profile Definitions + +Use this action to retrieve all profiles defined in the hub. It has one optional parameter: + +- Friendly Mode - By default (or when set to false), the returned format closely matches the format of the create/update service calls, so it can be used to copy the format, make the necessary changes and then upload it using the relevant service. When set to true, the result is a bit easier to read. + +You should target the NeoHub device itself or the Profile Format entity of the hub. + +``` +action: heatmiserneo.get_profile_definitions +data: + friendly_mode: false +target: + entity_id: sensor.neohub_192_168_1_10_profile_format +``` + +### Get Device Profile Definition + +This is very similar to the hub level service, but instead you can get the definition of the profile that a particular device is using. Target the device itself or the Active Profile entity of the device. + +``` +action: heatmiserneo.get_device_profile_definition +data: + friendly_mode: true +target: + entity_id: select.landing_active_profile +```