diff --git a/.devcontainer.json b/.devcontainer.json index 71f1d3f..49fc2f5 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -40,7 +40,6 @@ } } }, - "runArgs": ["--network=host"], "remoteUser": "vscode", "features": { "ghcr.io/devcontainers/features/rust:1": {} diff --git a/custom_components/somneo/__init__.py b/custom_components/somneo/__init__.py index e038a79..fcd2e81 100644 --- a/custom_components/somneo/__init__.py +++ b/custom_components/somneo/__init__.py @@ -1,9 +1,10 @@ """ Support for Philips Somneo devices.""" from __future__ import annotations -from datetime import timedelta -import logging import asyncio +from datetime import timedelta, time +import functools as ft +import logging from pysomneo import Somneo, SOURCES @@ -13,7 +14,7 @@ from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, WEEKEND, WORKDAYS, TOMORROW, EVERYDAY, CONF_SESSION +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -35,9 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Setup the Somneo component.""" host = entry.data[CONF_HOST] - coordinator = SomneoCoordinator( - hass, host, use_session=entry.options.get(CONF_SESSION, True) - ) + coordinator = SomneoCoordinator(hass, host) entry.async_on_unload(entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() @@ -84,6 +83,13 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): config_entry.version = 3 hass.config_entries.async_update_entry(config_entry, data=new) + if config_entry.version == 3: + new = {**config_entry.data} + new['options'].pop('use_session') + + config_entry.version = 4 + hass.config_entries.async_update_entry(config_entry, data=new) + _LOGGER.info("Migration to version %s successful", config_entry.version) return True @@ -95,11 +101,10 @@ class SomneoCoordinator(DataUpdateCoordinator[None]): def __init__( self, hass: HomeAssistant, - host: str, - use_session: bool, - ) -> None: + host: str + ) -> None: """Initialize Somneo client.""" - self.somneo = Somneo(host, use_session=use_session) + self.somneo = Somneo(host) self.state_lock = asyncio.Lock() super().__init__( @@ -120,36 +125,26 @@ async def _async_update(self): return await self.hass.async_add_executor_job(self.somneo.fetch_data) - async def async_turn_on_light(self, brightness) -> None: - """Turn the main light on.""" + async def async_toggle_light(self, state: bool, brightness: int | None = None) -> None: + """Toggle the main light.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.somneo.toggle_light, True, brightness + ft.partial( + self.somneo.toggle_light, + state, + brightness = brightness + ) ) await self.async_request_refresh() - async def async_turn_off_light(self) -> None: - """Turn the maing light off.""" + async def async_toggle_nightlight(self, state: bool) -> None: + """Toggle the night light.""" async with self.state_lock: - await self.hass.async_add_executor_job(self.somneo.toggle_light, False) - await self.async_request_refresh() - - async def async_turn_on_nightlight(self) -> None: - """Turn the night light on.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.somneo.toggle_night_light, True) - await self.async_request_refresh() - - async def async_turn_off_nightlight(self) -> None: - """Turn the night light off.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.somneo.toggle_night_light, False - ) + await self.hass.async_add_executor_job(self.somneo.toggle_night_light, state) await self.async_request_refresh() async def async_toggle_alarm(self, alarm: str, state: bool) -> None: - """Toggle alarm.""" + """Toggle an alarm.""" async with self.state_lock: await self.hass.async_add_executor_job( self.somneo.toggle_alarm, alarm, state @@ -162,44 +157,53 @@ async def async_dismiss_alarm(self) -> None: await self.hass.async_add_executor_job(self.somneo.dismiss_alarm) await self.async_request_refresh() - async def async_snooze_alarm(self) -> None: - """Snooze alarm.""" + async def async_set_alarm( + self, alarm: str, time: time | None = None, days: str | list | None = None + ): + """Set alarm time.""" async with self.state_lock: - await self.hass.async_add_executor_job(self.somneo.snooze_alarm) + await self.hass.async_add_executor_job( + ft.partial( + self.somneo.set_alarm, + alarm, + time = time, + days = days + ) + ) await self.async_request_refresh() - def set_powerwake(self, alarm: str, state: bool, delta: int): - """Toggle powerwake (default 10 minutes).""" - self.somneo.set_powerwake(alarm, onoff=state, delta=delta) - - async def async_toggle_powerwake(self, alarm: str, state: bool): + async def async_toggle_alarm_powerwake(self, alarm: str, state: bool): """Toggle powerwake (default 10 minutes).""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.set_powerwake, alarm, state, 10) - await self.async_request_refresh() - - def set_alarm(self, alarm: str, hour: int | None, minute: int | None) -> None: - """Set alarm time.""" - self.somneo.set_alarm(alarm, hour=hour, minute=minute) - - async def async_set_alarm( - self, alarm: str, hours: int | None = None, minutes: int | None = None - ): - """Set alarm time.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.set_alarm, alarm, hours, minutes + ft.partial( + self.somneo.set_alarm_powerwake, + alarm, + onoff = state, + delta = 10 + ) ) await self.async_request_refresh() - async def async_set_powerwake(self, alarm: str, delta: int = 0): + async def async_set_alarm_powerwake(self, alarm: str, delta: int = 0): """Set powerwake time.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.set_powerwake, alarm, bool(delta), delta + ft.partial( + self.somneo.set_alarm_powerwake, + alarm, + onoff = bool(delta), + delta = delta + ) ) await self.async_request_refresh() + async def async_snooze_alarm(self) -> None: + """Snooze alarm.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.somneo.snooze_alarm) + await self.async_request_refresh() + async def async_set_snooze_time(self, time): """Set snooze time.""" async with self.state_lock: @@ -208,71 +212,37 @@ async def async_set_snooze_time(self, time): ) await self.async_request_refresh() - async def async_set_alarm_day(self, alarm, day): - """Set the day of the alarm.""" - async with self.state_lock: - if type(day) == list: - await self.hass.async_add_executor_job( - self.somneo.set_alarm_days, - alarm, - day - ) - _LOGGER.debug("Optie is werkday") - else: - if day == WORKDAYS: - await self.hass.async_add_executor_job( - self.somneo.set_alarm_workdays, - alarm - ) - _LOGGER.debug("Optie is werkday") - elif day == WEEKEND: - await self.hass.async_add_executor_job( - self.somneo.set_alarm_weekend, - alarm - ) - _LOGGER.debug("Optie is weekend") - elif day == TOMORROW: - await self.hass.async_add_executor_job( - self.somneo.set_alarm_tomorrow, - alarm - ) - _LOGGER.debug("Optie is morgen") - elif day == EVERYDAY: - await self.hass.async_add_executor_job( - self.somneo.set_alarm_everyday, - alarm - ) - _LOGGER.debug("Optie is elke dag") - - await self.async_request_refresh() - - def set_light_alarm( - self, alarm: str, curve: str, level: int, duration: int - ) -> None: - """Adjust the light settings of an alarm.""" - self.somneo.set_light_alarm(alarm, curve=curve, level=level, duration=duration) - - async def async_set_light_alarm( + async def async_set_alarm_light( self, alarm: str, curve: str = "sunny day", level: int = 20, duration: int = 30 ): """Adjust the light settings of an alarm.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.set_light_alarm, alarm, curve, level, duration + ft.partial( + self.somneo.set_alarm_light, + alarm, + curve = curve, + level = level, + duration = duration + ) ) + await self.async_request_refresh() - def set_sound_alarm(self, alarm: str, source: str, level: int, channel: str): - """Adjust the sound settings of an alarm.""" - self.somneo.set_sound_alarm(alarm, source=source, level=level, channel=channel) - - async def async_set_sound_alarm( + async def async_set_alarm_sound( self, alarm: str, source="wake-up", level=12, channel="forest birds" ): """Adjust the sound settings of an alarm.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.set_sound_alarm, alarm, source, level, channel + ft.partial( + self.somneo.set_alarm_sound, + alarm, + source = source, + level = level, + channel = channel + ) ) + await self.async_request_refresh() async def async_remove_alarm(self, alarm: str): """Function to remove alarm from list in Somneo app.""" @@ -284,30 +254,52 @@ async def async_add_alarm(self, alarm: str): async with self.state_lock: await self.hass.async_add_executor_job(self.somneo.add_alarm, alarm) - async def async_player_turn_on(self): - """Turn on the audio player.""" + async def async_player_toggle(self, state: bool): + """Toggle the audio player.""" async with self.state_lock: - await self.hass.async_add_executor_job(self.somneo.toggle_player, True) + await self.hass.async_add_executor_job(self.somneo.toggle_player, state) await self.async_request_refresh() - async def async_player_turn_off(self): - """Turn off the audio player.""" + async def async_set_player_volume(self, volume: float): + """Set the volume of the audio player.""" async with self.state_lock: - await self.hass.async_add_executor_job(self.somneo.toggle_player, False) + await self.hass.async_add_executor_job( + self.somneo.set_player_volume, volume + ) await self.async_request_refresh() - async def async_set_volume_player(self, volume: float): + async def async_set_player_source(self, source: str): """Set the volume of the audio player.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.somneo.set_volume_player, volume + self.somneo.set_player_source, SOURCES[source] ) await self.async_request_refresh() - async def async_set_source_player(self, source: str): - """Set the volume of the audio player.""" + async def async_toggle_sunset(self, state: bool) -> None: + """Toggle the main light.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.somneo.toggle_sunset, state) + await self.async_request_refresh() + + async def async_set_sunset( + self, + curve: str | None = None, + level: int | None = None, + duration: int | None = None, + sound: str | None = None, + volume: int | None = None + ): + """Adjust the sunset settings.""" async with self.state_lock: await self.hass.async_add_executor_job( - self.somneo.set_source_player, SOURCES[source] + ft.partial( + self.somneo.set_sunset, + curve = curve, + level = level, + duration = duration, + sound = sound, + volume = volume + ) ) await self.async_request_refresh() diff --git a/custom_components/somneo/config_flow.py b/custom_components/somneo/config_flow.py index e639eac..f0df878 100644 --- a/custom_components/somneo/config_flow.py +++ b/custom_components/somneo/config_flow.py @@ -68,12 +68,6 @@ async def get_device_info(self): return dev_info - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> SomneoOptionsFlow: - """Thermosmart options callback.""" - return SomneoOptionsFlow(config_entry) - async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: """Prepare configuration for a discovered Somneo.""" _LOGGER.debug("SSDP discovery: %s", discovery_info) @@ -121,34 +115,5 @@ async def async_step_user( ) -class SomneoOptionsFlow(config_entries.OptionsFlow): - """Config flow options for Somneo""" - - def __init__(self, entry: ConfigEntry) -> None: - """Initialze the Somneo options flow.""" - self.entry = entry - self.use_session = self.entry.options.get(CONF_SESSION, True) - - async def async_step_init(self, _user_input=None): - """Manage the options.""" - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Process user input.""" - if user_input is not None: - return self.async_create_entry(title="Somneo", data=user_input) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Optional(CONF_SESSION, default=self.use_session): bool, - } - ), - ) - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/somneo/const.py b/custom_components/somneo/const.py index 8c362ee..4d976b3 100644 --- a/custom_components/somneo/const.py +++ b/custom_components/somneo/const.py @@ -35,6 +35,7 @@ SNOOZE_ICON: Final = "hass:alarm-snooze" WORKDAYS_ICON: Final = "hass:calendar-range" WEEKEND_ICON: Final = "hass:calendar-range" +SUNSET_ICON: Final = "hass:weather-sunset" ATTR_ALARM: Final = "alarm" ATTR_CURVE: Final = "curve" diff --git a/custom_components/somneo/light.py b/custom_components/somneo/light.py index 2fa9259..fd4c0ed 100644 --- a/custom_components/somneo/light.py +++ b/custom_components/somneo/light.py @@ -55,11 +55,11 @@ def _handle_coordinator_update(self) -> None: async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" - await self.coordinator.async_turn_on_light(kwargs.get(ATTR_BRIGHTNESS)) + await self.coordinator.async_toggle_light(True, brightness = kwargs.get(ATTR_BRIGHTNESS)) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - await self.coordinator.async_turn_off_light() + await self.coordinator.async_toggle_light(False) class SomneoNightLight(SomneoEntity, LightEntity): @@ -75,8 +75,8 @@ def _handle_coordinator_update(self) -> None: async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" - await self.coordinator.async_turn_on_nightlight() + await self.coordinator.async_toggle_nightlight(True) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - await self.coordinator.async_turn_off_nightlight() + await self.coordinator.async_toggle_nightlight(False) diff --git a/custom_components/somneo/manifest.json b/custom_components/somneo/manifest.json index 003b9a2..cee8a67 100644 --- a/custom_components/somneo/manifest.json +++ b/custom_components/somneo/manifest.json @@ -15,13 +15,13 @@ "somneo" ], "requirements": [ - "pysomneo==3.1.3" + "git+https://github.com/theneweinstein/pysomneo.git@rewrite#pysomneo>4.0.0" ], "ssdp": [ { "nt": "urn:philips-com:device:DiProduct:1", - "modelName": "Wake-up Light" + "server": "*Wake-up Light*" } ], - "version": "5.1.0" + "version": "6.0.0" } \ No newline at end of file diff --git a/custom_components/somneo/media_player.py b/custom_components/somneo/media_player.py index 8153f36..9ee3055 100644 --- a/custom_components/somneo/media_player.py +++ b/custom_components/somneo/media_player.py @@ -66,14 +66,14 @@ def _handle_coordinator_update(self) -> None: async def async_turn_on(self) -> None: """Instruct the light to turn on.""" - await self.coordinator.async_player_turn_on() + await self.coordinator.async_player_toggle(True) async def async_turn_off(self) -> None: """Instruct the light to turn off.""" - await self.coordinator.async_player_turn_off() + await self.coordinator.async_player_toggle(False) async def async_set_volume_level(self, volume: float) -> None: - await self.coordinator.async_set_volume_player(volume) + await self.coordinator.async_set_player_volume(volume) async def async_select_source(self, source: str) -> None: - await self.coordinator.async_set_source_player(source) + await self.coordinator.async_set_player_source(source) diff --git a/custom_components/somneo/number.py b/custom_components/somneo/number.py index af79133..e373908 100644 --- a/custom_components/somneo/number.py +++ b/custom_components/somneo/number.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, PW_ICON, SNOOZE_ICON +from .const import DOMAIN, PW_ICON, SNOOZE_ICON, SUNSET_ICON from .entity import SomneoEntity _LOGGER = logging.getLogger(__name__) @@ -33,8 +33,14 @@ async def async_setup_entry( snooze = [SomneoSnooze(coordinator, unique_id, name, device_info, "snooze")] + sunset = [SomneoSunsetDuration(coordinator, unique_id, name, device_info, "sunset_duration"), + SomneoSunsetLevel(coordinator, unique_id, name, device_info, "sunset_level"), + SomneoSunsetVolume(coordinator, unique_id, name, device_info, "sunset_volume") + ] + async_add_entities(alarms, update_before_add=True) async_add_entities(snooze, update_before_add=True) + async_add_entities(sunset, update_before_add=True) class SomneoPowerWake(SomneoEntity, NumberEntity): @@ -52,22 +58,22 @@ class SomneoPowerWake(SomneoEntity, NumberEntity): def __init__(self, coordinator, unique_id, name, dev_info, alarm): """Initialize number entities.""" super().__init__( - coordinator, unique_id, name, dev_info, alarm + "_powerwake_delta" + coordinator, unique_id, name, dev_info, "alarm" + str(alarm) + "_powerwake_delta" ) - self._attr_translation_key = alarm + "_powerwake_delta" + self._attr_translation_key = "alarm" + str(alarm) + "_powerwake_delta" self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: """Handle update""" - self._attr_native_value = self.coordinator.data["powerwake_delta"][self._alarm] + self._attr_native_value = self.coordinator.data["alarms"][self._alarm]["powerwake_delta"] self.async_write_ha_state() async def async_set_native_value(self, value: float) -> None: """Called when user adjust Hours / Minutes in the UI""" - await self.coordinator.async_set_powerwake(self._alarm, delta=int(value)) + await self.coordinator.async_set_alarm_powerwake(self._alarm, delta=int(value)) class SomneoSnooze(SomneoEntity, NumberEntity): @@ -91,3 +97,69 @@ def _handle_coordinator_update(self) -> None: async def async_set_native_value(self, value: float) -> None: """Called when user adjust snooze time in the UI""" await self.coordinator.async_set_snooze_time(int(value)) + +class SomneoSunsetDuration(SomneoEntity, NumberEntity): + """Represenation of the Sunset duration.""" + + _attr_should_poll = True + _attr_available = True + _attr_assumed_state = False + _attr_translation_key = "sunset_duration" + _attr_native_min_value = 5 + _attr_native_max_value = 60 + _attr_native_step = 5 + _attr_icon = SUNSET_ICON + _attr_has_entity_name = True + + @callback + def _handle_coordinator_update(self) -> None: + self._attr_native_value = self.coordinator.data["sunset"]["duration"] + self.async_write_ha_state() + + async def async_set_native_value(self, value: float) -> None: + """Called when user adjust snooze time in the UI""" + await self.coordinator.async_set_sunset(duration = int(value)) + +class SomneoSunsetLevel(SomneoEntity, NumberEntity): + """Represenation of the Sunset level.""" + + _attr_should_poll = True + _attr_available = True + _attr_assumed_state = False + _attr_translation_key = "sunset_level" + _attr_native_min_value = 0 + _attr_native_max_value = 25 + _attr_native_step = 1 + _attr_icon = SUNSET_ICON + _attr_has_entity_name = True + + @callback + def _handle_coordinator_update(self) -> None: + self._attr_native_value = self.coordinator.data["sunset"]["level"] + self.async_write_ha_state() + + async def async_set_native_value(self, value: float) -> None: + """Called when user adjust snooze time in the UI""" + await self.coordinator.async_set_sunset(level = int(value)) + +class SomneoSunsetVolume(SomneoEntity, NumberEntity): + """Represenation of the Sunset volume.""" + + _attr_should_poll = True + _attr_available = True + _attr_assumed_state = False + _attr_translation_key = "sunset_volume" + _attr_native_min_value = 1 + _attr_native_max_value = 25 + _attr_native_step = 1 + _attr_icon = SUNSET_ICON + _attr_has_entity_name = True + + @callback + def _handle_coordinator_update(self) -> None: + self._attr_native_value = self.coordinator.data["sunset"]["volume"] + self.async_write_ha_state() + + async def async_set_native_value(self, value: float) -> None: + """Called when user adjust snooze time in the UI""" + await self.coordinator.async_set_sunset(volume = int(value)) \ No newline at end of file diff --git a/custom_components/somneo/select.py b/custom_components/somneo/select.py index 9ce8078..028d1c2 100644 --- a/custom_components/somneo/select.py +++ b/custom_components/somneo/select.py @@ -1,13 +1,15 @@ """Select entities for Somneo.""" import logging +from pysomneo import SOUND_DUSK, LIGHT_CURVES + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.select import SelectEntity -from .const import DOMAIN, WORKDAYS, WEEKEND, TOMORROW, EVERYDAY, CUSTOM, WORKDAYS_ICON +from .const import DOMAIN, WORKDAYS, WEEKEND, TOMORROW, EVERYDAY, CUSTOM, WORKDAYS_ICON, SUNSET_ICON from .entity import SomneoEntity @@ -32,7 +34,12 @@ async def async_setup_entry( for alarm in list(coordinator.data["alarms"]): alarms.append(SomneoDays(coordinator, unique_id, name, device_info, alarm)) + sunset = [SomneoSunsetSound(coordinator, unique_id, name, device_info, "sunset_sound"), + SomneoSunsetCurve(coordinator, unique_id, name, device_info, "sunset_curve") + ] + async_add_entities(alarms, update_before_add=True) + async_add_entities(sunset, update_before_add=True) class SomneoDays(SomneoEntity, SelectEntity): @@ -47,16 +54,59 @@ class SomneoDays(SomneoEntity, SelectEntity): def __init__(self, coordinator, unique_id, name, dev_info, alarm): """Initialize number entities.""" - super().__init__(coordinator, unique_id, name, dev_info, alarm) + super().__init__(coordinator, unique_id, name, dev_info, "alarm" + str(alarm)) - self._attr_translation_key = alarm + "_days" + self._attr_translation_key = "alarm" + str(alarm) + "_days" self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: - self._attr_current_option = self.coordinator.data["alarms_day"][self._alarm] + _LOGGER.debug(self.coordinator.data["alarms"][self._alarm]["days_type"]) + self._attr_current_option = self.coordinator.data["alarms"][self._alarm]["days_type"] + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Called when user adjust the option in the UI.""" + await self.coordinator.async_set_alarm(self._alarm, days = option) + +class SomneoSunsetSound(SomneoEntity, SelectEntity): + """Representation of a sunset sound source.""" + + _attr_should_poll = True + _attr_icon = SUNSET_ICON + _attr_translation_key = "sunset_sound" + _attr_assumed_state = False + _attr_available = True + _attr_options = list(SOUND_DUSK.keys()) + _attr_current_option = 'soft rain' + + @callback + def _handle_coordinator_update(self) -> None: + _LOGGER.debug(self.coordinator.data["sunset"]["sound"]) + self._attr_current_option = self.coordinator.data["sunset"]["sound"] + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Called when user adjust the option in the UI.""" + await self.coordinator.async_set_sunset(sound = option) + +class SomneoSunsetCurve(SomneoEntity, SelectEntity): + """Representation of a sunset curve.""" + + _attr_should_poll = True + _attr_icon = SUNSET_ICON + _attr_translation_key = "sunset_curve" + _attr_assumed_state = False + _attr_available = True + _attr_options = list(LIGHT_CURVES.keys()) + _attr_current_option = 'sunny day' + + @callback + def _handle_coordinator_update(self) -> None: + _LOGGER.debug(self.coordinator.data["sunset"]["curve"]) + self._attr_current_option = self.coordinator.data["sunset"]["curve"].lower() self.async_write_ha_state() async def async_select_option(self, option: str) -> None: """Called when user adjust the option in the UI.""" - await self.coordinator.async_set_alarm_day(self._alarm, option) + await self.coordinator.async_set_sunset(curve = option) diff --git a/custom_components/somneo/sensor.py b/custom_components/somneo/sensor.py index 85ebb40..39c0c96 100644 --- a/custom_components/somneo/sensor.py +++ b/custom_components/somneo/sensor.py @@ -102,7 +102,7 @@ class SomneoAlarmStatus(SomneoEntity, SensorEntity): @callback def _handle_coordinator_update(self) -> None: - self._attr_native_value = self.coordinator.data["alarm_status"] + self._attr_native_value = self.coordinator.data["somneo_status"] self.async_write_ha_state() @property diff --git a/custom_components/somneo/services.yaml b/custom_components/somneo/services.yaml index 4b1c749..b2eddca 100644 --- a/custom_components/somneo/services.yaml +++ b/custom_components/somneo/services.yaml @@ -1,5 +1,5 @@ # Service ID -set_light_alarm: +set_alarm_light: # Service name as shown in UI name: Set wake-up light # Description of the service @@ -60,7 +60,7 @@ set_light_alarm: max: 40 step: 1 -set_sound_alarm: +set_alarm_sound: name: Set wake-up sound description: Set the sound of the alarm target: diff --git a/custom_components/somneo/switch.py b/custom_components/somneo/switch.py index 5c0676d..1a0b8c4 100644 --- a/custom_components/somneo/switch.py +++ b/custom_components/somneo/switch.py @@ -19,6 +19,7 @@ ATTR_SOURCE, ALARMS_ICON, PW_ICON, + SUNSET_ICON, ) from .entity import SomneoEntity @@ -49,29 +50,32 @@ async def async_setup_entry( SomneoPowerWakeToggle(coordinator, unique_id, name, device_info, alarm) ) + sunset = [SomneoSunsetToggle(coordinator, unique_id, name, device_info, "sunset")] + async_add_entities(alarms, update_before_add=True) async_add_entities(pwrwk, update_before_add=True) + async_add_entities(sunset, update_before_add=True) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - "set_light_alarm", + "set_alarm_light", { vol.Optional(ATTR_CURVE): cv.string, vol.Optional(ATTR_LEVEL): cv.positive_int, vol.Optional(ATTR_DURATION): cv.positive_int, }, - "set_light_alarm", + "set_alarm_light", ) platform.async_register_entity_service( - "set_sound_alarm", + "set_alarm_sound", { vol.Optional(ATTR_SOURCE): cv.string, vol.Optional(ATTR_LEVEL): cv.positive_int, vol.Optional(ATTR_CHANNEL): cv.string, }, - "set_sound_alarm", + "set_alarm_sound", ) platform.async_register_entity_service("remove_alarm", {}, "remove_alarm") @@ -87,21 +91,20 @@ class SomneoAlarmToggle(SomneoEntity, SwitchEntity): def __init__(self, coordinator, unique_id, name, device_info, alarm): """Initialize the switches.""" - super().__init__(coordinator, unique_id, name, device_info, alarm) + super().__init__(coordinator, unique_id, name, device_info, "alarm" + str(alarm)) - self._attr_translation_key = alarm + self._attr_translation_key = "alarm" + str(alarm) self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: - self._attr_is_on = self.coordinator.data["alarms"][self._alarm] + self._attr_is_on = self.coordinator.data["alarms"][self._alarm]['enabled'] self._attr_extra_state_attributes = { - "hour": self.coordinator.data["alarms_hour"][self._alarm], - "minute": self.coordinator.data["alarms_minute"][self._alarm], - "day": self.coordinator.data["alarms_day"][self._alarm], - "powerwake": self.coordinator.data["powerwake"][self._alarm], - "powerwake_delta": self.coordinator.data["powerwake_delta"][self._alarm], + "time": self.coordinator.data["alarms"][self._alarm]['time'], + "days": self.coordinator.data["alarms"][self._alarm]['days'], + "powerwake": self.coordinator.data["alarms"][self._alarm]["powerwake"], + "powerwake_delta": self.coordinator.data["alarms"][self._alarm]["powerwake_delta"], } self.async_write_ha_state() @@ -114,19 +117,19 @@ async def async_turn_off(self, **kwargs: Any): await self.coordinator.async_toggle_alarm(self._alarm, False) # Define service-calls - async def set_light_alarm( + async def set_alarm_light( self, curve: str = "sunny day", level: int = 20, duration: int = 30 ): """Adjust the light settings of an alarm.""" - await self.coordinator.async_set_light_alarm( + await self.coordinator.async_set_alarm_light( self._alarm, curve=curve, level=level, duration=duration ) - async def set_sound_alarm( + async def set_alarm_sound( self, source: str = "wake-up", level: int = 12, channel: str = "forest birds" ): """Adjust the sound settings of an alarm.""" - await self.coordinator.async_set_sound_alarm( + await self.coordinator.async_set_alarm_sound( self._alarm, source=source, level=level, channel=channel ) @@ -147,23 +150,50 @@ class SomneoPowerWakeToggle(SomneoEntity, SwitchEntity): def __init__(self, coordinator, unique_id, name, device_info, alarm): """Initialize the switches.""" - super().__init__(coordinator, unique_id, name, device_info, alarm + "_PW") + super().__init__(coordinator, unique_id, name, device_info, "alarm" + str(alarm) + "_PW") - self._attr_translation_key = alarm + "_powerwake" + self._attr_translation_key = "alarm" + str(alarm) + "_powerwake" self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: - self._attr_is_on = self.coordinator.data["powerwake"][self._alarm] + self._attr_is_on = self.coordinator.data["alarms"][self._alarm]["powerwake"] + self._attr_extra_state_attributes = { + "powerwake_delta": self.coordinator.data["alarms"][self._alarm]["powerwake_delta"] + } + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any): + """Called when user Turn On the switch from UI.""" + await self.coordinator.async_toggle_alarm_powerwake(self._alarm, True) + + async def async_turn_off(self, **kwargs: Any): + """Called when user Turn Off the switch from UI.""" + await self.coordinator.async_toggle_alarm_powerwake(self._alarm, False) + +class SomneoSunsetToggle(SomneoEntity, SwitchEntity): + """Representation of a Sunset switch.""" + + _attr_icon = SUNSET_ICON + _attr_should_poll = True + _attr_translation_key = "sunset" + + @callback + def _handle_coordinator_update(self) -> None: + self._attr_is_on = self.coordinator.data["sunset"]["is_on"] self._attr_extra_state_attributes = { - "powerwake_delta": self.coordinator.data["powerwake_delta"][self._alarm] + "duration": self.coordinator.data["sunset"]["duration"], + "curve": self.coordinator.data["sunset"]["curve"], + "level": self.coordinator.data["sunset"]["level"], + "sound": self.coordinator.data["sunset"]["sound"], + "volume": self.coordinator.data["sunset"]["volume"], } self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any): """Called when user Turn On the switch from UI.""" - await self.coordinator.async_toggle_powerwake(self._alarm, True) + await self.coordinator.async_toggle_sunset(True) async def async_turn_off(self, **kwargs: Any): """Called when user Turn Off the switch from UI.""" - await self.coordinator.async_toggle_powerwake(self._alarm, False) + await self.coordinator.async_toggle_sunset(False) diff --git a/custom_components/somneo/text.py b/custom_components/somneo/text.py index 116e85c..a50015b 100644 --- a/custom_components/somneo/text.py +++ b/custom_components/somneo/text.py @@ -54,18 +54,18 @@ class SomneoAlarmDays(SomneoEntity, TextEntity): def __init__(self, coordinator, unique_id, name, device_info, alarm): """Initialize the switches.""" - super().__init__(coordinator, unique_id, name, device_info, alarm) + super().__init__(coordinator, unique_id, name, device_info, "alarm" + str(alarm)) - self._attr_translation_key = alarm + '_days_str' + self._attr_translation_key = "alarm" + str(alarm) + '_days_str' self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: - days_list = self.coordinator.data["alarm_day_list"][self._alarm] + days_list = self.coordinator.data["alarms"][self._alarm]['days'] self._attr_native_value = ",".join([str(item) for item in days_list if item]) self.async_write_ha_state() async def async_set_value(self, value: str) -> None: """Set the text value.""" - await self.coordinator.async_set_alarm_day(self._alarm, value.split(',')) \ No newline at end of file + await self.coordinator.async_set_alarm(self._alarm, days = value.split(',')) \ No newline at end of file diff --git a/custom_components/somneo/time.py b/custom_components/somneo/time.py index 2ee38d5..e7d34a5 100644 --- a/custom_components/somneo/time.py +++ b/custom_components/somneo/time.py @@ -47,23 +47,20 @@ class SomneoTime(SomneoEntity, TimeEntity): def __init__(self, coordinator, unique_id, name, dev_info, alarm): """Initialize number entities.""" - super().__init__(coordinator, unique_id, name, dev_info, alarm + "_time") + super().__init__(coordinator, unique_id, name, dev_info, "alarm" + str(alarm) + "_time") - self._attr_translation_key = alarm + "_time" + self._attr_translation_key = "alarm" + str(alarm) + "_time" self._alarm = alarm @callback def _handle_coordinator_update(self) -> None: - self._attr_native_value = time( - self.coordinator.data["alarms_hour"][self._alarm], - self.coordinator.data["alarms_minute"][self._alarm], - ) + self._attr_native_value = self.coordinator.data["alarms"][self._alarm]['time'] self.async_write_ha_state() async def async_set_value(self, value: time) -> None: """Called when user adjust Hours / Minutes in the UI""" await self.coordinator.async_set_alarm( - self._alarm, hours=value.hour, minutes=value.minute + self._alarm, time = value ) diff --git a/custom_components/somneo/translations/en.json b/custom_components/somneo/translations/en.json index f36a345..1b9e8a6 100644 --- a/custom_components/somneo/translations/en.json +++ b/custom_components/somneo/translations/en.json @@ -229,6 +229,29 @@ "tomorrow": "Tomorrow", "custom": "Custom" } + }, + "sunset_curve": { + "name": "Sunset light curve", + "state": { + "sunny day": "Sunny day", + "island red": "Island red", + "nordic white": "Nordic white" + } + }, + "sunset_sound": { + "name": "Sunset sound", + "state": { + "soft rain": "Soft rain", + "ocean waves": "Ocean waves", + "under water": "Under water", + "summer lake": "Summer lake", + "FM 1": "FM 1", + "FM 2": "FM 2", + "FM 3": "FM 3", + "FM 4": "FM 4", + "FM 5": "FM 5", + "off": "Off" + } } }, "switch": { @@ -839,6 +862,16 @@ "name": "PowerWake delay" } } + }, + "sunset": { + "name": "Sunset", + "state_attributes": { + "duration": "Duration", + "curve": "Light curve", + "level": "Light level", + "sound": "Sound", + "volume": "Volume" + } } }, "number": { @@ -892,6 +925,15 @@ }, "snooze_time": { "name": "Snooze time" + }, + "sunset_duration": { + "name": "Sunset duration" + }, + "sunset_level": { + "name": "Sunset light level" + }, + "sunset_volume": { + "name": "Sunset volume" } }, "text": { diff --git a/custom_components/somneo/translations/nl.json b/custom_components/somneo/translations/nl.json index 9c3d2d3..08f6876 100644 --- a/custom_components/somneo/translations/nl.json +++ b/custom_components/somneo/translations/nl.json @@ -229,6 +229,29 @@ "tomorrow": "Morgen", "custom": "Aangepast" } + }, + "sunset_curve": { + "name": "Zonsondergang lichtcurve", + "state": { + "sunny day": "Zonnige dag", + "island red": "Eiland rood", + "nordic white": "Noords wit" + } + }, + "sunset_sound": { + "name": "Zonsondergang geluid", + "state": { + "soft rain": "Zachte regen", + "ocean waves": "Oceaangolven", + "under water": "Onder water", + "summer lake": "Zomers meer", + "FM 1": "FM 1", + "FM 2": "FM 2", + "FM 3": "FM 3", + "FM 4": "FM 4", + "FM 5": "FM 5", + "off": "Uit" + } } }, "switch": { @@ -839,6 +862,16 @@ "name": "PowerWake vertraging" } } + }, + "sunset": { + "name": "Zonsondergang", + "state_attributes": { + "duration": "Duur", + "curve": "Lichtcurve", + "level": "Lichtniveau", + "sound": "Geluid", + "volume": "Volume" + } } }, "number": { @@ -892,6 +925,15 @@ }, "snooze_time": { "name": "Sluimertijd" + }, + "sunset_duration": { + "name": "Zonsondergang duur" + }, + "sunset_level": { + "name": "Zonsondergang lichtniveau" + }, + "sunset_volume": { + "name": "Zonsondergang volume" } }, "text": { diff --git a/requirements.txt b/requirements.txt index f9b344c..4e21f22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.7.0 -homeassistant==2023.7.1 +homeassistant==2023.10.3 pip>=21.0,<23.2 ruff==0.0.267 diff --git a/scripts/develop b/scripts/develop index 89eda50..8f23f9e 100644 --- a/scripts/develop +++ b/scripts/develop @@ -16,5 +16,14 @@ fi ## without resulting to symlinks. export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" +#if [[ ! -d "${PWD}/pysomneo" ]]; then +# git clone --branch rewrite https://github.com/theneweinstein/pysomneo.git +#else +# cd pysomneo +# git pull +# cd .. +#fi +#pip install -e ./pysomneo --target ./config/deps + # Start Home Assistant -hass --config "${PWD}/config" --debug +hass --config "${PWD}/config" --debug #--skip-pip-packages pysomneo