mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 16:27:08 +00:00
Add an RSSI sensor to the LIFX integration (#80993)
This commit is contained in:
parent
0d4b1866a7
commit
dde763418a
@ -57,7 +57,13 @@ CONFIG_SCHEMA = vol.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.SELECT]
|
PLATFORMS = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.LIGHT,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
|
]
|
||||||
DISCOVERY_INTERVAL = timedelta(minutes=15)
|
DISCOVERY_INTERVAL = timedelta(minutes=15)
|
||||||
MIGRATION_INTERVAL = timedelta(minutes=5)
|
MIGRATION_INTERVAL = timedelta(minutes=5)
|
||||||
|
|
||||||
@ -199,6 +205,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
coordinator.async_setup()
|
coordinator.async_setup()
|
||||||
try:
|
try:
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
await coordinator.sensor_coordinator.async_config_entry_first_refresh()
|
||||||
except ConfigEntryNotReady:
|
except ConfigEntryNotReady:
|
||||||
connection.async_stop()
|
connection.async_stop()
|
||||||
raise
|
raise
|
||||||
|
@ -12,8 +12,8 @@ from homeassistant.helpers.entity import EntityCategory
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, HEV_CYCLE_STATE
|
from .const import DOMAIN, HEV_CYCLE_STATE
|
||||||
from .coordinator import LIFXUpdateCoordinator
|
from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
|
||||||
from .entity import LIFXEntity
|
from .entity import LIFXSensorEntity
|
||||||
from .util import lifx_features
|
from .util import lifx_features
|
||||||
|
|
||||||
HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription(
|
HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription(
|
||||||
@ -34,28 +34,28 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
LIFXHevCycleBinarySensorEntity(
|
LIFXHevCycleBinarySensorEntity(
|
||||||
coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR
|
coordinator=coordinator.sensor_coordinator,
|
||||||
|
description=HEV_CYCLE_STATE_SENSOR,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity):
|
class LIFXHevCycleBinarySensorEntity(LIFXSensorEntity, BinarySensorEntity):
|
||||||
"""LIFX HEV cycle state binary sensor."""
|
"""LIFX HEV cycle state binary sensor."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: LIFXUpdateCoordinator,
|
coordinator: LIFXSensorUpdateCoordinator,
|
||||||
description: BinarySensorEntityDescription,
|
description: BinarySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the sensor."""
|
"""Initialise the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_name = description.name
|
self._attr_name = description.name
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
|
||||||
self._async_update_attrs()
|
self._async_update_attrs()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -12,8 +12,8 @@ from homeassistant.helpers.entity import EntityCategory
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, IDENTIFY, RESTART
|
from .const import DOMAIN, IDENTIFY, RESTART
|
||||||
from .coordinator import LIFXUpdateCoordinator
|
from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
|
||||||
from .entity import LIFXEntity
|
from .entity import LIFXSensorEntity
|
||||||
|
|
||||||
RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription(
|
RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription(
|
||||||
key=RESTART,
|
key=RESTART,
|
||||||
@ -38,20 +38,22 @@ async def async_setup_entry(
|
|||||||
domain_data = hass.data[DOMAIN]
|
domain_data = hass.data[DOMAIN]
|
||||||
coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
|
coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
cls(coordinator) for cls in (LIFXRestartButton, LIFXIdentifyButton)
|
cls(coordinator.sensor_coordinator)
|
||||||
|
for cls in (LIFXRestartButton, LIFXIdentifyButton)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LIFXButton(LIFXEntity, ButtonEntity):
|
class LIFXButton(LIFXSensorEntity, ButtonEntity):
|
||||||
"""Base LIFX button."""
|
"""Base LIFX button."""
|
||||||
|
|
||||||
_attr_has_entity_name: bool = True
|
_attr_has_entity_name: bool = True
|
||||||
|
_attr_should_poll: bool = False
|
||||||
|
|
||||||
def __init__(self, coordinator: LIFXUpdateCoordinator) -> None:
|
def __init__(self, coordinator: LIFXSensorUpdateCoordinator) -> None:
|
||||||
"""Initialise a LIFX button."""
|
"""Initialise a LIFX button."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.serial_number}_{self.entity_description.key}"
|
f"{coordinator.parent.serial_number}_{self.entity_description.key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ ATTR_INDICATION = "indication"
|
|||||||
ATTR_INFRARED = "infrared"
|
ATTR_INFRARED = "infrared"
|
||||||
ATTR_POWER = "power"
|
ATTR_POWER = "power"
|
||||||
ATTR_REMAINING = "remaining"
|
ATTR_REMAINING = "remaining"
|
||||||
|
ATTR_RSSI = "rssi"
|
||||||
ATTR_ZONES = "zones"
|
ATTR_ZONES = "zones"
|
||||||
|
|
||||||
ATTR_THEME = "theme"
|
ATTR_THEME = "theme"
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from math import floor, log10
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from aiolifx.aiolifx import (
|
from aiolifx.aiolifx import (
|
||||||
@ -15,8 +17,13 @@ from aiolifx.aiolifx import (
|
|||||||
)
|
)
|
||||||
from aiolifx.connection import LIFXConnection
|
from aiolifx.connection import LIFXConnection
|
||||||
from aiolifx_themes.themes import ThemeLibrary, ThemePainter
|
from aiolifx_themes.themes import ThemeLibrary, ThemePainter
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import (
|
||||||
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -41,8 +48,11 @@ from .util import (
|
|||||||
lifx_features,
|
lifx_features,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LIGHT_UPDATE_INTERVAL = 10
|
||||||
|
SENSOR_UPDATE_INTERVAL = 30
|
||||||
REQUEST_REFRESH_DELAY = 0.35
|
REQUEST_REFRESH_DELAY = 0.35
|
||||||
LIFX_IDENTIFY_DELAY = 3.0
|
LIFX_IDENTIFY_DELAY = 3.0
|
||||||
|
RSSI_DBM_FW = AwesomeVersion("2.77")
|
||||||
|
|
||||||
|
|
||||||
class FirmwareEffect(IntEnum):
|
class FirmwareEffect(IntEnum):
|
||||||
@ -69,14 +79,13 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
self.device: Light = connection.device
|
self.device: Light = connection.device
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self.active_effect = FirmwareEffect.OFF
|
self.active_effect = FirmwareEffect.OFF
|
||||||
update_interval = timedelta(seconds=10)
|
self.sensor_coordinator = LIFXSensorUpdateCoordinator(hass, self, title)
|
||||||
self.last_used_theme: str = ""
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=f"{title} ({self.device.ip_addr})",
|
name=f"{title} ({self.device.ip_addr})",
|
||||||
update_interval=update_interval,
|
update_interval=timedelta(seconds=LIGHT_UPDATE_INTERVAL),
|
||||||
# We don't want an immediate refresh since the device
|
# We don't want an immediate refresh since the device
|
||||||
# takes a moment to reflect the state change
|
# takes a moment to reflect the state change
|
||||||
request_refresh_debouncer=Debouncer(
|
request_refresh_debouncer=Debouncer(
|
||||||
@ -112,11 +121,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"""Return the label of the bulb."""
|
"""Return the label of the bulb."""
|
||||||
return cast(str, self.device.label)
|
return cast(str, self.device.label)
|
||||||
|
|
||||||
@property
|
|
||||||
def current_infrared_brightness(self) -> str | None:
|
|
||||||
"""Return the current infrared brightness as a string."""
|
|
||||||
return infrared_brightness_value_to_option(self.device.infrared_brightness)
|
|
||||||
|
|
||||||
async def diagnostics(self) -> dict[str, Any]:
|
async def diagnostics(self) -> dict[str, Any]:
|
||||||
"""Return diagnostic information about the device."""
|
"""Return diagnostic information about the device."""
|
||||||
features = lifx_features(self.device)
|
features = lifx_features(self.device)
|
||||||
@ -162,19 +166,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
platform, DOMAIN, f"{self.serial_number}_{key}"
|
platform, DOMAIN, f"{self.serial_number}_{key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_identify_bulb(self) -> None:
|
|
||||||
"""Identify the device by flashing it three times."""
|
|
||||||
bulb: Light = self.device
|
|
||||||
if bulb.power_level:
|
|
||||||
# just flash the bulb for three seconds
|
|
||||||
await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
|
|
||||||
return
|
|
||||||
# Turn the bulb on first, flash for 3 seconds, then turn off
|
|
||||||
await self.async_set_power(state=True, duration=1)
|
|
||||||
await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
|
|
||||||
await asyncio.sleep(LIFX_IDENTIFY_DELAY)
|
|
||||||
await self.async_set_power(state=False, duration=1)
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch all device data from the api."""
|
"""Fetch all device data from the api."""
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
@ -203,12 +194,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
await self.async_get_color_zones()
|
await self.async_get_color_zones()
|
||||||
await self.async_get_multizone_effect()
|
await self.async_get_multizone_effect()
|
||||||
|
|
||||||
if lifx_features(self.device)["hev"]:
|
|
||||||
await self.async_get_hev_cycle()
|
|
||||||
|
|
||||||
if lifx_features(self.device)["infrared"]:
|
|
||||||
response = await async_execute_lifx(self.device.get_infrared)
|
|
||||||
|
|
||||||
async def async_get_color_zones(self) -> None:
|
async def async_get_color_zones(self) -> None:
|
||||||
"""Get updated color information for each zone."""
|
"""Get updated color information for each zone."""
|
||||||
zone = 0
|
zone = 0
|
||||||
@ -234,17 +219,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
f"Timeout getting color zones from {self.name}"
|
f"Timeout getting color zones from {self.name}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
def async_get_hev_cycle_state(self) -> bool | None:
|
|
||||||
"""Return the current HEV cycle state."""
|
|
||||||
if self.device.hev_cycle is None:
|
|
||||||
return None
|
|
||||||
return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
|
|
||||||
|
|
||||||
async def async_get_hev_cycle(self) -> None:
|
|
||||||
"""Update the HEV cycle status from a LIFX Clean bulb."""
|
|
||||||
if lifx_features(self.device)["hev"]:
|
|
||||||
await async_execute_lifx(self.device.get_hev_cycle)
|
|
||||||
|
|
||||||
async def async_set_waveform_optional(
|
async def async_set_waveform_optional(
|
||||||
self, value: dict[str, Any], rapid: bool = False
|
self, value: dict[str, Any], rapid: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -381,6 +355,109 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"""Return the enum value of the currently active firmware effect."""
|
"""Return the enum value of the currently active firmware effect."""
|
||||||
return self.active_effect.value
|
return self.active_effect.value
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXSensorUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""DataUpdateCoordinator to gather data for a specific lifx device."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
parent: LIFXUpdateCoordinator,
|
||||||
|
title: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize DataUpdateCoordinator."""
|
||||||
|
self.parent: LIFXUpdateCoordinator = parent
|
||||||
|
self.device: Light = parent.device
|
||||||
|
self._update_rssi: bool = False
|
||||||
|
self._rssi: int = 0
|
||||||
|
self.last_used_theme: str = ""
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=f"{title} Sensors ({self.device.ip_addr})",
|
||||||
|
update_interval=timedelta(seconds=SENSOR_UPDATE_INTERVAL),
|
||||||
|
# Refresh immediately because the changes are not visible
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=0, immediate=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rssi(self) -> int:
|
||||||
|
"""Return stored RSSI value."""
|
||||||
|
return self._rssi
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rssi_uom(self) -> str:
|
||||||
|
"""Return the RSSI unit of measurement."""
|
||||||
|
if AwesomeVersion(self.device.host_firmware_version) <= RSSI_DBM_FW:
|
||||||
|
return SIGNAL_STRENGTH_DECIBELS
|
||||||
|
|
||||||
|
return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_infrared_brightness(self) -> str | None:
|
||||||
|
"""Return the current infrared brightness as a string."""
|
||||||
|
return infrared_brightness_value_to_option(self.device.infrared_brightness)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Fetch all device data from the api."""
|
||||||
|
|
||||||
|
if self._update_rssi is True:
|
||||||
|
await self.async_update_rssi()
|
||||||
|
|
||||||
|
if lifx_features(self.device)["hev"]:
|
||||||
|
await self.async_get_hev_cycle()
|
||||||
|
|
||||||
|
if lifx_features(self.device)["infrared"]:
|
||||||
|
await async_execute_lifx(self.device.get_infrared)
|
||||||
|
|
||||||
|
async def async_set_infrared_brightness(self, option: str) -> None:
|
||||||
|
"""Set infrared brightness."""
|
||||||
|
infrared_brightness = infrared_brightness_option_to_value(option)
|
||||||
|
await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
|
||||||
|
|
||||||
|
async def async_identify_bulb(self) -> None:
|
||||||
|
"""Identify the device by flashing it three times."""
|
||||||
|
bulb: Light = self.device
|
||||||
|
if bulb.power_level:
|
||||||
|
# just flash the bulb for three seconds
|
||||||
|
await self.parent.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
|
||||||
|
return
|
||||||
|
# Turn the bulb on first, flash for 3 seconds, then turn off
|
||||||
|
await self.parent.async_set_power(state=True, duration=1)
|
||||||
|
await self.parent.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
|
||||||
|
await asyncio.sleep(LIFX_IDENTIFY_DELAY)
|
||||||
|
await self.parent.async_set_power(state=False, duration=1)
|
||||||
|
|
||||||
|
def async_enable_rssi_updates(self) -> Callable[[], None]:
|
||||||
|
"""Enable RSSI signal strength updates."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_disable_rssi_updates() -> None:
|
||||||
|
"""Disable RSSI updates when sensor removed."""
|
||||||
|
self._update_rssi = False
|
||||||
|
|
||||||
|
self._update_rssi = True
|
||||||
|
return _async_disable_rssi_updates
|
||||||
|
|
||||||
|
async def async_update_rssi(self) -> None:
|
||||||
|
"""Update RSSI value."""
|
||||||
|
resp = await async_execute_lifx(self.device.get_wifiinfo)
|
||||||
|
self._rssi = int(floor(10 * log10(resp.signal) + 0.5))
|
||||||
|
|
||||||
|
def async_get_hev_cycle_state(self) -> bool | None:
|
||||||
|
"""Return the current HEV cycle state."""
|
||||||
|
if self.device.hev_cycle is None:
|
||||||
|
return None
|
||||||
|
return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
|
||||||
|
|
||||||
|
async def async_get_hev_cycle(self) -> None:
|
||||||
|
"""Update the HEV cycle status from a LIFX Clean bulb."""
|
||||||
|
if lifx_features(self.device)["hev"]:
|
||||||
|
await async_execute_lifx(self.device.get_hev_cycle)
|
||||||
|
|
||||||
async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None:
|
async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None:
|
||||||
"""Start or stop an HEV cycle on a LIFX Clean bulb."""
|
"""Start or stop an HEV cycle on a LIFX Clean bulb."""
|
||||||
if lifx_features(self.device)["hev"]:
|
if lifx_features(self.device)["hev"]:
|
||||||
@ -388,13 +465,8 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
partial(self.device.set_hev_cycle, enable=enable, duration=duration)
|
partial(self.device.set_hev_cycle, enable=enable, duration=duration)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_set_infrared_brightness(self, option: str) -> None:
|
|
||||||
"""Set infrared brightness."""
|
|
||||||
infrared_brightness = infrared_brightness_option_to_value(option)
|
|
||||||
await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
|
|
||||||
|
|
||||||
async def async_apply_theme(self, theme_name: str) -> None:
|
async def async_apply_theme(self, theme_name: str) -> None:
|
||||||
"""Apply the selected theme to the device."""
|
"""Apply the selected theme to the device."""
|
||||||
self.last_used_theme = theme_name
|
self.last_used_theme = theme_name
|
||||||
theme = ThemeLibrary().get_theme(theme_name)
|
theme = ThemeLibrary().get_theme(theme_name)
|
||||||
await ThemePainter(self.hass.loop).paint(theme, [self.device])
|
await ThemePainter(self.hass.loop).paint(theme, [self.parent.device])
|
||||||
|
@ -8,7 +8,7 @@ from homeassistant.helpers.entity import DeviceInfo
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import LIFXUpdateCoordinator
|
from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]):
|
class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]):
|
||||||
@ -26,3 +26,20 @@ class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]):
|
|||||||
model=products.product_map.get(self.bulb.product, "LIFX Bulb"),
|
model=products.product_map.get(self.bulb.product, "LIFX Bulb"),
|
||||||
sw_version=self.bulb.host_firmware_version,
|
sw_version=self.bulb.host_firmware_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXSensorEntity(CoordinatorEntity[LIFXSensorUpdateCoordinator]):
|
||||||
|
"""Representation of a LIFX sensor entity with a sensor coordinator."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator: LIFXSensorUpdateCoordinator) -> None:
|
||||||
|
"""Initialise the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.bulb = coordinator.parent.device
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, coordinator.parent.serial_number)},
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.parent.mac_address)},
|
||||||
|
manufacturer="LIFX",
|
||||||
|
name=coordinator.parent.label,
|
||||||
|
model=products.product_map.get(self.bulb.product, "LIFX Bulb"),
|
||||||
|
sw_version=self.bulb.host_firmware_version,
|
||||||
|
)
|
||||||
|
@ -271,7 +271,9 @@ class LIFXLight(LIFXEntity, LightEntity):
|
|||||||
"This device does not support setting HEV cycle state"
|
"This device does not support setting HEV cycle state"
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.coordinator.async_set_hev_cycle_state(power, duration or 0)
|
await self.coordinator.sensor_coordinator.async_set_hev_cycle_state(
|
||||||
|
power, duration or 0
|
||||||
|
)
|
||||||
await self.update_during_transition(duration or 0)
|
await self.update_during_transition(duration or 0)
|
||||||
|
|
||||||
async def set_power(
|
async def set_power(
|
||||||
|
@ -15,8 +15,8 @@ from .const import (
|
|||||||
INFRARED_BRIGHTNESS,
|
INFRARED_BRIGHTNESS,
|
||||||
INFRARED_BRIGHTNESS_VALUES_MAP,
|
INFRARED_BRIGHTNESS_VALUES_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import LIFXUpdateCoordinator
|
from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
|
||||||
from .entity import LIFXEntity
|
from .entity import LIFXSensorEntity
|
||||||
from .util import lifx_features
|
from .util import lifx_features
|
||||||
|
|
||||||
THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
|
THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
|
||||||
@ -41,36 +41,41 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up LIFX from a config entry."""
|
"""Set up LIFX from a config entry."""
|
||||||
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
entities: list[LIFXEntity] = []
|
|
||||||
|
entities: list[LIFXSensorEntity] = []
|
||||||
|
|
||||||
if lifx_features(coordinator.device)["infrared"]:
|
if lifx_features(coordinator.device)["infrared"]:
|
||||||
entities.append(
|
entities.append(
|
||||||
LIFXInfraredBrightnessSelectEntity(
|
LIFXInfraredBrightnessSelectEntity(
|
||||||
coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
|
coordinator.sensor_coordinator, description=INFRARED_BRIGHTNESS_ENTITY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if lifx_features(coordinator.device)["multizone"] is True:
|
if lifx_features(coordinator.device)["multizone"] is True:
|
||||||
entities.append(
|
entities.append(
|
||||||
LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)
|
LIFXThemeSelectEntity(
|
||||||
|
coordinator.sensor_coordinator, description=THEME_ENTITY
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
|
class LIFXInfraredBrightnessSelectEntity(LIFXSensorEntity, SelectEntity):
|
||||||
"""LIFX Nightvision infrared brightness configuration entity."""
|
"""LIFX Nightvision infrared brightness configuration entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
|
self,
|
||||||
|
coordinator: LIFXSensorUpdateCoordinator,
|
||||||
|
description: SelectEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the IR brightness config entity."""
|
"""Initialise the IR brightness config entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_name = description.name
|
self._attr_name = description.name
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
|
||||||
self._attr_current_option = coordinator.current_infrared_brightness
|
self._attr_current_option = coordinator.current_infrared_brightness
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -89,21 +94,22 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
|
|||||||
await self.coordinator.async_set_infrared_brightness(option)
|
await self.coordinator.async_set_infrared_brightness(option)
|
||||||
|
|
||||||
|
|
||||||
class LIFXThemeSelectEntity(LIFXEntity, SelectEntity):
|
class LIFXThemeSelectEntity(LIFXSensorEntity, SelectEntity):
|
||||||
"""Theme entity for LIFX multizone devices."""
|
"""Theme entity for LIFX multizone devices."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
|
self,
|
||||||
|
coordinator: LIFXSensorUpdateCoordinator,
|
||||||
|
description: SelectEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise the theme selection entity."""
|
"""Initialise the theme selection entity."""
|
||||||
|
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_name = description.name
|
self._attr_name = description.name
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
|
||||||
self._attr_current_option = None
|
self._attr_current_option = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
74
homeassistant/components/lifx/sensor.py
Normal file
74
homeassistant/components/lifx/sensor.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
"""Sensors for LIFX lights."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import ATTR_RSSI, DOMAIN
|
||||||
|
from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
|
||||||
|
from .entity import LIFXSensorEntity
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
RSSI_SENSOR = SensorEntityDescription(
|
||||||
|
key=ATTR_RSSI,
|
||||||
|
name="RSSI",
|
||||||
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up LIFX sensor from config entry."""
|
||||||
|
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
async_add_entities([LIFXRssiSensor(coordinator.sensor_coordinator, RSSI_SENSOR)])
|
||||||
|
|
||||||
|
|
||||||
|
class LIFXRssiSensor(LIFXSensorEntity, SensorEntity):
|
||||||
|
"""LIFX RSSI sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: LIFXSensorUpdateCoordinator,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialise the RSSI sensor."""
|
||||||
|
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_name = description.name
|
||||||
|
self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
|
||||||
|
self._attr_native_unit_of_measurement = coordinator.rssi_uom
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Handle coordinator updates."""
|
||||||
|
self._attr_native_value = self.coordinator.rssi
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Enable RSSI updates."""
|
||||||
|
self.async_on_remove(self.coordinator.async_enable_rssi_updates())
|
||||||
|
return await super().async_added_to_hass()
|
@ -91,6 +91,7 @@ def _mocked_bulb() -> Light:
|
|||||||
bulb.set_power = MockLifxCommand(bulb)
|
bulb.set_power = MockLifxCommand(bulb)
|
||||||
bulb.set_color = MockLifxCommand(bulb)
|
bulb.set_color = MockLifxCommand(bulb)
|
||||||
bulb.get_hostfirmware = MockLifxCommand(bulb)
|
bulb.get_hostfirmware = MockLifxCommand(bulb)
|
||||||
|
bulb.get_wifiinfo = MockLifxCommand(bulb, signal=100)
|
||||||
bulb.get_version = MockLifxCommand(bulb)
|
bulb.get_version = MockLifxCommand(bulb)
|
||||||
bulb.set_waveform_optional = MockLifxCommand(bulb)
|
bulb.set_waveform_optional = MockLifxCommand(bulb)
|
||||||
bulb.product = 1 # LIFX Original 1000
|
bulb.product = 1 # LIFX Original 1000
|
||||||
@ -168,6 +169,12 @@ def _mocked_tile() -> Light:
|
|||||||
return bulb
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
|
def _mocked_bulb_old_firmware() -> Light:
|
||||||
|
bulb = _mocked_bulb()
|
||||||
|
bulb.host_firmware_version = "2.77"
|
||||||
|
return bulb
|
||||||
|
|
||||||
|
|
||||||
def _mocked_bulb_new_firmware() -> Light:
|
def _mocked_bulb_new_firmware() -> Light:
|
||||||
bulb = _mocked_bulb()
|
bulb = _mocked_bulb()
|
||||||
bulb.host_firmware_version = "3.90"
|
bulb.host_firmware_version = "3.90"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Test the lifx binary sensor platwform."""
|
"""Test the lifx binary sensor platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
131
tests/components/lifx/test_sensor.py
Normal file
131
tests/components/lifx/test_sensor.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""Test the LIFX sensor platform."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components import lifx
|
||||||
|
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
CONF_HOST,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
DEFAULT_ENTRY_TITLE,
|
||||||
|
IP_ADDRESS,
|
||||||
|
MAC_ADDRESS,
|
||||||
|
_mocked_bulb,
|
||||||
|
_mocked_bulb_old_firmware,
|
||||||
|
_patch_config_flow_try_connect,
|
||||||
|
_patch_device,
|
||||||
|
_patch_discovery,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rssi_sensor(hass: HomeAssistant) -> None:
|
||||||
|
"""Test LIFX RSSI sensor entity."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=lifx.DOMAIN,
|
||||||
|
title=DEFAULT_ENTRY_TITLE,
|
||||||
|
data={CONF_HOST: IP_ADDRESS},
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
bulb = _mocked_bulb()
|
||||||
|
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||||
|
device=bulb
|
||||||
|
), _patch_device(device=bulb):
|
||||||
|
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "sensor.my_bulb_rssi"
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
entry = entity_registry.entities.get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.disabled
|
||||||
|
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = entity_registry.async_update_entity(
|
||||||
|
entry.entity_id, **{"disabled_by": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||||
|
device=bulb
|
||||||
|
), _patch_device(device=bulb):
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
assert updated_entry.unit_of_measurement == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
rssi = hass.states.get(entity_id)
|
||||||
|
assert (
|
||||||
|
rssi.attributes[ATTR_UNIT_OF_MEASUREMENT] == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
|
)
|
||||||
|
assert rssi.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SIGNAL_STRENGTH
|
||||||
|
assert rssi.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rssi_sensor_old_firmware(hass: HomeAssistant) -> None:
|
||||||
|
"""Test LIFX RSSI sensor entity."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=lifx.DOMAIN,
|
||||||
|
title=DEFAULT_ENTRY_TITLE,
|
||||||
|
data={CONF_HOST: IP_ADDRESS},
|
||||||
|
unique_id=MAC_ADDRESS,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
bulb = _mocked_bulb_old_firmware()
|
||||||
|
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||||
|
device=bulb
|
||||||
|
), _patch_device(device=bulb):
|
||||||
|
await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = "sensor.my_bulb_rssi"
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
entry = entity_registry.entities.get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.disabled
|
||||||
|
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = entity_registry.async_update_entity(
|
||||||
|
entry.entity_id, **{"disabled_by": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
|
||||||
|
device=bulb
|
||||||
|
), _patch_device(device=bulb):
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
assert updated_entry.unit_of_measurement == SIGNAL_STRENGTH_DECIBELS
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
rssi = hass.states.get(entity_id)
|
||||||
|
assert rssi.attributes[ATTR_UNIT_OF_MEASUREMENT] == SIGNAL_STRENGTH_DECIBELS
|
||||||
|
assert rssi.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SIGNAL_STRENGTH
|
||||||
|
assert rssi.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
Loading…
x
Reference in New Issue
Block a user