mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Support for Fritz!DECT 500 lightbulbs (#52830)
This commit is contained in:
parent
ab2ff45726
commit
2148c84386
@ -181,7 +181,7 @@ homeassistant/components/foscam/* @skgsergio
|
||||
homeassistant/components/freebox/* @hacf-fr @Quentame
|
||||
homeassistant/components/freedompro/* @stefano055415
|
||||
homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74
|
||||
homeassistant/components/fritzbox/* @mib1185
|
||||
homeassistant/components/fritzbox/* @mib1185 @flabbamann
|
||||
homeassistant/components/fronius/* @nielstron
|
||||
homeassistant/components/frontend/* @home-assistant/frontend
|
||||
homeassistant/components/garages_amsterdam/* @klaasnicolaas
|
||||
|
@ -11,6 +11,9 @@ ATTR_STATE_LOCKED: Final = "locked"
|
||||
ATTR_STATE_SUMMER_MODE: Final = "summer_mode"
|
||||
ATTR_STATE_WINDOW_OPEN: Final = "window_open"
|
||||
|
||||
COLOR_MODE: Final = "1"
|
||||
COLOR_TEMP_MODE: Final = "4"
|
||||
|
||||
CONF_CONNECTIONS: Final = "connections"
|
||||
CONF_COORDINATOR: Final = "coordinator"
|
||||
|
||||
@ -21,4 +24,4 @@ DOMAIN: Final = "fritzbox"
|
||||
|
||||
LOGGER: Final[logging.Logger] = logging.getLogger(__package__)
|
||||
|
||||
PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "switch", "sensor"]
|
||||
PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "light", "switch", "sensor"]
|
||||
|
155
homeassistant/components/fritzbox/light.py
Normal file
155
homeassistant/components/fritzbox/light.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""Support for AVM FRITZ!SmartHome lightbulbs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyfritzhome.fritzhomedevice import FritzhomeDevice
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_HS_COLOR,
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_HS,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import color
|
||||
|
||||
from . import FritzBoxEntity
|
||||
from .const import (
|
||||
COLOR_MODE,
|
||||
COLOR_TEMP_MODE,
|
||||
CONF_COORDINATOR,
|
||||
DOMAIN as FRITZBOX_DOMAIN,
|
||||
)
|
||||
|
||||
SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the FRITZ!SmartHome light from ConfigEntry."""
|
||||
entities: list[FritzboxLight] = []
|
||||
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
|
||||
|
||||
for ain, device in coordinator.data.items():
|
||||
if not device.has_lightbulb:
|
||||
continue
|
||||
|
||||
supported_color_temps = await hass.async_add_executor_job(
|
||||
device.get_color_temps
|
||||
)
|
||||
|
||||
supported_colors = await hass.async_add_executor_job(device.get_colors)
|
||||
|
||||
entities.append(
|
||||
FritzboxLight(
|
||||
coordinator,
|
||||
ain,
|
||||
supported_colors,
|
||||
supported_color_temps,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class FritzboxLight(FritzBoxEntity, LightEntity):
|
||||
"""The light class for FRITZ!SmartHome lightbulbs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]],
|
||||
ain: str,
|
||||
supported_colors: dict,
|
||||
supported_color_temps: list[str],
|
||||
) -> None:
|
||||
"""Initialize the FritzboxLight entity."""
|
||||
super().__init__(coordinator, ain, None)
|
||||
|
||||
max_kelvin = int(max(supported_color_temps))
|
||||
min_kelvin = int(min(supported_color_temps))
|
||||
|
||||
# max kelvin is min mireds and min kelvin is max mireds
|
||||
self._attr_min_mireds = color.color_temperature_kelvin_to_mired(max_kelvin)
|
||||
self._attr_max_mireds = color.color_temperature_kelvin_to_mired(min_kelvin)
|
||||
|
||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||
self._supported_hs = {}
|
||||
for values in supported_colors.values():
|
||||
hue = int(values[0][0])
|
||||
self._supported_hs[hue] = [
|
||||
int(values[0][1]),
|
||||
int(values[1][1]),
|
||||
int(values[2][1]),
|
||||
]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""If the light is currently on or off."""
|
||||
return self.device.state # type: ignore [no-any-return]
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the current Brightness."""
|
||||
return self.device.level # type: ignore [no-any-return]
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""Return the hs color value."""
|
||||
if self.device.color_mode != COLOR_MODE:
|
||||
return None
|
||||
|
||||
hue = self.device.hue
|
||||
saturation = self.device.saturation
|
||||
|
||||
return (hue, float(saturation) * 100.0 / 255.0)
|
||||
|
||||
@property
|
||||
def color_temp(self) -> int | None:
|
||||
"""Return the CT color value."""
|
||||
if self.device.color_mode != COLOR_TEMP_MODE:
|
||||
return None
|
||||
|
||||
kelvin = self.device.color_temp
|
||||
return color.color_temperature_kelvin_to_mired(kelvin)
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set:
|
||||
"""Flag supported color modes."""
|
||||
return SUPPORTED_COLOR_MODES
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
if kwargs.get(ATTR_BRIGHTNESS) is not None:
|
||||
level = kwargs[ATTR_BRIGHTNESS]
|
||||
await self.hass.async_add_executor_job(self.device.set_level, level)
|
||||
if kwargs.get(ATTR_HS_COLOR) is not None:
|
||||
hass_hue = int(kwargs[ATTR_HS_COLOR][0])
|
||||
hass_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0)
|
||||
# find supported hs values closest to what user selected
|
||||
hue = min(self._supported_hs.keys(), key=lambda x: abs(x - hass_hue))
|
||||
saturation = min(
|
||||
self._supported_hs[hue], key=lambda x: abs(x - hass_saturation)
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.set_color, (hue, saturation)
|
||||
)
|
||||
|
||||
if kwargs.get(ATTR_COLOR_TEMP) is not None:
|
||||
kelvin = color.color_temperature_kelvin_to_mired(kwargs[ATTR_COLOR_TEMP])
|
||||
await self.hass.async_add_executor_job(self.device.set_color_temp, kelvin)
|
||||
|
||||
await self.hass.async_add_executor_job(self.device.set_state_on)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self.hass.async_add_executor_job(self.device.set_state_off)
|
||||
await self.coordinator.async_refresh()
|
@ -8,7 +8,7 @@
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@mib1185"],
|
||||
"codeowners": ["@mib1185", "@flabbamann"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock):
|
||||
has_alarm = True
|
||||
has_powermeter = False
|
||||
has_switch = False
|
||||
has_lightbulb = False
|
||||
has_temperature_sensor = False
|
||||
has_thermostat = False
|
||||
present = True
|
||||
@ -75,6 +76,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock):
|
||||
fw_version = "1.2.3"
|
||||
has_alarm = False
|
||||
has_powermeter = False
|
||||
has_lightbulb = False
|
||||
has_switch = False
|
||||
has_temperature_sensor = False
|
||||
has_thermostat = True
|
||||
@ -94,6 +96,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock):
|
||||
fw_version = "1.2.3"
|
||||
has_alarm = False
|
||||
has_powermeter = False
|
||||
has_lightbulb = False
|
||||
has_switch = False
|
||||
has_temperature_sensor = True
|
||||
has_thermostat = False
|
||||
@ -113,6 +116,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock):
|
||||
fw_version = "1.2.3"
|
||||
has_alarm = False
|
||||
has_powermeter = True
|
||||
has_lightbulb = False
|
||||
has_switch = True
|
||||
has_temperature_sensor = True
|
||||
has_thermostat = False
|
||||
@ -121,3 +125,18 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock):
|
||||
power = 5678
|
||||
present = True
|
||||
temperature = 1.23
|
||||
|
||||
|
||||
class FritzDeviceLightMock(FritzDeviceBaseMock):
|
||||
"""Mock of a AVM Fritz!Box light device."""
|
||||
|
||||
fw_version = "1.2.3"
|
||||
has_alarm = False
|
||||
has_powermeter = False
|
||||
has_lightbulb = True
|
||||
has_switch = False
|
||||
has_temperature_sensor = False
|
||||
has_thermostat = False
|
||||
level = 100
|
||||
present = True
|
||||
state = True
|
||||
|
185
tests/components/fritzbox/test_light.py
Normal file
185
tests/components/fritzbox/test_light.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""Tests for AVM Fritz!Box light component."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from homeassistant.components.fritzbox.const import (
|
||||
COLOR_MODE,
|
||||
COLOR_TEMP_MODE,
|
||||
DOMAIN as FB_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_HS_COLOR,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICES,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import color
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import FritzDeviceLightMock, setup_config_entry
|
||||
from .const import CONF_FAKE_NAME, MOCK_CONFIG
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}"
|
||||
|
||||
|
||||
async def test_setup(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test setup of platform."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
device.color_mode = COLOR_TEMP_MODE
|
||||
device.color_temp = 2700
|
||||
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
|
||||
assert state.attributes[ATTR_COLOR_TEMP] == color.color_temperature_kelvin_to_mired(
|
||||
2700
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_color(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test setup of platform in color mode."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
device.color_mode = COLOR_MODE
|
||||
device.hue = 100
|
||||
device.saturation = 70 * 255.0 / 100.0
|
||||
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 100
|
||||
assert state.attributes[ATTR_HS_COLOR] == (100, 70)
|
||||
|
||||
|
||||
async def test_turn_on(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test turn device on."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP: 300},
|
||||
True,
|
||||
)
|
||||
assert device.set_state_on.call_count == 1
|
||||
assert device.set_level.call_count == 1
|
||||
assert device.set_color_temp.call_count == 1
|
||||
|
||||
|
||||
async def test_turn_on_color(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test turn device on in color mode."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_HS_COLOR: (100, 70)},
|
||||
True,
|
||||
)
|
||||
assert device.set_state_on.call_count == 1
|
||||
assert device.set_level.call_count == 1
|
||||
assert device.set_color.call_count == 1
|
||||
|
||||
|
||||
async def test_turn_off(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test turn device off."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||
)
|
||||
assert device.set_state_off.call_count == 1
|
||||
|
||||
|
||||
async def test_update(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test update without error."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
assert device.update.call_count == 1
|
||||
assert fritz().login.call_count == 1
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device.update.call_count == 2
|
||||
assert fritz().login.call_count == 1
|
||||
|
||||
|
||||
async def test_update_error(hass: HomeAssistant, fritz: Mock):
|
||||
"""Test update with error."""
|
||||
device = FritzDeviceLightMock()
|
||||
device.get_color_temps.return_value = [2700, 6500]
|
||||
device.get_colors.return_value = {
|
||||
"Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")]
|
||||
}
|
||||
device.update.side_effect = HTTPError("Boom")
|
||||
assert not await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
assert device.update.call_count == 1
|
||||
assert fritz().login.call_count == 1
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=200)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device.update.call_count == 2
|
||||
assert fritz().login.call_count == 2
|
Loading…
x
Reference in New Issue
Block a user