Skip to content

Commit

Permalink
Merge pull request #227 from ocrease/AwayService
Browse files Browse the repository at this point in the history
Away/Holiday service
  • Loading branch information
MindrustUK authored Dec 19, 2024
2 parents cb7b123 + 791ab37 commit 16b8a2c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 13 deletions.
102 changes: 100 additions & 2 deletions custom_components/heatmiserneo/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,131 @@

from collections.abc import Callable
from dataclasses import dataclass
import datetime
from functools import partial
import logging
from typing import Any

from neohubapi.neohub import NeoHub, NeoStat
import voluptuous as vol

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import HeatmiserNeoConfigEntry
from .const import (
ATTR_AWAY_END,
ATTR_AWAY_STATE,
HEATMISER_TYPE_IDS_AWAY,
HEATMISER_TYPE_IDS_HOLD,
HEATMISER_TYPE_IDS_STANDBY,
HEATMISER_TYPE_IDS_THERMOSTAT,
HEATMISER_TYPE_IDS_THERMOSTAT_NOT_HC,
HEATMISER_TYPE_IDS_TIMER,
SERVICE_HUB_AWAY,
)
from .coordinator import HeatmiserNeoCoordinator
from .entity import (
HeatmiserNeoEntity,
HeatmiserNeoEntityDescription,
HeatmiserNeoHubEntity,
HeatmiserNeoHubEntityDescription,
_device_supports_away,
call_custom_action,
profile_sensor_enabled_by_default,
)
from .helpers import profile_level
from .helpers import profile_level, set_away, set_holiday

_LOGGER = logging.getLogger(__name__)


def _dates_only_provided_when_setting_away(
state_key, end_key
) -> Callable[[dict[str, Any]], dict[str, Any]]:
"""Verify that all values are of the same type."""

def validate(obj: dict[str, Any]) -> dict[str, Any]:
"""Test that all keys in the dict have values of the same type."""
state_val = obj[state_key]
# start_val = obj.get(start_key)
end_val = obj.get(end_key)
if not state_val:
# if start_val:
# raise vol.Invalid(
# "Start date should only be specified if setting away."
# )
if end_val:
raise vol.Invalid("End date should only be specified if setting away.")
return obj

return validate


SET_AWAY_MODE_SCHEMA = vol.Schema(
vol.All(
cv.make_entity_service_schema(
{
vol.Required(ATTR_AWAY_STATE, default=False): cv.boolean,
# vol.Optional(ATTR_AWAY_START): cv.datetime,
vol.Optional(ATTR_AWAY_END): cv.datetime,
}
),
_dates_only_provided_when_setting_away(ATTR_AWAY_STATE, ATTR_AWAY_END),
)
)


async def async_set_away_mode(entity: HeatmiserNeoEntity, service_call: ServiceCall):
"""Set away mode on the hub."""
state = service_call.data[ATTR_AWAY_STATE]
holiday = None
away = None
if not state:
if entity.coordinator.live_data.HUB_AWAY:
await entity.coordinator.hub.set_away(False)
away = False
if entity.coordinator.live_data.HUB_HOLIDAY:
await entity.coordinator.hub.cancel_holiday()
holiday = False
else:
end_date = service_call.data.get(ATTR_AWAY_END)
if end_date:
if entity.coordinator.live_data.HUB_AWAY:
await entity.coordinator.hub.set_away(False)
away = False

await entity.coordinator.hub.set_holiday(
datetime.datetime.now() - datetime.timedelta(days=1), end_date
)
holiday = True
else:
if entity.coordinator.live_data.HUB_HOLIDAY:
await entity.coordinator.hub.cancel_holiday()
holiday = False
await entity.coordinator.hub.set_away(True)
away = True
if away is not None:
entity.coordinator.update_in_memory_state(
partial(set_away, away),
_device_supports_away,
)
entity.coordinator.live_data.HUB_AWAY = away
if holiday is not None:
entity.coordinator.update_in_memory_state(
partial(set_holiday, holiday),
_device_supports_away,
)
entity.coordinator.live_data.HUB_HOLIDAY = holiday


async def async_setup_entry(
hass: HomeAssistant,
entry: HeatmiserNeoConfigEntry,
Expand Down Expand Up @@ -70,6 +160,13 @@ async def async_setup_entry(
if description.setup_filter_fn(coordinator)
)

platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_HUB_AWAY,
SET_AWAY_MODE_SCHEMA,
call_custom_action,
)


