From 885caa297e97201c864938c669476a94e3fd2a5f Mon Sep 17 00:00:00 2001 From: sirmrak Date: Wed, 8 Jul 2026 13:02:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?custom=5Fcomponents=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/number.py | 165 ++++++++++++++++++++++++++++++++++++ custom_components/sensor.py | 69 +++++++++++++++ custom_components/switch.py | 148 ++++++++++++++++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 custom_components/number.py create mode 100644 custom_components/sensor.py create mode 100644 custom_components/switch.py diff --git a/custom_components/number.py b/custom_components/number.py new file mode 100644 index 0000000..531dfc5 --- /dev/null +++ b/custom_components/number.py @@ -0,0 +1,165 @@ +"""Number platform for Motion Light settings.""" +import logging +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.device_registry import DeviceInfo +from .const import ( + DOMAIN, + ENTITY_MOTION_FILTER, + ENTITY_OFF_DELAY, + ENTITY_LUX_THRESHOLD, + ENTITY_LUX_COOLDOWN, + ENTITY_MANUAL_IDLE_TIMEOUT, + ENTITY_MANUAL_OFF_COOLDOWN, + DEFAULT_MOTION_FILTER, + DEFAULT_OFF_DELAY, + DEFAULT_LUX_THRESHOLD, + DEFAULT_LUX_COOLDOWN, + DEFAULT_MANUAL_IDLE_TIMEOUT, + DEFAULT_MANUAL_OFF_COOLDOWN, +) + +_LOGGER = logging.getLogger(__name__) + + +class PrefixLoggerAdapter(logging.LoggerAdapter): + """Logger adapter that adds a prefix to log messages.""" + + def process(self, msg, kwargs): + """Add prefix to the log message.""" + return f"[{self.extra.get('name', 'Unknown')}] {msg}", kwargs + + +TRANSLATION_KEYS = { + ENTITY_MOTION_FILTER: "motion_light_motion_filter", + ENTITY_OFF_DELAY: "motion_light_off_delay", + ENTITY_LUX_THRESHOLD: "motion_light_lux_threshold", + ENTITY_LUX_COOLDOWN: "motion_light_lux_cooldown", + ENTITY_MANUAL_IDLE_TIMEOUT: "motion_light_manual_idle_timeout", + ENTITY_MANUAL_OFF_COOLDOWN: "motion_light_manual_off_cooldown", +} + +ICONS = { + ENTITY_MOTION_FILTER: "mdi:timer-outline", + ENTITY_OFF_DELAY: "mdi:timer-sand", + ENTITY_LUX_THRESHOLD: "mdi:brightness-5", + ENTITY_LUX_COOLDOWN: "mdi:timer-pause", + ENTITY_MANUAL_IDLE_TIMEOUT: "mdi:timer-alert", + ENTITY_MANUAL_OFF_COOLDOWN: "mdi:timer-off", +} + +UNITS = { + ENTITY_MOTION_FILTER: UnitOfTime.SECONDS, + ENTITY_OFF_DELAY: UnitOfTime.SECONDS, + ENTITY_LUX_THRESHOLD: "lx", + ENTITY_LUX_COOLDOWN: UnitOfTime.SECONDS, + ENTITY_MANUAL_IDLE_TIMEOUT: UnitOfTime.SECONDS, + ENTITY_MANUAL_OFF_COOLDOWN: UnitOfTime.SECONDS, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Motion Light number entities.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [ + MotionLightNumber(coordinator, entry, ENTITY_MOTION_FILTER, 0, 10, 1, DEFAULT_MOTION_FILTER), + MotionLightNumber(coordinator, entry, ENTITY_OFF_DELAY, 0, 600, 5, DEFAULT_OFF_DELAY), + MotionLightNumber(coordinator, entry, ENTITY_LUX_THRESHOLD, 1, 1000, 10, DEFAULT_LUX_THRESHOLD), + MotionLightNumber(coordinator, entry, ENTITY_LUX_COOLDOWN, 0, 300, 5, DEFAULT_LUX_COOLDOWN), + MotionLightNumber(coordinator, entry, ENTITY_MANUAL_IDLE_TIMEOUT, 0, 3600, 60, DEFAULT_MANUAL_IDLE_TIMEOUT), + MotionLightNumber(coordinator, entry, ENTITY_MANUAL_OFF_COOLDOWN, 0, 300, 5, DEFAULT_MANUAL_OFF_COOLDOWN), + ] + async_add_entities(entities) + + +class MotionLightNumber(CoordinatorEntity, NumberEntity, RestoreEntity): + """Representation of a Motion Light setting as a number entity.""" + _attr_has_entity_name = True + + def __init__(self, coordinator, entry, entity_id, min_val, max_val, step, default): + """Initialize the number entity.""" + super().__init__(coordinator) + self.entry = entry + + self.logger = PrefixLoggerAdapter(_LOGGER, {"name": entry.title}) + + self._entity_suffix = entity_id + self._attr_unique_id = f"{entry.entry_id}_{entity_id}" + self._attr_translation_key = TRANSLATION_KEYS[entity_id] + self._attr_native_min_value = min_val + self._attr_native_max_value = max_val + self._attr_native_step = step + self._attr_native_unit_of_measurement = UNITS[entity_id] + self._attr_icon = ICONS[entity_id] + self._default_value = default + self._attr_native_value = default + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Light", + model="Switch Control Integration", + ) + + @property + def native_value(self) -> float: + """Return the current value.""" + return self._attr_native_value + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + self._attr_native_value = value + self.logger.info("Setting %s to %s", self._entity_suffix, value) + + if self._entity_suffix == ENTITY_MOTION_FILTER: + self.coordinator.set_motion_filter(value) + elif self._entity_suffix == ENTITY_OFF_DELAY: + self.coordinator.set_off_delay(value) + elif self._entity_suffix == ENTITY_LUX_THRESHOLD: + self.coordinator.set_lux_threshold(value) + elif self._entity_suffix == ENTITY_LUX_COOLDOWN: + self.coordinator.set_lux_cooldown(value) + elif self._entity_suffix == ENTITY_MANUAL_IDLE_TIMEOUT: + self.coordinator.set_manual_idle_timeout(value) + elif self._entity_suffix == ENTITY_MANUAL_OFF_COOLDOWN: + self.coordinator.set_manual_off_cooldown(value) + + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is not None: + try: + value = float(last_state.state) + self._attr_native_value = value + self.logger.info("Restored %s: %s", self._entity_suffix, value) + + if self._entity_suffix == ENTITY_MOTION_FILTER: + self.coordinator.set_motion_filter(value) + elif self._entity_suffix == ENTITY_OFF_DELAY: + self.coordinator.set_off_delay(value) + elif self._entity_suffix == ENTITY_LUX_THRESHOLD: + self.coordinator.set_lux_threshold(value) + elif self._entity_suffix == ENTITY_LUX_COOLDOWN: + self.coordinator.set_lux_cooldown(value) + elif self._entity_suffix == ENTITY_MANUAL_IDLE_TIMEOUT: + self.coordinator.set_manual_idle_timeout(value) + elif self._entity_suffix == ENTITY_MANUAL_OFF_COOLDOWN: + self.coordinator.set_manual_off_cooldown(value) + except (ValueError, TypeError): + self.logger.warning("Could not restore %s, using default", self._entity_suffix) + self.async_write_ha_state() + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/sensor.py b/custom_components/sensor.py new file mode 100644 index 0000000..413ceb6 --- /dev/null +++ b/custom_components/sensor.py @@ -0,0 +1,69 @@ +"""Sensor platform for Motion Light.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Motion Light sensor.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([MotionLightStatusSensor(coordinator, entry)]) + + +class MotionLightStatusSensor(CoordinatorEntity, SensorEntity): + """Representation of a Motion Light status sensor.""" + + _attr_translation_key = "motion_light_status" + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__(self, coordinator, entry): + """Initialize the sensor.""" + super().__init__(coordinator) + self.entry = entry + self._attr_unique_id = f"{entry.entry_id}_status" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Light", + model="Switch Control Integration", + ) + self._icon_map = { + "idle": "mdi:sleep", + "detecting": "mdi:motion-sensor", + "on": "mdi:lightbulb-on", + "delaying": "mdi:timer-sand", + "active": "mdi:lightbulb-on-outline", + "off": "mdi:lightbulb-off-outline", + "manual_on": "mdi:hand-back-left", + "manual_off_cooldown": "mdi:timer-pause", + "error": "mdi:alert-circle", + "disabled": "mdi:lightbulb-off", + } + + @property + def native_value(self): + """Return the state of the sensor.""" + return self.coordinator.state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self.coordinator.attributes + + @property + def icon(self): + """Return the icon.""" + return self._icon_map.get(self.coordinator.state, "mdi:lightbulb") + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/switch.py b/custom_components/switch.py new file mode 100644 index 0000000..89d8432 --- /dev/null +++ b/custom_components/switch.py @@ -0,0 +1,148 @@ +"""Switch platform for Motion Light.""" +import logging +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN, ENTITY_RESPECT_MANUAL, DEFAULT_RESPECT_MANUAL + +_LOGGER = logging.getLogger(__name__) + + +class PrefixLoggerAdapter(logging.LoggerAdapter): + """Logger adapter that adds a prefix to log messages.""" + + def process(self, msg, kwargs): + """Add prefix to the log message.""" + return f"[{self.extra.get('name', 'Unknown')}] {msg}", kwargs + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Motion Light switches.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([ + MotionLightEnabledSwitch(coordinator, entry), + MotionLightRespectManualSwitch(coordinator, entry), + ]) + + +class MotionLightEnabledSwitch(CoordinatorEntity, SwitchEntity, RestoreEntity): + """Representation of a Motion Light enabled switch.""" + _attr_translation_key = "motion_light_enabled" + _attr_has_entity_name = True + + def __init__(self, coordinator, entry): + """Initialize the switch.""" + super().__init__(coordinator) + self.entry = entry + + self.logger = PrefixLoggerAdapter(_LOGGER, {"name": entry.title}) + + self._attr_unique_id = f"{entry.entry_id}_enabled" + self._attr_icon = "mdi:lightbulb-auto" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Light", + model="Switch Control Integration", + ) + + @property + def is_on(self) -> bool: + """Return True if integration is enabled.""" + return self.coordinator.is_enabled + + async def async_turn_on(self, **kwargs) -> None: + """Turn on integration.""" + self.logger.info("Switch turned ON, enabling integration") + await self.coordinator.set_enabled(True) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off integration.""" + self.logger.info("Switch turned OFF, disabling integration") + await self.coordinator.set_enabled(False) + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is not None: + is_enabled = last_state.state == STATE_ON + self.logger.info("Restored enabled switch state: %s", last_state.state) + if self.coordinator.is_enabled != is_enabled: + await self.coordinator.set_enabled(is_enabled) + self.async_write_ha_state() + else: + self.logger.info("Enabled switch added for the first time, defaulting to enabled") + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() + + +class MotionLightRespectManualSwitch(CoordinatorEntity, SwitchEntity, RestoreEntity): + """Representation of a Motion Light respect_manual switch.""" + _attr_translation_key = "motion_light_priority_manual" + _attr_has_entity_name = True + + def __init__(self, coordinator, entry): + """Initialize the switch.""" + super().__init__(coordinator) + self.entry = entry + + self.logger = PrefixLoggerAdapter(_LOGGER, {"name": entry.title}) + + self._attr_unique_id = f"{entry.entry_id}_{ENTITY_RESPECT_MANUAL}" + self._attr_icon = "mdi:hand-back-left" + self._is_on = DEFAULT_RESPECT_MANUAL + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Light", + model="Switch Control Integration", + ) + + @property + def is_on(self) -> bool: + """Return True if respect_manual is enabled.""" + return self._is_on + + async def async_turn_on(self, **kwargs) -> None: + """Turn on respect_manual.""" + self.logger.info("Respect manual turned ON") + self._is_on = True + self.coordinator.set_respect_manual(True) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn off respect_manual.""" + self.logger.info("Respect manual turned OFF") + self._is_on = False + self.coordinator.set_respect_manual(False) + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is not None: + self._is_on = last_state.state == STATE_ON + self.logger.info("Restored respect_manual state: %s", last_state.state) + self.coordinator.set_respect_manual(self._is_on) + self.async_write_ha_state() + else: + self.logger.info("Respect manual switch added for the first time, defaulting to %s", self._is_on) + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() \ No newline at end of file