mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.27.13"],
|
||||
"requirements": ["flux_led==0.27.21"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch"],
|
||||
"iot_class": "local_push",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20211227.0"
|
||||
"home-assistant-frontend==20211229.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Philips Hue",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||
"requirements": ["aiohue==3.0.10"],
|
||||
"requirements": ["aiohue==3.0.11"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for Hue groups (room/zone)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
@ -18,6 +19,7 @@ from homeassistant.components.light import (
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_ONOFF,
|
||||
COLOR_MODE_XY,
|
||||
FLASH_SHORT,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
@ -29,14 +31,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from ..bridge import HueBridge
|
||||
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
|
||||
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 = [
|
||||
"device (groupedLight) has communication issues, 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) 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."""
|
||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||
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))
|
||||
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
|
||||
# To set other features, you'll have to control the attached lights
|
||||
if (
|
||||
@ -173,22 +183,32 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
|
||||
# redirect all other feature commands to underlying lights
|
||||
# note that this silently ignores params sent to light that are not supported
|
||||
for light in self.controller.get_lights(self.resource.id):
|
||||
await self.bridge.async_request_call(
|
||||
self.api.lights.set_state,
|
||||
light.id,
|
||||
on=True,
|
||||
brightness=brightness if light.supports_dimming else None,
|
||||
color_xy=xy_color if light.supports_color else None,
|
||||
color_temp=color_temp if light.supports_color_temperature else None,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.bridge.async_request_call(
|
||||
self.api.lights.set_state,
|
||||
light.id,
|
||||
on=True,
|
||||
brightness=brightness if light.supports_dimming else None,
|
||||
color_xy=xy_color if light.supports_color else None,
|
||||
color_temp=color_temp if light.supports_color_temperature else None,
|
||||
transition_time=transition,
|
||||
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:
|
||||
"""Turn the light off."""
|
||||
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
|
||||
# To set other features, you'll have to control the attached lights
|
||||
@ -202,14 +222,31 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
return
|
||||
|
||||
# redirect all other feature commands to underlying lights
|
||||
for light in self.controller.get_lights(self.resource.id):
|
||||
await self.bridge.async_request_call(
|
||||
self.api.lights.set_state,
|
||||
light.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.bridge.async_request_call(
|
||||
self.api.lights.set_state,
|
||||
light.id,
|
||||
on=False,
|
||||
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
|
||||
def on_update(self) -> None:
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""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."""
|
||||
if brightness is not None:
|
||||
# Hue uses a range of [0, 100] to control brightness.
|
||||
@ -10,10 +11,19 @@ def normalize_hue_brightness(brightness):
|
||||
return brightness
|
||||
|
||||
|
||||
def normalize_hue_transition(transition):
|
||||
def normalize_hue_transition(transition: float | None) -> float | None:
|
||||
"""Return rounded transition values."""
|
||||
if transition is not None:
|
||||
# hue transition duration is in milliseconds and round them to 100ms
|
||||
transition = int(round(transition, 1) * 1000)
|
||||
|
||||
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.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.core import callback
|
||||
@ -27,11 +27,6 @@ async def async_setup_hue_events(bridge: "HueBridge"):
|
||||
api: HueBridgeV2 = bridge.api # to satisfy typing
|
||||
conf_entry = bridge.config_entry
|
||||
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
|
||||
btn_controller = api.sensors.button
|
||||
@ -45,26 +40,16 @@ async def async_setup_hue_events(bridge: "HueBridge"):
|
||||
if hue_resource.button is None:
|
||||
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)
|
||||
device = dev_reg.async_get_device({(DOMAIN, hue_device.id)})
|
||||
|
||||
# Fire event
|
||||
data = {
|
||||
# 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_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,
|
||||
}
|
||||
hass.bus.async_fire(ATTR_HUE_EVENT, data)
|
||||
|
@ -6,7 +6,6 @@ from typing import Any
|
||||
from aiohue import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.lights import LightsController
|
||||
from aiohue.v2.models.feature import AlertEffectType
|
||||
from aiohue.v2.models.light import Light
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@ -19,6 +18,7 @@ from homeassistant.components.light import (
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_ONOFF,
|
||||
COLOR_MODE_XY,
|
||||
FLASH_SHORT,
|
||||
SUPPORT_FLASH,
|
||||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
@ -30,12 +30,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from ..bridge import HueBridge
|
||||
from ..const import DOMAIN
|
||||
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 = [
|
||||
"device (light) has communication issues, 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:
|
||||
"""Initialize the light."""
|
||||
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.controller = controller
|
||||
self._supported_color_modes = set()
|
||||
@ -158,10 +162,18 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
"""Turn the device on."""
|
||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||
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))
|
||||
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(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
@ -170,7 +182,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color,
|
||||
color_temp=color_temp,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
|
||||
@ -179,11 +190,25 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
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 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(
|
||||
self.controller.set_state,
|
||||
id=self.resource.id,
|
||||
on=False,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
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,
|
||||
ERROR_STATES,
|
||||
)
|
||||
from .helpers import parse_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -80,6 +81,14 @@ async def async_setup_entry(hass, entry):
|
||||
|
||||
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:
|
||||
bridge = await hass.async_add_executor_job(
|
||||
NukiBridge,
|
||||
|
@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
|
||||
from .helpers import parse_id
|
||||
|
||||
_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:
|
||||
"""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()
|
||||
|
||||
@ -114,7 +115,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
|
||||
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:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=conf)
|
||||
self.hass.async_create_task(
|
||||
@ -143,11 +146,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
|
||||
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()
|
||||
return self.async_create_entry(
|
||||
title=info["ids"]["hardwareId"], data=user_input
|
||||
)
|
||||
return self.async_create_entry(title=bridge_id, data=user_input)
|
||||
|
||||
data_schema = self.discovery_schema or USER_SCHEMA
|
||||
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",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@mdz"],
|
||||
"requirements": ["python-smarttub==0.0.28"],
|
||||
"requirements": ["python-smarttub==0.0.29"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
@ -482,7 +482,7 @@ class SonosSpeaker:
|
||||
|
||||
for bool_var in (
|
||||
"dialog_level",
|
||||
"night_level",
|
||||
"night_mode",
|
||||
"sub_enabled",
|
||||
"surround_enabled",
|
||||
):
|
||||
|
@ -7,8 +7,9 @@ import logging
|
||||
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException
|
||||
|
||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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.dispatcher import async_dispatcher_connect
|
||||
|
||||
@ -113,6 +114,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
available_soco_attributes, speaker
|
||||
)
|
||||
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(
|
||||
"Creating %s switch on %s",
|
||||
FRIENDLY_NAMES[feature_type],
|
||||
@ -344,3 +349,48 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
||||
await self.hass.async_add_executor_job(self.alarm.save)
|
||||
except (OSError, SoCoException, SoCoUPnPException) as 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 .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 = {
|
||||
"auto": HVAC_MODE_HEAT_COOL,
|
||||
@ -182,10 +182,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
# it to define min, max & step temperatures
|
||||
if (
|
||||
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(
|
||||
device.status_range[self._set_temperature_dpcode].values
|
||||
device.function[self._set_temperature_dpcode].values
|
||||
)
|
||||
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||
self._set_temperature_type = type_data
|
||||
@ -232,14 +232,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
# Determine dpcode to use for setting the humidity
|
||||
if (
|
||||
DPCode.HUMIDITY_SET in device.status
|
||||
and DPCode.HUMIDITY_SET in device.status_range
|
||||
):
|
||||
if DPCode.HUMIDITY_SET in device.function:
|
||||
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
|
||||
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
|
||||
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._attr_min_humidity = int(type_data.min_scaled)
|
||||
@ -298,6 +295,21 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
if DPCode.SWITCH_VERTICAL in device.function:
|
||||
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:
|
||||
"""Set new target hvac mode."""
|
||||
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 HVAC_MODE_OFF
|
||||
|
||||
if self.device.status.get(DPCode.MODE) is not None:
|
||||
return TUYA_HVAC_TO_HA[self.device.status[DPCode.MODE]]
|
||||
if (
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
from tuya_iot import TuyaCloudOpenAPIEndpoint
|
||||
|
||||
@ -38,6 +39,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
DOMAIN = "tuya"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_AUTH_TYPE = "auth_type"
|
||||
CONF_PROJECT_TYPE = "tuya_project_type"
|
||||
|
@ -160,9 +160,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
||||
return self.ha_preset_modes
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset_mode."""
|
||||
return self.device.status[DPCode.MODE]
|
||||
return self.device.status.get(DPCode.MODE)
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
|
@ -405,7 +405,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||
if self._brightness_dpcode:
|
||||
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||
self._brightness_type = IntegerTypeData.from_json(
|
||||
device.status_range[self._brightness_dpcode].values
|
||||
device.function[self._brightness_dpcode].values
|
||||
)
|
||||
|
||||
# Check if min/max capable
|
||||
@ -416,17 +416,17 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||
and description.brightness_min in device.function
|
||||
):
|
||||
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(
|
||||
device.status_range[description.brightness_min].values
|
||||
device.function[description.brightness_min].values
|
||||
)
|
||||
|
||||
# Update internals based on found color temperature dpcode
|
||||
if self._color_temp_dpcode:
|
||||
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
|
||||
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
|
||||
|
@ -727,15 +727,15 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
# We cannot have a device class, if the UOM isn't set or the
|
||||
# device class cannot be found in the validation mapping.
|
||||
if (
|
||||
self.unit_of_measurement is None
|
||||
self.native_unit_of_measurement is None
|
||||
or self.device_class not in DEVICE_CLASS_UNITS
|
||||
):
|
||||
self._attr_device_class = None
|
||||
return
|
||||
|
||||
uoms = DEVICE_CLASS_UNITS[self.device_class]
|
||||
self._uom = uoms.get(self.unit_of_measurement) or uoms.get(
|
||||
self.unit_of_measurement.lower()
|
||||
self._uom = uoms.get(self.native_unit_of_measurement) or uoms.get(
|
||||
self.native_unit_of_measurement.lower()
|
||||
)
|
||||
|
||||
# Unknown unit of measurement, device class should not be used.
|
||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "6"
|
||||
PATCH_VERSION: Final = "7"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -16,7 +16,7 @@ ciso8601==2.2.0
|
||||
cryptography==35.0.0
|
||||
emoji==1.5.0
|
||||
hass-nabucasa==0.50.0
|
||||
home-assistant-frontend==20211227.0
|
||||
home-assistant-frontend==20211229.0
|
||||
httpx==0.21.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.3
|
||||
|
@ -187,7 +187,7 @@ aiohomekit==0.6.4
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==3.0.10
|
||||
aiohue==3.0.11
|
||||
|
||||
# homeassistant.components.imap
|
||||
aioimaplib==0.9.0
|
||||
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.13
|
||||
flux_led==0.27.21
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -820,7 +820,7 @@ hole==0.7.0
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211227.0
|
||||
home-assistant-frontend==20211229.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1929,7 +1929,7 @@ python-qbittorrent==0.4.2
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.28
|
||||
python-smarttub==0.0.29
|
||||
|
||||
# homeassistant.components.sochain
|
||||
python-sochain-api==0.0.2
|
||||
|
@ -131,7 +131,7 @@ aiohomekit==0.6.4
|
||||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==3.0.10
|
||||
aiohue==3.0.11
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.6.0
|
||||
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.13
|
||||
flux_led==0.27.21
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -515,7 +515,7 @@ hole==0.7.0
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211227.0
|
||||
home-assistant-frontend==20211229.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1157,7 +1157,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0
|
||||
python-picnic-api==1.1.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.28
|
||||
python-smarttub==0.0.29
|
||||
|
||||
# homeassistant.components.songpal
|
||||
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"]["dynamics"]["duration"] == 200
|
||||
|
||||
# test again with sending flash/alert
|
||||
# test again with sending long flash
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
@ -129,9 +129,37 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
|
||||
blocking=True,
|
||||
)
|
||||
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"
|
||||
|
||||
# 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):
|
||||
"""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"]["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):
|
||||
"""Test new light added to bridge."""
|
||||
@ -386,3 +434,65 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
|
||||
assert (
|
||||
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"
|
||||
|
||||
HW_ID = 123456789
|
||||
ID_HEX = "75BCD15"
|
||||
|
||||
MOCK_INFO = {"ids": {"hardwareId": HW_ID}}
|
||||
|
||||
@ -16,7 +17,7 @@ async def setup_nuki_integration(hass):
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain="nuki",
|
||||
unique_id=HW_ID,
|
||||
unique_id=ID_HEX,
|
||||
data={"host": HOST, "port": 8080, "token": "test-token"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
@ -41,7 +41,7 @@ async def test_form(hass):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == 123456789
|
||||
assert result2["title"] == "75BCD15"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"port": 8080,
|
||||
@ -69,7 +69,7 @@ async def test_import(hass):
|
||||
data={"host": "1.1.1.1", "port": 8080, "token": "test-token"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == 123456789
|
||||
assert result["title"] == "75BCD15"
|
||||
assert result["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"port": 8080,
|
||||
@ -204,7 +204,7 @@ async def test_dhcp_flow(hass):
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == 123456789
|
||||
assert result2["title"] == "75BCD15"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"port": 8080,
|
||||
|
Loading…
x
Reference in New Issue
Block a user