@dataclass(frozen=True, kw_only=True)
class HeatmiserNeoBinarySensorEntityDescription(
Expand Down Expand Up @@ -204,6 +301,7 @@ class HeatmiserNeoHubBinarySensorEntityDescription(
key="heatmiser_neohub_away",
name="Away",
value_fn=lambda coordinator: coordinator.live_data.HUB_AWAY,
custom_functions={SERVICE_HUB_AWAY: async_set_away_mode},
),
HeatmiserNeoHubBinarySensorEntityDescription(
key="heatmiser_neohub_holiday",
Expand Down
4 changes: 4 additions & 0 deletions custom_components/heatmiserneo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
SERVICE_HOLD_ON = "hold_on"
SERVICE_HOLD_OFF = "hold_off"
SERVICE_TIMER_HOLD_ON = "timer_hold_on"
SERVICE_HUB_AWAY = "set_away_mode"
ATTR_HOLD_DURATION = "hold_duration"
ATTR_HOLD_STATE = "hold_state"
ATTR_HOLD_TEMPERATURE = "hold_temperature"
ATTR_AWAY_STATE = "away"
# ATTR_AWAY_START = "start"
ATTR_AWAY_END = "end"

PROFILE_0 = "PROFILE_0"

Expand Down
14 changes: 11 additions & 3 deletions custom_components/heatmiserneo/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
HEATMISER_TYPE_IDS_AWAY,
)
from .coordinator import HeatmiserNeoCoordinator
from .helpers import cancel_holiday, set_away
from .helpers import set_away, set_holiday

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -168,6 +168,7 @@ async def call_custom_action(self, service_call: ServiceCall) -> None:
await self.entity_description.custom_functions.get(service_call.service)(
self, service_call
)
self.coordinator.async_update_listeners()

async def async_cancel_away_or_holiday(self) -> None:
"""Cancel away/holiday mode."""
Expand All @@ -180,9 +181,9 @@ async def async_cancel_away_or_holiday(self) -> None:
_device_supports_away,
)
if dev.holiday:
await self._hub.cancel_holiday(False)
await self._hub.cancel_holiday()
self.coordinator.update_in_memory_state(
cancel_holiday,
partial(set_holiday, False),
_device_supports_away,
)

Expand Down Expand Up @@ -269,6 +270,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:
"""Call a custom action specified in the entity description."""
await self.entity_description.custom_functions.get(service_call.service)(
self, service_call
)
self.coordinator.async_update_listeners()


async def call_custom_action(
entity: HeatmiserNeoEntity, service_call: ServiceCall
Expand Down
4 changes: 2 additions & 2 deletions custom_components/heatmiserneo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ def set_away(state: bool, dev: NeoStat) -> None:
dev.target_temperature = dev._data_.FROST_TEMP


def cancel_holiday(dev: NeoStat) -> None:
def set_holiday(state: bool, dev: NeoStat) -> None:
"""Cancel holiday on device."""
dev.holiday = False
dev.holiday = state


def _profile_current_day_key(
Expand Down
2 changes: 0 additions & 2 deletions custom_components/heatmiserneo/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,6 @@ async def async_timer_hold(entity: HeatmiserNeoSelectEntity, service_call: Servi
hold_minutes = int(duration.total_seconds() / 60)
hold_minutes = min(hold_minutes, 60 * 99)
await set_timer_override(entity, state, hold_minutes)
entity.coordinator.async_update_listeners()


async def async_plug_hold(entity: HeatmiserNeoSelectEntity, service_call: ServiceCall):
Expand All @@ -271,7 +270,6 @@ async def async_plug_hold(entity: HeatmiserNeoSelectEntity, service_call: Servic
hold_minutes = int(duration.total_seconds() / 60)
hold_minutes = min(hold_minutes, 60 * 99)
await set_plug_override(entity, state, hold_minutes)
entity.coordinator.async_update_listeners()


async def async_set_switching_differential(
Expand Down
8 changes: 4 additions & 4 deletions custom_components/heatmiserneo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

_LOGGER = logging.getLogger(__name__)

HOLIDAY_FORMAT = "%a %b %d %H:%M:%S %Y\n"


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -384,13 +386,11 @@ def _holiday_end(coordinator: HeatmiserNeoCoordinator) -> datetime.datetime | No
holiday = coordinator.live_data.HUB_HOLIDAY
holiday_end = coordinator.live_data.HOLIDAY_END

if not holiday:
if not holiday or holiday_end == 0:
return None

try:
parsed_datetime = datetime.datetime.strptime(
holiday_end, "%a %b %d %H:%M:%S %Y\n"
)
parsed_datetime = datetime.datetime.strptime(holiday_end, HOLIDAY_FORMAT)
return parsed_datetime.replace(
tzinfo=datetime.timezone(
datetime.timedelta(minutes=coordinator.system_data.TIME_ZONE * 60)
Expand Down
22 changes: 22 additions & 0 deletions custom_components/heatmiserneo/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,25 @@ timer_hold_on:
default: true
selector:
boolean:
set_away_mode:
name: Set Away Mode
description: Set Away/Holiday Mode on the Heatmiser NeoHub
target:
entity:
integration: heatmiserneo
domain: binary_sensor
fields:
away:
name: Away State
description: Whether to set away on or off
default: false
example: true
selector:
boolean:
end:
name: Holiday End
description: Optional end date for holiday mode
required: false
example: "2024-01-01 00:00:00"
selector:
datetime:

0 comments on commit 16b8a2c

Please sign in to comment.