mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Merge pull request #63004 from home-assistant/rc
This commit is contained in:
commit
92066d2a62
@ -3,7 +3,7 @@
|
|||||||
"name": "Flux LED/MagicHome",
|
"name": "Flux LED/MagicHome",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||||
"requirements": ["flux_led==0.27.13"],
|
"requirements": ["flux_led==0.27.21"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"codeowners": ["@icemanch"],
|
"codeowners": ["@icemanch"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"home-assistant-frontend==20211227.0"
|
"home-assistant-frontend==20211229.0"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Philips Hue",
|
"name": "Philips Hue",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||||
"requirements": ["aiohue==3.0.10"],
|
"requirements": ["aiohue==3.0.11"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics",
|
"manufacturer": "Royal Philips Electronics",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Support for Hue groups (room/zone)."""
|
"""Support for Hue groups (room/zone)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohue.v2 import HueBridgeV2
|
from aiohue.v2 import HueBridgeV2
|
||||||
@ -18,6 +19,7 @@ from homeassistant.components.light import (
|
|||||||
COLOR_MODE_COLOR_TEMP,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
COLOR_MODE_ONOFF,
|
COLOR_MODE_ONOFF,
|
||||||
COLOR_MODE_XY,
|
COLOR_MODE_XY,
|
||||||
|
FLASH_SHORT,
|
||||||
SUPPORT_FLASH,
|
SUPPORT_FLASH,
|
||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
@ -29,14 +31,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from ..bridge import HueBridge
|
from ..bridge import HueBridge
|
||||||
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
|
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
|
||||||
from .entity import HueBaseEntity
|
from .entity import HueBaseEntity
|
||||||
from .helpers import normalize_hue_brightness, normalize_hue_transition
|
from .helpers import (
|
||||||
|
normalize_hue_brightness,
|
||||||
|
normalize_hue_colortemp,
|
||||||
|
normalize_hue_transition,
|
||||||
|
)
|
||||||
|
|
||||||
ALLOWED_ERRORS = [
|
ALLOWED_ERRORS = [
|
||||||
"device (groupedLight) has communication issues, command (on) may not have effect",
|
"device (groupedLight) has communication issues, command (on) may not have effect",
|
||||||
'device (groupedLight) is "soft off", command (on) may not have effect',
|
'device (groupedLight) is "soft off", command (on) may not have effect',
|
||||||
"device (light) has communication issues, command (on) may not have effect",
|
"device (light) has communication issues, command (on) may not have effect",
|
||||||
'device (light) is "soft off", command (on) may not have effect',
|
'device (light) is "soft off", command (on) may not have effect',
|
||||||
"attribute (supportedAlertActions) cannot be written",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -150,10 +155,15 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
"""Turn the light on."""
|
"""Turn the light on."""
|
||||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
|
||||||
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
||||||
flash = kwargs.get(ATTR_FLASH)
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
if flash is not None:
|
||||||
|
await self.async_set_flash(flash)
|
||||||
|
# flash can not be sent with other commands at the same time
|
||||||
|
return
|
||||||
|
|
||||||
# NOTE: a grouped_light can only handle turn on/off
|
# NOTE: a grouped_light can only handle turn on/off
|
||||||
# To set other features, you'll have to control the attached lights
|
# To set other features, you'll have to control the attached lights
|
||||||
if (
|
if (
|
||||||
@ -173,22 +183,32 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
|
|
||||||
# redirect all other feature commands to underlying lights
|
# redirect all other feature commands to underlying lights
|
||||||
# note that this silently ignores params sent to light that are not supported
|
# note that this silently ignores params sent to light that are not supported
|
||||||
for light in self.controller.get_lights(self.resource.id):
|
await asyncio.gather(
|
||||||
await self.bridge.async_request_call(
|
*[
|
||||||
self.api.lights.set_state,
|
self.bridge.async_request_call(
|
||||||
light.id,
|
self.api.lights.set_state,
|
||||||
on=True,
|
light.id,
|
||||||
brightness=brightness if light.supports_dimming else None,
|
on=True,
|
||||||
color_xy=xy_color if light.supports_color else None,
|
brightness=brightness if light.supports_dimming else None,
|
||||||
color_temp=color_temp if light.supports_color_temperature else None,
|
color_xy=xy_color if light.supports_color else None,
|
||||||
transition_time=transition,
|
color_temp=color_temp if light.supports_color_temperature else None,
|
||||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
transition_time=transition,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||||
)
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
|
)
|
||||||
|
for light in self.controller.get_lights(self.resource.id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||||
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
if flash is not None:
|
||||||
|
await self.async_set_flash(flash)
|
||||||
|
# flash can not be sent with other commands at the same time
|
||||||
|
return
|
||||||
|
|
||||||
# NOTE: a grouped_light can only handle turn on/off
|
# NOTE: a grouped_light can only handle turn on/off
|
||||||
# To set other features, you'll have to control the attached lights
|
# To set other features, you'll have to control the attached lights
|
||||||
@ -202,14 +222,31 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# redirect all other feature commands to underlying lights
|
# redirect all other feature commands to underlying lights
|
||||||
for light in self.controller.get_lights(self.resource.id):
|
await asyncio.gather(
|
||||||
await self.bridge.async_request_call(
|
*[
|
||||||
self.api.lights.set_state,
|
self.bridge.async_request_call(
|
||||||
light.id,
|
self.api.lights.set_state,
|
||||||
on=False,
|
light.id,
|
||||||
transition_time=transition,
|
on=False,
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
transition_time=transition,
|
||||||
)
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
|
)
|
||||||
|
for light in self.controller.get_lights(self.resource.id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_set_flash(self, flash: str) -> None:
|
||||||
|
"""Send flash command to light."""
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.bridge.async_request_call(
|
||||||
|
self.api.lights.set_flash,
|
||||||
|
id=light.id,
|
||||||
|
short=flash == FLASH_SHORT,
|
||||||
|
)
|
||||||
|
for light in self.controller.get_lights(self.resource.id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def on_update(self) -> None:
|
def on_update(self) -> None:
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Helper functions for Philips Hue v2."""
|
"""Helper functions for Philips Hue v2."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
def normalize_hue_brightness(brightness):
|
def normalize_hue_brightness(brightness: float | None) -> float | None:
|
||||||
"""Return calculated brightness values."""
|
"""Return calculated brightness values."""
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
# Hue uses a range of [0, 100] to control brightness.
|
# Hue uses a range of [0, 100] to control brightness.
|
||||||
@ -10,10 +11,19 @@ def normalize_hue_brightness(brightness):
|
|||||||
return brightness
|
return brightness
|
||||||
|
|
||||||
|
|
||||||
def normalize_hue_transition(transition):
|
def normalize_hue_transition(transition: float | None) -> float | None:
|
||||||
"""Return rounded transition values."""
|
"""Return rounded transition values."""
|
||||||
if transition is not None:
|
if transition is not None:
|
||||||
# hue transition duration is in milliseconds and round them to 100ms
|
# hue transition duration is in milliseconds and round them to 100ms
|
||||||
transition = int(round(transition, 1) * 1000)
|
transition = int(round(transition, 1) * 1000)
|
||||||
|
|
||||||
return transition
|
return transition
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_hue_colortemp(colortemp: int | None) -> int | None:
|
||||||
|
"""Return color temperature within Hue's ranges."""
|
||||||
|
if colortemp is not None:
|
||||||
|
# Hue only accepts a range between 153..500
|
||||||
|
colortemp = min(colortemp, 500)
|
||||||
|
colortemp = max(colortemp, 153)
|
||||||
|
return colortemp
|
||||||
|
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from aiohue.v2 import HueBridgeV2
|
from aiohue.v2 import HueBridgeV2
|
||||||
from aiohue.v2.controllers.events import EventType
|
from aiohue.v2.controllers.events import EventType
|
||||||
from aiohue.v2.models.button import Button, ButtonEvent
|
from aiohue.v2.models.button import Button
|
||||||
|
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID
|
from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@ -27,11 +27,6 @@ async def async_setup_hue_events(bridge: "HueBridge"):
|
|||||||
api: HueBridgeV2 = bridge.api # to satisfy typing
|
api: HueBridgeV2 = bridge.api # to satisfy typing
|
||||||
conf_entry = bridge.config_entry
|
conf_entry = bridge.config_entry
|
||||||
dev_reg = device_registry.async_get(hass)
|
dev_reg = device_registry.async_get(hass)
|
||||||
last_state = {
|
|
||||||
x.id: x.button.last_event
|
|
||||||
for x in api.sensors.button.items
|
|
||||||
if x.button is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
# at this time the `button` resource is the only source of hue events
|
# at this time the `button` resource is the only source of hue events
|
||||||
btn_controller = api.sensors.button
|
btn_controller = api.sensors.button
|
||||||
@ -45,26 +40,16 @@ async def async_setup_hue_events(bridge: "HueBridge"):
|
|||||||
if hue_resource.button is None:
|
if hue_resource.button is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
cur_event = hue_resource.button.last_event
|
|
||||||
last_event = last_state.get(hue_resource.id)
|
|
||||||
# ignore the event if the last_event value is exactly the same
|
|
||||||
# this may happen if some other metadata of the button resource is adjusted
|
|
||||||
if cur_event == last_event:
|
|
||||||
return
|
|
||||||
if cur_event != ButtonEvent.REPEAT:
|
|
||||||
# do not store repeat event
|
|
||||||
last_state[hue_resource.id] = cur_event
|
|
||||||
|
|
||||||
hue_device = btn_controller.get_device(hue_resource.id)
|
hue_device = btn_controller.get_device(hue_resource.id)
|
||||||
device = dev_reg.async_get_device({(DOMAIN, hue_device.id)})
|
device = dev_reg.async_get_device({(DOMAIN, hue_device.id)})
|
||||||
|
|
||||||
# Fire event
|
# Fire event
|
||||||
data = {
|
data = {
|
||||||
# send slugified entity name as id = backwards compatibility with previous version
|
# send slugified entity name as id = backwards compatibility with previous version
|
||||||
CONF_ID: slugify(f"{hue_device.metadata.name}: Button"),
|
CONF_ID: slugify(f"{hue_device.metadata.name} Button"),
|
||||||
CONF_DEVICE_ID: device.id, # type: ignore
|
CONF_DEVICE_ID: device.id, # type: ignore
|
||||||
CONF_UNIQUE_ID: hue_resource.id,
|
CONF_UNIQUE_ID: hue_resource.id,
|
||||||
CONF_TYPE: cur_event.value,
|
CONF_TYPE: hue_resource.button.last_event.value,
|
||||||
CONF_SUBTYPE: hue_resource.metadata.control_id,
|
CONF_SUBTYPE: hue_resource.metadata.control_id,
|
||||||
}
|
}
|
||||||
hass.bus.async_fire(ATTR_HUE_EVENT, data)
|
hass.bus.async_fire(ATTR_HUE_EVENT, data)
|
||||||
|
@ -6,7 +6,6 @@ from typing import Any
|
|||||||
from aiohue import HueBridgeV2
|
from aiohue import HueBridgeV2
|
||||||
from aiohue.v2.controllers.events import EventType
|
from aiohue.v2.controllers.events import EventType
|
||||||
from aiohue.v2.controllers.lights import LightsController
|
from aiohue.v2.controllers.lights import LightsController
|
||||||
from aiohue.v2.models.feature import AlertEffectType
|
|
||||||
from aiohue.v2.models.light import Light
|
from aiohue.v2.models.light import Light
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
@ -19,6 +18,7 @@ from homeassistant.components.light import (
|
|||||||
COLOR_MODE_COLOR_TEMP,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
COLOR_MODE_ONOFF,
|
COLOR_MODE_ONOFF,
|
||||||
COLOR_MODE_XY,
|
COLOR_MODE_XY,
|
||||||
|
FLASH_SHORT,
|
||||||
SUPPORT_FLASH,
|
SUPPORT_FLASH,
|
||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
@ -30,12 +30,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from ..bridge import HueBridge
|
from ..bridge import HueBridge
|
||||||
from ..const import DOMAIN
|
from ..const import DOMAIN
|
||||||
from .entity import HueBaseEntity
|
from .entity import HueBaseEntity
|
||||||
from .helpers import normalize_hue_brightness, normalize_hue_transition
|
from .helpers import (
|
||||||
|
normalize_hue_brightness,
|
||||||
|
normalize_hue_colortemp,
|
||||||
|
normalize_hue_transition,
|
||||||
|
)
|
||||||
|
|
||||||
ALLOWED_ERRORS = [
|
ALLOWED_ERRORS = [
|
||||||
"device (light) has communication issues, command (on) may not have effect",
|
"device (light) has communication issues, command (on) may not have effect",
|
||||||
'device (light) is "soft off", command (on) may not have effect',
|
'device (light) is "soft off", command (on) may not have effect',
|
||||||
"attribute (supportedAlertActions) cannot be written",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +76,8 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
super().__init__(bridge, controller, resource)
|
super().__init__(bridge, controller, resource)
|
||||||
self._attr_supported_features |= SUPPORT_FLASH
|
if self.resource.alert and self.resource.alert.action_values:
|
||||||
|
self._attr_supported_features |= SUPPORT_FLASH
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self._supported_color_modes = set()
|
self._supported_color_modes = set()
|
||||||
@ -158,10 +162,18 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
|
||||||
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
||||||
flash = kwargs.get(ATTR_FLASH)
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
if flash is not None:
|
||||||
|
await self.async_set_flash(flash)
|
||||||
|
# flash can not be sent with other commands at the same time or result will be flaky
|
||||||
|
# Hue's default behavior is that a light returns to its previous state for short
|
||||||
|
# flash (identify) and the light is kept turned on for long flash (breathe effect)
|
||||||
|
# Why is this flash alert/effect hidden in the turn_on/off commands ?
|
||||||
|
return
|
||||||
|
|
||||||
await self.bridge.async_request_call(
|
await self.bridge.async_request_call(
|
||||||
self.controller.set_state,
|
self.controller.set_state,
|
||||||
id=self.resource.id,
|
id=self.resource.id,
|
||||||
@ -170,7 +182,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
color_xy=xy_color,
|
color_xy=xy_color,
|
||||||
color_temp=color_temp,
|
color_temp=color_temp,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -179,11 +190,25 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||||
flash = kwargs.get(ATTR_FLASH)
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
if flash is not None:
|
||||||
|
await self.async_set_flash(flash)
|
||||||
|
# flash can not be sent with other commands at the same time or result will be flaky
|
||||||
|
# Hue's default behavior is that a light returns to its previous state for short
|
||||||
|
# flash (identify) and the light is kept turned on for long flash (breathe effect)
|
||||||
|
return
|
||||||
|
|
||||||
await self.bridge.async_request_call(
|
await self.bridge.async_request_call(
|
||||||
self.controller.set_state,
|
self.controller.set_state,
|
||||||
id=self.resource.id,
|
id=self.resource.id,
|
||||||
on=False,
|
on=False,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_set_flash(self, flash: str) -> None:
|
||||||
|
"""Send flash command to light."""
|
||||||
|
await self.bridge.async_request_call(
|
||||||
|
self.controller.set_flash,
|
||||||
|
id=self.resource.id,
|
||||||
|
short=flash == FLASH_SHORT,
|
||||||
|
)
|
||||||
|
@ -27,6 +27,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
ERROR_STATES,
|
ERROR_STATES,
|
||||||
)
|
)
|
||||||
|
from .helpers import parse_id
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -80,6 +81,14 @@ async def async_setup_entry(hass, entry):
|
|||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
# Migration of entry unique_id
|
||||||
|
if isinstance(entry.unique_id, int):
|
||||||
|
new_id = parse_id(entry.unique_id)
|
||||||
|
params = {"unique_id": new_id}
|
||||||
|
if entry.title == entry.unique_id:
|
||||||
|
params["title"] = new_id
|
||||||
|
hass.config_entries.async_update_entry(entry, **params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bridge = await hass.async_add_executor_job(
|
bridge = await hass.async_add_executor_job(
|
||||||
NukiBridge,
|
NukiBridge,
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
|
|||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
|
from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
|
||||||
|
from .helpers import parse_id
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||||
"""Prepare configuration for a DHCP discovered Nuki bridge."""
|
"""Prepare configuration for a DHCP discovered Nuki bridge."""
|
||||||
await self.async_set_unique_id(int(discovery_info.hostname[12:], 16))
|
await self.async_set_unique_id(discovery_info.hostname[12:].upper())
|
||||||
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
@ -114,7 +115,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"])
|
existing_entry = await self.async_set_unique_id(
|
||||||
|
parse_id(info["ids"]["hardwareId"])
|
||||||
|
)
|
||||||
if existing_entry:
|
if existing_entry:
|
||||||
self.hass.config_entries.async_update_entry(existing_entry, data=conf)
|
self.hass.config_entries.async_update_entry(existing_entry, data=conf)
|
||||||
self.hass.async_create_task(
|
self.hass.async_create_task(
|
||||||
@ -143,11 +146,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
if "base" not in errors:
|
if "base" not in errors:
|
||||||
await self.async_set_unique_id(info["ids"]["hardwareId"])
|
bridge_id = parse_id(info["ids"]["hardwareId"])
|
||||||
|
await self.async_set_unique_id(bridge_id)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(title=bridge_id, data=user_input)
|
||||||
title=info["ids"]["hardwareId"], data=user_input
|
|
||||||
)
|
|
||||||
|
|
||||||
data_schema = self.discovery_schema or USER_SCHEMA
|
data_schema = self.discovery_schema or USER_SCHEMA
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
6
homeassistant/components/nuki/helpers.py
Normal file
6
homeassistant/components/nuki/helpers.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""nuki integration helpers."""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_id(hardware_id):
|
||||||
|
"""Parse Nuki ID."""
|
||||||
|
return hex(hardware_id).split("x")[-1].upper()
|
@ -5,7 +5,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/smarttub",
|
"documentation": "https://www.home-assistant.io/integrations/smarttub",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@mdz"],
|
"codeowners": ["@mdz"],
|
||||||
"requirements": ["python-smarttub==0.0.28"],
|
"requirements": ["python-smarttub==0.0.29"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
@ -482,7 +482,7 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
for bool_var in (
|
for bool_var in (
|
||||||
"dialog_level",
|
"dialog_level",
|
||||||
"night_level",
|
"night_mode",
|
||||||
"sub_enabled",
|
"sub_enabled",
|
||||||
"surround_enabled",
|
"surround_enabled",
|
||||||
):
|
):
|
||||||
|
@ -7,8 +7,9 @@ import logging
|
|||||||
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException
|
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException
|
||||||
|
|
||||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||||
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import callback
|
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG, Platform
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
@ -113,6 +114,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
available_soco_attributes, speaker
|
available_soco_attributes, speaker
|
||||||
)
|
)
|
||||||
for feature_type in available_features:
|
for feature_type in available_features:
|
||||||
|
if feature_type == ATTR_SPEECH_ENHANCEMENT:
|
||||||
|
async_migrate_speech_enhancement_entity_unique_id(
|
||||||
|
hass, config_entry, speaker
|
||||||
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Creating %s switch on %s",
|
"Creating %s switch on %s",
|
||||||
FRIENDLY_NAMES[feature_type],
|
FRIENDLY_NAMES[feature_type],
|
||||||
@ -344,3 +349,48 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||||||
await self.hass.async_add_executor_job(self.alarm.save)
|
await self.hass.async_add_executor_job(self.alarm.save)
|
||||||
except (OSError, SoCoException, SoCoUPnPException) as exc:
|
except (OSError, SoCoException, SoCoUPnPException) as exc:
|
||||||
_LOGGER.error("Could not update %s: %s", self.entity_id, exc)
|
_LOGGER.error("Could not update %s: %s", self.entity_id, exc)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_migrate_speech_enhancement_entity_unique_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
speaker: SonosSpeaker,
|
||||||
|
) -> None:
|
||||||
|
"""Migrate Speech Enhancement switch entity unique_id."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
registry_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
speech_enhancement_entries = [
|
||||||
|
entry
|
||||||
|
for entry in registry_entries
|
||||||
|
if entry.domain == Platform.SWITCH
|
||||||
|
and entry.original_icon == FEATURE_ICONS[ATTR_SPEECH_ENHANCEMENT]
|
||||||
|
and entry.unique_id.startswith(speaker.soco.uid)
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(speech_enhancement_entries) > 1:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Migration of Speech Enhancement switches on %s failed, manual cleanup required: %s",
|
||||||
|
speaker.zone_name,
|
||||||
|
[e.entity_id for e in speech_enhancement_entries],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(speech_enhancement_entries) == 1:
|
||||||
|
old_entry = speech_enhancement_entries[0]
|
||||||
|
if old_entry.unique_id.endswith("dialog_level"):
|
||||||
|
return
|
||||||
|
|
||||||
|
new_unique_id = f"{speaker.soco.uid}-{ATTR_SPEECH_ENHANCEMENT}"
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating unique_id for %s from %s to %s",
|
||||||
|
old_entry.entity_id,
|
||||||
|
old_entry.unique_id,
|
||||||
|
new_unique_id,
|
||||||
|
)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
old_entry.entity_id, new_unique_id=new_unique_id
|
||||||
|
)
|
||||||
|
@ -32,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
||||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
|
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode
|
||||||
|
|
||||||
TUYA_HVAC_TO_HA = {
|
TUYA_HVAC_TO_HA = {
|
||||||
"auto": HVAC_MODE_HEAT_COOL,
|
"auto": HVAC_MODE_HEAT_COOL,
|
||||||
@ -182,10 +182,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
# it to define min, max & step temperatures
|
# it to define min, max & step temperatures
|
||||||
if (
|
if (
|
||||||
self._set_temperature_dpcode
|
self._set_temperature_dpcode
|
||||||
and self._set_temperature_dpcode in device.status_range
|
and self._set_temperature_dpcode in device.function
|
||||||
):
|
):
|
||||||
type_data = IntegerTypeData.from_json(
|
type_data = IntegerTypeData.from_json(
|
||||||
device.status_range[self._set_temperature_dpcode].values
|
device.function[self._set_temperature_dpcode].values
|
||||||
)
|
)
|
||||||
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
|
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||||
self._set_temperature_type = type_data
|
self._set_temperature_type = type_data
|
||||||
@ -232,14 +232,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Determine dpcode to use for setting the humidity
|
# Determine dpcode to use for setting the humidity
|
||||||
if (
|
if DPCode.HUMIDITY_SET in device.function:
|
||||||
DPCode.HUMIDITY_SET in device.status
|
|
||||||
and DPCode.HUMIDITY_SET in device.status_range
|
|
||||||
):
|
|
||||||
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
|
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
|
||||||
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
|
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
|
||||||
type_data = IntegerTypeData.from_json(
|
type_data = IntegerTypeData.from_json(
|
||||||
device.status_range[DPCode.HUMIDITY_SET].values
|
device.function[DPCode.HUMIDITY_SET].values
|
||||||
)
|
)
|
||||||
self._set_humidity_type = type_data
|
self._set_humidity_type = type_data
|
||||||
self._attr_min_humidity = int(type_data.min_scaled)
|
self._attr_min_humidity = int(type_data.min_scaled)
|
||||||
@ -298,6 +295,21 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
if DPCode.SWITCH_VERTICAL in device.function:
|
if DPCode.SWITCH_VERTICAL in device.function:
|
||||||
self._attr_swing_modes.append(SWING_VERTICAL)
|
self._attr_swing_modes.append(SWING_VERTICAL)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Call when entity is added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
# Log unknown modes
|
||||||
|
if DPCode.MODE in self.device.function:
|
||||||
|
data_type = EnumTypeData.from_json(self.device.function[DPCode.MODE].values)
|
||||||
|
for tuya_mode in data_type.range:
|
||||||
|
if tuya_mode not in TUYA_HVAC_TO_HA:
|
||||||
|
LOGGER.warning(
|
||||||
|
"Unknown HVAC mode '%s' for device %s; assuming it as off",
|
||||||
|
tuya_mode,
|
||||||
|
self.device.name,
|
||||||
|
)
|
||||||
|
|
||||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
def set_hvac_mode(self, hvac_mode: str) -> None:
|
||||||
"""Set new target hvac mode."""
|
"""Set new target hvac mode."""
|
||||||
commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVAC_MODE_OFF}]
|
commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVAC_MODE_OFF}]
|
||||||
@ -436,8 +448,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
return self.entity_description.switch_only_hvac_mode
|
return self.entity_description.switch_only_hvac_mode
|
||||||
return HVAC_MODE_OFF
|
return HVAC_MODE_OFF
|
||||||
|
|
||||||
if self.device.status.get(DPCode.MODE) is not None:
|
if (
|
||||||
return TUYA_HVAC_TO_HA[self.device.status[DPCode.MODE]]
|
mode := self.device.status.get(DPCode.MODE)
|
||||||
|
) is not None and mode in TUYA_HVAC_TO_HA:
|
||||||
|
return TUYA_HVAC_TO_HA[mode]
|
||||||
|
|
||||||
return HVAC_MODE_OFF
|
return HVAC_MODE_OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
import logging
|
||||||
|
|
||||||
from tuya_iot import TuyaCloudOpenAPIEndpoint
|
from tuya_iot import TuyaCloudOpenAPIEndpoint
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DOMAIN = "tuya"
|
DOMAIN = "tuya"
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
CONF_AUTH_TYPE = "auth_type"
|
CONF_AUTH_TYPE = "auth_type"
|
||||||
CONF_PROJECT_TYPE = "tuya_project_type"
|
CONF_PROJECT_TYPE = "tuya_project_type"
|
||||||
|
@ -160,9 +160,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
|||||||
return self.ha_preset_modes
|
return self.ha_preset_modes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_mode(self) -> str:
|
def preset_mode(self) -> str | None:
|
||||||
"""Return the current preset_mode."""
|
"""Return the current preset_mode."""
|
||||||
return self.device.status[DPCode.MODE]
|
return self.device.status.get(DPCode.MODE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percentage(self) -> int | None:
|
def percentage(self) -> int | None:
|
||||||
|
@ -405,7 +405,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
|||||||
if self._brightness_dpcode:
|
if self._brightness_dpcode:
|
||||||
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||||
self._brightness_type = IntegerTypeData.from_json(
|
self._brightness_type = IntegerTypeData.from_json(
|
||||||
device.status_range[self._brightness_dpcode].values
|
device.function[self._brightness_dpcode].values
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if min/max capable
|
# Check if min/max capable
|
||||||
@ -416,17 +416,17 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
|||||||
and description.brightness_min in device.function
|
and description.brightness_min in device.function
|
||||||
):
|
):
|
||||||
self._brightness_max_type = IntegerTypeData.from_json(
|
self._brightness_max_type = IntegerTypeData.from_json(
|
||||||
device.status_range[description.brightness_max].values
|
device.function[description.brightness_max].values
|
||||||
)
|
)
|
||||||
self._brightness_min_type = IntegerTypeData.from_json(
|
self._brightness_min_type = IntegerTypeData.from_json(
|
||||||
device.status_range[description.brightness_min].values
|
device.function[description.brightness_min].values
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update internals based on found color temperature dpcode
|
# Update internals based on found color temperature dpcode
|
||||||
if self._color_temp_dpcode:
|
if self._color_temp_dpcode:
|
||||||
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
|
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
|
||||||
self._color_temp_type = IntegerTypeData.from_json(
|
self._color_temp_type = IntegerTypeData.from_json(
|
||||||
device.status_range[self._color_temp_dpcode].values
|
device.function[self._color_temp_dpcode].values
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update internals based on found color data dpcode
|
# Update internals based on found color data dpcode
|
||||||
|
@ -727,15 +727,15 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
|||||||
# We cannot have a device class, if the UOM isn't set or the
|
# We cannot have a device class, if the UOM isn't set or the
|
||||||
# device class cannot be found in the validation mapping.
|
# device class cannot be found in the validation mapping.
|
||||||
if (
|
if (
|
||||||
self.unit_of_measurement is None
|
self.native_unit_of_measurement is None
|
||||||
or self.device_class not in DEVICE_CLASS_UNITS
|
or self.device_class not in DEVICE_CLASS_UNITS
|
||||||
):
|
):
|
||||||
self._attr_device_class = None
|
self._attr_device_class = None
|
||||||
return
|
return
|
||||||
|
|
||||||
uoms = DEVICE_CLASS_UNITS[self.device_class]
|
uoms = DEVICE_CLASS_UNITS[self.device_class]
|
||||||
self._uom = uoms.get(self.unit_of_measurement) or uoms.get(
|
self._uom = uoms.get(self.native_unit_of_measurement) or uoms.get(
|
||||||
self.unit_of_measurement.lower()
|
self.native_unit_of_measurement.lower()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Unknown unit of measurement, device class should not be used.
|
# Unknown unit of measurement, device class should not be used.
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 12
|
MINOR_VERSION: Final = 12
|
||||||
PATCH_VERSION: Final = "6"
|
PATCH_VERSION: Final = "7"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -16,7 +16,7 @@ ciso8601==2.2.0
|
|||||||
cryptography==35.0.0
|
cryptography==35.0.0
|
||||||
emoji==1.5.0
|
emoji==1.5.0
|
||||||
hass-nabucasa==0.50.0
|
hass-nabucasa==0.50.0
|
||||||
home-assistant-frontend==20211227.0
|
home-assistant-frontend==20211229.0
|
||||||
httpx==0.21.0
|
httpx==0.21.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.0.3
|
jinja2==3.0.3
|
||||||
|
@ -187,7 +187,7 @@ aiohomekit==0.6.4
|
|||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.hue
|
# homeassistant.components.hue
|
||||||
aiohue==3.0.10
|
aiohue==3.0.11
|
||||||
|
|
||||||
# homeassistant.components.imap
|
# homeassistant.components.imap
|
||||||
aioimaplib==0.9.0
|
aioimaplib==0.9.0
|
||||||
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
|
|||||||
flipr-api==1.4.1
|
flipr-api==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux_led==0.27.13
|
flux_led==0.27.21
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
@ -820,7 +820,7 @@ hole==0.7.0
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211227.0
|
home-assistant-frontend==20211229.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -1929,7 +1929,7 @@ python-qbittorrent==0.4.2
|
|||||||
python-ripple-api==0.0.3
|
python-ripple-api==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.28
|
python-smarttub==0.0.29
|
||||||
|
|
||||||
# homeassistant.components.sochain
|
# homeassistant.components.sochain
|
||||||
python-sochain-api==0.0.2
|
python-sochain-api==0.0.2
|
||||||
|
@ -131,7 +131,7 @@ aiohomekit==0.6.4
|
|||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
|
|
||||||
# homeassistant.components.hue
|
# homeassistant.components.hue
|
||||||
aiohue==3.0.10
|
aiohue==3.0.11
|
||||||
|
|
||||||
# homeassistant.components.apache_kafka
|
# homeassistant.components.apache_kafka
|
||||||
aiokafka==0.6.0
|
aiokafka==0.6.0
|
||||||
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
|||||||
flipr-api==1.4.1
|
flipr-api==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux_led==0.27.13
|
flux_led==0.27.21
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
@ -515,7 +515,7 @@ hole==0.7.0
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211227.0
|
home-assistant-frontend==20211229.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -1157,7 +1157,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0
|
|||||||
python-picnic-api==1.1.0
|
python-picnic-api==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.smarttub
|
# homeassistant.components.smarttub
|
||||||
python-smarttub==0.0.28
|
python-smarttub==0.0.29
|
||||||
|
|
||||||
# homeassistant.components.songpal
|
# homeassistant.components.songpal
|
||||||
python-songpal==0.12
|
python-songpal==0.12
|
||||||
|
@ -121,7 +121,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
|
|||||||
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
|
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
|
||||||
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
|
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
|
||||||
|
|
||||||
# test again with sending flash/alert
|
# test again with sending long flash
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
"turn_on",
|
"turn_on",
|
||||||
@ -129,9 +129,37 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert len(mock_bridge_v2.mock_requests) == 3
|
assert len(mock_bridge_v2.mock_requests) == 3
|
||||||
assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True
|
|
||||||
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
|
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
|
||||||
|
|
||||||
|
# test again with sending short flash
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": test_light_id, "flash": "short"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 4
|
||||||
|
assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify"
|
||||||
|
|
||||||
|
# test again with sending a colortemperature which is out of range
|
||||||
|
# which should be normalized to the upper/lower bounds Hue can handle
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": test_light_id, "color_temp": 50},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 5
|
||||||
|
assert mock_bridge_v2.mock_requests[4]["json"]["color_temperature"]["mirek"] == 153
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": test_light_id, "color_temp": 550},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 6
|
||||||
|
assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500
|
||||||
|
|
||||||
|
|
||||||
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
|
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
|
||||||
"""Test calling the turn off service on a light."""
|
"""Test calling the turn off service on a light."""
|
||||||
@ -177,6 +205,26 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da
|
|||||||
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False
|
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False
|
||||||
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
|
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
|
||||||
|
|
||||||
|
# test again with sending long flash
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_off",
|
||||||
|
{"entity_id": test_light_id, "flash": "long"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 3
|
||||||
|
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
|
||||||
|
|
||||||
|
# test again with sending short flash
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_off",
|
||||||
|
{"entity_id": test_light_id, "flash": "short"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 4
|
||||||
|
assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify"
|
||||||
|
|
||||||
|
|
||||||
async def test_light_added(hass, mock_bridge_v2):
|
async def test_light_added(hass, mock_bridge_v2):
|
||||||
"""Test new light added to bridge."""
|
"""Test new light added to bridge."""
|
||||||
@ -386,3 +434,65 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
|||||||
assert (
|
assert (
|
||||||
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200
|
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test sending short flash effect to a grouped light
|
||||||
|
mock_bridge_v2.mock_requests.clear()
|
||||||
|
test_light_id = "light.test_zone"
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": test_light_id,
|
||||||
|
"flash": "short",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PUT request should have been sent to ALL group lights with correct params
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 3
|
||||||
|
for index in range(0, 3):
|
||||||
|
assert (
|
||||||
|
mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"]
|
||||||
|
== "identify"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test sending long flash effect to a grouped light
|
||||||
|
mock_bridge_v2.mock_requests.clear()
|
||||||
|
test_light_id = "light.test_zone"
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": test_light_id,
|
||||||
|
"flash": "long",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PUT request should have been sent to ALL group lights with correct params
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 3
|
||||||
|
for index in range(0, 3):
|
||||||
|
assert (
|
||||||
|
mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test sending flash effect in turn_off call
|
||||||
|
mock_bridge_v2.mock_requests.clear()
|
||||||
|
test_light_id = "light.test_zone"
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": test_light_id,
|
||||||
|
"flash": "short",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PUT request should have been sent to ALL group lights with correct params
|
||||||
|
assert len(mock_bridge_v2.mock_requests) == 3
|
||||||
|
for index in range(0, 3):
|
||||||
|
assert (
|
||||||
|
mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"]
|
||||||
|
== "identify"
|
||||||
|
)
|
||||||
|
@ -7,6 +7,7 @@ HOST = "1.1.1.1"
|
|||||||
MAC = "01:23:45:67:89:ab"
|
MAC = "01:23:45:67:89:ab"
|
||||||
|
|
||||||
HW_ID = 123456789
|
HW_ID = 123456789
|
||||||
|
ID_HEX = "75BCD15"
|
||||||
|
|
||||||
MOCK_INFO = {"ids": {"hardwareId": HW_ID}}
|
MOCK_INFO = {"ids": {"hardwareId": HW_ID}}
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ async def setup_nuki_integration(hass):
|
|||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain="nuki",
|
domain="nuki",
|
||||||
unique_id=HW_ID,
|
unique_id=ID_HEX,
|
||||||
data={"host": HOST, "port": 8080, "token": "test-token"},
|
data={"host": HOST, "port": 8080, "token": "test-token"},
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
@ -41,7 +41,7 @@ async def test_form(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result2["title"] == 123456789
|
assert result2["title"] == "75BCD15"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
@ -69,7 +69,7 @@ async def test_import(hass):
|
|||||||
data={"host": "1.1.1.1", "port": 8080, "token": "test-token"},
|
data={"host": "1.1.1.1", "port": 8080, "token": "test-token"},
|
||||||
)
|
)
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["title"] == 123456789
|
assert result["title"] == "75BCD15"
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
@ -204,7 +204,7 @@ async def test_dhcp_flow(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result2["title"] == 123456789
|
assert result2["title"] == "75BCD15"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"host": "1.1.1.1",
|
"host": "1.1.1.1",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user