mirror of
https://github.com/home-assistant/core.git
synced 2026-05-24 17:55:24 +02:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0723d8d83f | |||
| 73c9edd3e8 | |||
| 18f30bd97b | |||
| eae6e79b61 | |||
| 5bb42801d9 | |||
| 98271265d3 | |||
| 92d20477bc | |||
| 9352a0057e | |||
| 5fb874277a | |||
| d65f605398 | |||
| 7e5b448f70 | |||
| ef5da5ef36 | |||
| 410f00c4ed | |||
| 33c205dc04 | |||
| 267b3e279d | |||
| 9c1cd8093d | |||
| 201c0c2470 | |||
| 281d6e0e8b | |||
| 88746534a4 | |||
| 135f91c3c5 | |||
| 49d8dc88d9 | |||
| a7a2c1eb02 | |||
| 6596f956d2 | |||
| 9d8859833b | |||
| 65a4c10660 | |||
| 1737b50558 | |||
| 614c7006f6 | |||
| 8c901cc405 | |||
| 5d0fdfd38b | |||
| c9ed57bc56 | |||
| 0e0901993d | |||
| 54aba11091 | |||
| dc9116a7a7 | |||
| 1e90882918 | |||
| e8295e14b1 | |||
| 7ebaaf129a | |||
| ee734dede6 | |||
| ebc582c813 | |||
| 311e5a9bd2 | |||
| cd6c3c878b | |||
| 51589ec2ff | |||
| 8e1a04dc82 | |||
| 6b15f9a2ec | |||
| 8d66752556 | |||
| 266767e37d | |||
| d39775ac34 | |||
| a314f7bf64 | |||
| 37478d33eb | |||
| 5a76f3bd19 | |||
| 17e105083e | |||
| db8589b2bc | |||
| 771b016f33 | |||
| 0bc0745e8c | |||
| ea084797d3 | |||
| 2456753caf | |||
| 070de13c14 | |||
| 5e45f37ee6 | |||
| 4a96880f51 | |||
| 228ac01124 | |||
| d366027e6b | |||
| 2f35ad2a8a | |||
| 95cc9aed64 | |||
| 37d6449a49 | |||
| 249b5435d9 | |||
| 3293ebcea5 | |||
| 47d8adc77c | |||
| 356e6a691b | |||
| b26c2f3854 | |||
| 0830988687 |
@@ -11,7 +11,7 @@
|
||||
"service": "mdi:dialpad"
|
||||
},
|
||||
"alarm_toggle_chime": {
|
||||
"service": "mdi:abc"
|
||||
"service": "mdi:bell-ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Invalid authentication credentials: {error}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ from pyatv.interface import AppleTV, KeyboardListener
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -23,23 +23,33 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Load Apple TV binary sensor based on a config entry."""
|
||||
# apple_tv config entries always have a unique id
|
||||
manager = config_entry.runtime_data
|
||||
cb: CALLBACK_TYPE
|
||||
added = False
|
||||
|
||||
@callback
|
||||
def setup_entities(atv: AppleTV) -> None:
|
||||
nonlocal added
|
||||
if added:
|
||||
return
|
||||
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
|
||||
assert config_entry.unique_id is not None
|
||||
name: str = config_entry.data[CONF_NAME]
|
||||
async_add_entities(
|
||||
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
|
||||
)
|
||||
cb()
|
||||
added = True
|
||||
|
||||
cb = async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(cb)
|
||||
|
||||
# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
|
||||
# before this platform was forwarded, in which case the signal above was
|
||||
# missed; handle that case directly.
|
||||
if manager.atv is not None:
|
||||
setup_entities(manager.atv)
|
||||
|
||||
|
||||
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
|
||||
|
||||
@@ -13,3 +13,7 @@ UNKNOWN = "unknown"
|
||||
|
||||
DEFAULT_HOST = "192.168.0.2"
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
|
||||
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
|
||||
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
import blebox_uniapi.light
|
||||
@@ -22,9 +23,9 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from . import BleBoxConfigEntry
|
||||
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -59,8 +60,8 @@ COLOR_MODE_MAP = {
|
||||
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
"""Representation of BleBox lights."""
|
||||
|
||||
_attr_min_color_temp_kelvin = 2700 # 370 Mireds
|
||||
_attr_max_color_temp_kelvin = 6500 # 154 Mireds
|
||||
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
|
||||
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS
|
||||
|
||||
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
|
||||
"""Initialize a BleBox light."""
|
||||
@@ -78,10 +79,43 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
"""Return the name."""
|
||||
return self._feature.brightness
|
||||
|
||||
def _color_temp_to_native_scale(self, x: int) -> int:
|
||||
"""Convert color temperature from Kelvin to native BleBox scale (0-255).
|
||||
|
||||
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
|
||||
"""
|
||||
scaled = (
|
||||
(self._attr_max_color_temp_kelvin - x)
|
||||
/ (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin)
|
||||
) * 255
|
||||
# note: within the operating temperature range here the Kelvin
|
||||
# scale has less "integer steps" than the native scale used
|
||||
# by blebox devices. Thus we need to use rounding method that is opposite
|
||||
# to the one used in _color_temp_from_native_scale in order to avoid
|
||||
# temperature value jumping by one step when the temperature value is read
|
||||
# back from the device
|
||||
bounded = max(min(math.floor(scaled), 255), 0)
|
||||
return int(bounded)
|
||||
|
||||
def _color_temp_from_native_scale(self, x: int) -> int:
|
||||
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.
|
||||
|
||||
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
|
||||
"""
|
||||
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
|
||||
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
|
||||
)
|
||||
# note: see _color_temp_to_native_scale for explanation of rounding method
|
||||
bounded = max(
|
||||
min(math.ceil(scaled), self._attr_max_color_temp_kelvin),
|
||||
self._attr_min_color_temp_kelvin,
|
||||
)
|
||||
return int(bounded)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int:
|
||||
"""Return the color temperature value in Kelvin."""
|
||||
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
|
||||
return self._color_temp_from_native_scale(self._feature.color_temp)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode:
|
||||
@@ -139,15 +173,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
rgbww = kwargs.get(ATTR_RGBWW_COLOR)
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
|
||||
feature = self._feature
|
||||
value = feature.sensible_on_value
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
|
||||
if rgbw is not None:
|
||||
value = list(rgbw)
|
||||
if color_temp_kelvin is not None:
|
||||
value = feature.return_color_temp_with_brightness(
|
||||
int(color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)),
|
||||
self._color_temp_to_native_scale(color_temp_kelvin),
|
||||
self.brightness,
|
||||
)
|
||||
|
||||
@@ -162,14 +197,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
if brightness is not None:
|
||||
if self.color_mode == ColorMode.COLOR_TEMP:
|
||||
value = feature.return_color_temp_with_brightness(
|
||||
color_util.color_temperature_kelvin_to_mired(
|
||||
self.color_temp_kelvin
|
||||
),
|
||||
self._color_temp_to_native_scale(self.color_temp_kelvin),
|
||||
brightness,
|
||||
)
|
||||
else:
|
||||
value = feature.apply_brightness(value, brightness)
|
||||
|
||||
if isinstance(value, (list, tuple)) and not any(value):
|
||||
await self._feature.async_off()
|
||||
return
|
||||
|
||||
try:
|
||||
await self._feature.async_on(value)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -81,6 +81,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
end=self.to_local(self.get_end_date(vevent)),
|
||||
location=get_attr_value(vevent, "location"),
|
||||
description=get_attr_value(vevent, "description"),
|
||||
uid=get_attr_value(vevent, "uid"),
|
||||
recurrence_id=(
|
||||
str(v)
|
||||
if (v := get_attr_value(vevent, "recurrence_id")) is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -176,6 +182,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
end=self.to_local(self.get_end_date(vevent)),
|
||||
location=get_attr_value(vevent, "location"),
|
||||
description=get_attr_value(vevent, "description"),
|
||||
uid=get_attr_value(vevent, "uid"),
|
||||
recurrence_id=(
|
||||
str(v)
|
||||
if (v := get_attr_value(vevent, "recurrence_id")) is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -70,7 +70,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
@@ -273,7 +273,10 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
if data is None:
|
||||
return None
|
||||
humidity = data.get(CONTROL4_HUMIDITY)
|
||||
return int(humidity) if humidity is not None else None
|
||||
try:
|
||||
return int(humidity) if humidity is not None else None
|
||||
except ValueError, TypeError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
|
||||
@@ -63,7 +63,7 @@ class CurrencylayerSensor(SensorEntity):
|
||||
"""Implementing the Currencylayer sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by currencylayer.com"
|
||||
_attr_icon = "mdi:currency"
|
||||
_attr_icon = "mdi:currency-usd"
|
||||
|
||||
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -4,10 +4,11 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pydaikin.daikin_base import Appliance
|
||||
from pydaikin.exceptions import DaikinException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, TIMEOUT_SEC
|
||||
|
||||
@@ -33,4 +34,11 @@ class DaikinCoordinator(DataUpdateCoordinator[None]):
|
||||
self.device = device
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
await self.device.update_status()
|
||||
try:
|
||||
await self.device.update_status()
|
||||
except DaikinException as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_communicating",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"error_communicating": {
|
||||
"message": "Error communicating with Daikin device: {error}"
|
||||
},
|
||||
"zone_hvac_mode_unsupported": {
|
||||
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==4.0.0"]
|
||||
"requirements": ["aiodns==4.0.4"]
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ async def async_create_upnp_datagram_endpoint(
|
||||
|
||||
ssdp_socket.bind(("" if upnp_bind_multicast else host_ip_addr, BROADCAST_PORT))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
transport_protocol = await loop.create_datagram_endpoint(
|
||||
lambda: UPNPResponderProtocol(loop, ssdp_socket, advertise_ip, advertise_port),
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"service": "mdi:refresh"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"service": "mdi:water-heater"
|
||||
"service": "mdi:water-boiler"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"service": "mdi:pencil"
|
||||
|
||||
@@ -16,7 +16,7 @@ class DeviceType(Enum):
|
||||
GAME_CONSOLE = "mdi:nintendo-game-boy"
|
||||
STREAMING_DONGLE = "mdi:cast"
|
||||
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
|
||||
DISC_PLAYER = "mdi:disk-player"
|
||||
DISC_PLAYER = "mdi:disc-player"
|
||||
REMOTE_CONTROL = "mdi:remote-tv"
|
||||
RADIO = "mdi:radio"
|
||||
PHOTO_CAMERA = PHOTOS = "mdi:camera"
|
||||
|
||||
@@ -470,9 +470,11 @@ class ForkedDaapdMaster(MediaPlayerEntity):
|
||||
return self._player["volume"] == 0
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
def media_content_id(self) -> str | None:
|
||||
"""Content ID of current playing media."""
|
||||
return self._player["item_id"]
|
||||
if (item_id := self._player["item_id"]) == 0:
|
||||
return None
|
||||
return str(item_id)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"entity": {
|
||||
"button": {
|
||||
"sync_clock": {
|
||||
"default": "mdi:clock-sync"
|
||||
"default": "mdi:clock-check"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
||||
@@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bo
|
||||
host=host,
|
||||
port=port,
|
||||
family=model_family,
|
||||
retries=10,
|
||||
retries=3,
|
||||
)
|
||||
except InverterError as err:
|
||||
try:
|
||||
|
||||
@@ -73,8 +73,8 @@ class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Detects the port of the Inverter."""
|
||||
port = GOODWE_UDP_PORT
|
||||
try:
|
||||
inverter = await connect(host=host, port=port, retries=10)
|
||||
inverter = await connect(host=host, port=port, retries=3)
|
||||
except InverterError:
|
||||
port = GOODWE_TCP_PORT
|
||||
inverter = await connect(host=host, port=port, retries=10)
|
||||
inverter = await connect(host=host, port=port, retries=3)
|
||||
return inverter, port
|
||||
|
||||
@@ -114,7 +114,7 @@ class AbstractConfig(ABC):
|
||||
"""Sync entities to Google."""
|
||||
await self.async_sync_entities_all()
|
||||
|
||||
self._on_deinitialize.append(start.async_at_start(self.hass, sync_google))
|
||||
self._on_deinitialize.append(start.async_at_started(self.hass, sync_google))
|
||||
|
||||
@callback
|
||||
def async_deinitialize(self) -> None:
|
||||
|
||||
@@ -209,14 +209,15 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
mix_chart_entries = mix_detail["chartData"]
|
||||
sorted_keys = sorted(mix_chart_entries)
|
||||
|
||||
# Create datetime from the latest entry
|
||||
date_now = dt_util.now().date()
|
||||
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
|
||||
mix_detail["lastdataupdate"] = datetime.datetime.combine(
|
||||
date_now,
|
||||
last_updated_time, # type: ignore[arg-type]
|
||||
dt_util.get_default_time_zone(),
|
||||
)
|
||||
if sorted_keys:
|
||||
# Create datetime from the latest entry
|
||||
date_now = dt_util.now().date()
|
||||
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
|
||||
mix_detail["lastdataupdate"] = datetime.datetime.combine(
|
||||
date_now,
|
||||
last_updated_time, # type: ignore[arg-type]
|
||||
dt_util.get_default_time_zone(),
|
||||
)
|
||||
|
||||
# Dashboard data for mix system
|
||||
dashboard_data = self.api.dashboard_data(self.plant_id)
|
||||
|
||||
@@ -808,10 +808,10 @@ async def _create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa:
|
||||
data["daysOfMonth"] = [start_date.day]
|
||||
data["weeksOfMonth"] = []
|
||||
|
||||
if interval := call.data.get(ATTR_INTERVAL):
|
||||
if (interval := call.data.get(ATTR_INTERVAL)) is not None:
|
||||
data["everyX"] = interval
|
||||
|
||||
if streak := call.data.get(ATTR_STREAK):
|
||||
if (streak := call.data.get(ATTR_STREAK)) is not None:
|
||||
data["streak"] = streak
|
||||
|
||||
try:
|
||||
|
||||
@@ -265,9 +265,9 @@ async def async_attach_trigger( # noqa: C901
|
||||
# entity
|
||||
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
|
||||
to_track.append(TrackEntity(at_time, update_entity_trigger_event))
|
||||
elif isinstance(at_time, dict) and CONF_OFFSET in at_time:
|
||||
# entity with offset
|
||||
entity_id: str = at_time.get(CONF_ENTITY_ID, "")
|
||||
elif isinstance(at_time, dict):
|
||||
# entity with optional offset
|
||||
entity_id: str = at_time[CONF_ENTITY_ID]
|
||||
offset: timedelta = at_time.get(CONF_OFFSET, timedelta(0))
|
||||
update_entity_trigger(
|
||||
entity_id, new_state=hass.states.get(entity_id), offset=offset
|
||||
|
||||
@@ -21,7 +21,9 @@ EXPECTED_ENTRY_VERSION = (
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entries = hass.config_entries.async_entries(
|
||||
DOMAIN, include_ignore=False, include_disabled=False
|
||||
)
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
|
||||
@@ -89,6 +89,8 @@ def async_get_triggers(
|
||||
|
||||
# Get Hue device id from device identifier
|
||||
hue_dev_id = get_hue_device_id(device_entry)
|
||||
if hue_dev_id is None or hue_dev_id not in api.devices:
|
||||
return []
|
||||
# extract triggers from all button resources of this Hue device
|
||||
triggers: list[dict[str, Any]] = []
|
||||
model_id = api.devices[hue_dev_id].product_data.product_name
|
||||
|
||||
@@ -135,6 +135,11 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
"""
|
||||
return self._is_hard_wired
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if shade position data is available."""
|
||||
return super().available and self.positions.primary is not None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
import httpx
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceThrottledException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
|
||||
@@ -46,6 +47,12 @@ class AqualinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
await self.system.update()
|
||||
except AqualinkServiceUnauthorizedException as err:
|
||||
raise ConfigEntryAuthFailed("Invalid credentials for iAquaLink") from err
|
||||
except AqualinkServiceThrottledException:
|
||||
_LOGGER.warning(
|
||||
"Rate limited by iAquaLink system %s, will retry later",
|
||||
self.system.serial,
|
||||
)
|
||||
return
|
||||
except (AqualinkServiceException, httpx.HTTPError) as err:
|
||||
raise UpdateFailed(
|
||||
f"Unable to update iAquaLink system {self.system.serial}: {err}"
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioimmich==0.14.0"]
|
||||
"requirements": ["aioimmich==0.14.1"]
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
|
||||
|
||||
create_insteon_device(hass, devices.modem, entry.entry_id)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
|
||||
|
||||
for address in devices:
|
||||
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
register_new_device_callback(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
create_insteon_device(hass, devices.modem, entry.entry_id)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pyintesishome"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyintesishome==1.8.7"]
|
||||
"requirements": ["pyintesishome==1.8.8"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"erev_shabbat_hag": { "default": "mdi:candle-light" },
|
||||
"erev_shabbat_hag": { "default": "mdi:candle" },
|
||||
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
|
||||
"motzei_shabbat_hag": { "default": "mdi:fire" }
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"service": "mdi:lock-open"
|
||||
},
|
||||
"disable": {
|
||||
"service": "mdi:fash-off"
|
||||
"service": "mdi:flash-off"
|
||||
},
|
||||
"enable": {
|
||||
"service": "mdi:flash"
|
||||
|
||||
@@ -28,25 +28,25 @@
|
||||
"ice_maker": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_bottom_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_middle_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
},
|
||||
"ice_maker_top_zone": {
|
||||
"default": "mdi:cube-outline",
|
||||
"state": {
|
||||
"off": "mdi:cube-outline-off"
|
||||
"off": "mdi:cube-off-outline"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
|
||||
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -133,7 +133,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Fetch the latest battery status from the bridge."""
|
||||
status = await self._smartbridge.get_battery_status(self.device_id)
|
||||
try:
|
||||
status = await self._smartbridge.get_battery_status(self.device_id)
|
||||
except BridgeResponseError:
|
||||
self._attr_is_on = None
|
||||
return
|
||||
normalized_status = status.strip().casefold() if status else None
|
||||
if normalized_status == BATTERY_STATUS_LOW:
|
||||
self._attr_is_on = True
|
||||
|
||||
@@ -5,8 +5,8 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException
|
||||
|
||||
DOMAIN = "lyric"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize"
|
||||
OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token"
|
||||
OAUTH2_AUTHORIZE = "https://api.honeywellhome.com/oauth2/authorize"
|
||||
OAUTH2_TOKEN = "https://api.honeywellhome.com/oauth2/token"
|
||||
|
||||
PRESET_NO_HOLD = "NoHold"
|
||||
PRESET_TEMPORARY_HOLD = "TemporaryHold"
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiolyric"],
|
||||
"requirements": ["aiolyric==2.0.2"]
|
||||
"requirements": ["aiolyric==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -455,12 +455,15 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
|
||||
await super().async_added_to_hass()
|
||||
if state := await self.async_get_last_state():
|
||||
self._state_ts = state.last_updated
|
||||
if next_state := state.attributes.get(ATTR_NEXT_STATE):
|
||||
# If in arming or pending state we record the transition,
|
||||
# not the current state
|
||||
self._state = AlarmControlPanelState(next_state)
|
||||
else:
|
||||
self._state = AlarmControlPanelState(state.state)
|
||||
try:
|
||||
if next_state := state.attributes.get(ATTR_NEXT_STATE):
|
||||
# If in arming or pending state we record the transition,
|
||||
# not the current state
|
||||
self._state = AlarmControlPanelState(next_state)
|
||||
else:
|
||||
self._state = AlarmControlPanelState(state.state)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
|
||||
self._previous_state = prev_state
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"default": "mdi:home-lightning-bolt"
|
||||
},
|
||||
"eve_weather_trend": {
|
||||
"default": "mdi:weather",
|
||||
"default": "mdi:weather-cloudy",
|
||||
"state": {
|
||||
"cloudy": "mdi:weather-cloudy",
|
||||
"rainy": "mdi:weather-rainy",
|
||||
|
||||
@@ -511,7 +511,9 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
|
||||
tall_items = 17, 42
|
||||
glasses_warm = 19
|
||||
quick_intense = 21
|
||||
normal = 30
|
||||
normal = 23, 30
|
||||
pre_wash = 24
|
||||
pot_rests_and_filters = 25
|
||||
power_wash = 44, 204
|
||||
comfort_wash = 203
|
||||
comfort_wash_plus = 209
|
||||
|
||||
@@ -709,6 +709,7 @@
|
||||
"pork_tenderloin_medaillons_4_cm": "Pork tenderloin (medaillons, 4 cm)",
|
||||
"pork_tenderloin_medaillons_5_cm": "Pork tenderloin (medaillons, 5 cm)",
|
||||
"pork_with_crackling": "Pork with crackling",
|
||||
"pot_rests_and_filters": "Pot rests and filters",
|
||||
"potato_cheese_gratin": "Potato cheese gratin",
|
||||
"potato_dumplings_half_half_boil_in_bag": "Potato dumplings (half/half, boil-in-bag)",
|
||||
"potato_dumplings_half_half_deep_frozen": "Potato dumplings (half/half, deep-frozen)",
|
||||
@@ -751,6 +752,7 @@
|
||||
"powerfresh": "PowerFresh",
|
||||
"prawns": "Prawns",
|
||||
"pre_ironing": "Pre-ironing",
|
||||
"pre_wash": "Pre-wash",
|
||||
"proofing": "Proofing",
|
||||
"prove_15_min": "Prove for 15 min",
|
||||
"prove_30_min": "Prove for 30 min",
|
||||
|
||||
@@ -597,8 +597,8 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
)
|
||||
)
|
||||
|
||||
if "reasoning" not in model_args:
|
||||
# Reasoning models handle this correctly with just a prompt
|
||||
if not model_args["model"].startswith("o"):
|
||||
# o-series models handle this correctly with just a prompt
|
||||
remove_citations = True
|
||||
|
||||
tools.append(web_search)
|
||||
|
||||
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
|
||||
return self.async_abort(reason="incomplete_discovery")
|
||||
|
||||
_LOGGER.debug(
|
||||
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
|
||||
)
|
||||
udn = discovery_info.upnp[ATTR_UPNP_UDN]
|
||||
if isinstance(udn, list):
|
||||
if not udn:
|
||||
return self.async_abort(reason="incomplete_discovery")
|
||||
udn = udn[0]
|
||||
|
||||
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
|
||||
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
|
||||
|
||||
await self.async_set_unique_id(udn)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -74,11 +74,13 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running hvac operation."""
|
||||
return OVERKIZ_TO_HVAC_ACTION[
|
||||
cast(str, self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE))
|
||||
]
|
||||
if (
|
||||
state := self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE)
|
||||
) is None:
|
||||
return None
|
||||
return OVERKIZ_TO_HVAC_ACTION[cast(str, state)]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
|
||||
@@ -70,6 +70,7 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
set_position_command=OverkizCommand.SET_DEPLOYMENT,
|
||||
open_command=OverkizCommand.DEPLOY,
|
||||
close_command=OverkizCommand.UNDEPLOY,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
invert_position=False,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
|
||||
),
|
||||
@@ -80,6 +81,7 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
set_position_command=OverkizCommand.SET_DEPLOYMENT,
|
||||
open_command=OverkizCommand.DEPLOY,
|
||||
close_command=OverkizCommand.UNDEPLOY,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
invert_position=False,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
|
||||
),
|
||||
@@ -146,6 +148,36 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
close_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override to support very specific tilt commands (rts:SheerBlindRTSComponent)
|
||||
# uiClass is VenetianBlind
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.UP_DOWN_SHEER_SCREEN,
|
||||
device_class=CoverDeviceClass.BLIND,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
open_tilt_command=OverkizCommand.TILT_POSITIVE,
|
||||
open_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
|
||||
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
|
||||
close_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since BioclimaticPergola uses core:SlatsOpenClosedState
|
||||
# and core:SlateOrientationState (tilt-only, no position)
|
||||
# uiClass is Pergola
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.BIOCLIMATIC_PERGOLA,
|
||||
device_class=CoverDeviceClass.AWNING,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED,
|
||||
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
|
||||
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
|
||||
open_tilt_command=OverkizCommand.OPEN_SLATS,
|
||||
close_tilt_command=OverkizCommand.CLOSE_SLATS,
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since PositionableGarageDoor reports
|
||||
# core:OpenClosedUnknownState instead of core:OpenClosedState
|
||||
# uiClass is GarageDoor
|
||||
@@ -159,6 +191,17 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
stop_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN,
|
||||
),
|
||||
# Needs override since DiscretePositionableGarageDoor reports
|
||||
# core:OpenClosedUnknownState instead of core:OpenClosedState
|
||||
# uiClass is GarageDoor
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.DISCRETE_POSITIONABLE_GARAGE_DOOR,
|
||||
device_class=CoverDeviceClass.GARAGE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN,
|
||||
),
|
||||
# Needs override since PositionableGarageDoorWithPartialPosition reports
|
||||
# core:OpenClosedPartialState instead of core:OpenClosedState
|
||||
# uiClass is GarageDoor
|
||||
@@ -183,6 +226,111 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since OpenCloseGate4T only supports the cycle command
|
||||
# (rts:GateOpenerRTS4TComponent)
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.OPEN_CLOSE_GATE_4T,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since UpDownGarageDoor4T only supports the cycle command
|
||||
# (rts:GarageDoor4TRTSComponent)
|
||||
# uiClass is GarageDoor
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.UP_DOWN_GARAGE_DOOR_4T,
|
||||
device_class=CoverDeviceClass.GARAGE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since OpenCloseSlidingGarageDoor4T only supports the cycle command
|
||||
# uiClass is GarageDoor
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.OPEN_CLOSE_SLIDING_GARAGE_DOOR_4T,
|
||||
device_class=CoverDeviceClass.GARAGE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since OpenCloseSlidingGate4T only supports the cycle command
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.OPEN_CLOSE_SLIDING_GATE_4T,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since CyclicGarageDoor only supports the cycle command
|
||||
# (io:CyclicGarageOpenerIOComponent)
|
||||
# uiClass is GarageDoor
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.CYCLIC_GARAGE_DOOR,
|
||||
device_class=CoverDeviceClass.GARAGE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since CyclicSlidingGateOpener only supports the cycle command
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.CYCLIC_SLIDING_GATE_OPENER,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since CyclicSwingingGateOpener only supports the cycle command
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.CYCLIC_SWINGING_GATE_OPENER,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.CYCLE,
|
||||
close_command=OverkizCommand.CYCLE,
|
||||
),
|
||||
# Needs override since SlidingDiscreteGateWithPedestrianPosition reports
|
||||
# core:OpenClosedPedestrianState instead of core:OpenClosedState
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.SLIDING_DISCRETE_GATE_WITH_PEDESTRIAN_POSITION,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since OpenCloseGateWithPedestrianPosition reports
|
||||
# core:OpenClosedPedestrianState instead of core:OpenClosedState
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.OPEN_CLOSE_GATE_WITH_PEDESTRIAN_POSITION,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since OpenCloseSlidingGateWithPedestrianPosition reports
|
||||
# core:OpenClosedPedestrianState instead of core:OpenClosedState
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.OPEN_CLOSE_SLIDING_GATE_WITH_PEDESTRIAN_POSITION,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override since PositionableGateWithPedestrianPosition reports
|
||||
# core:OpenClosedPedestrianState instead of core:OpenClosedState
|
||||
# uiClass is Gate
|
||||
OverkizCoverDescription(
|
||||
key=UIWidget.POSITIONABLE_GATE_WITH_PEDESTRIAN_POSITION,
|
||||
device_class=CoverDeviceClass.GATE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
|
||||
current_position_state=OverkizState.CORE_CLOSURE,
|
||||
set_position_command=OverkizCommand.SET_CLOSURE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
),
|
||||
# Needs override to support this Generic device (rts:GenericRTSComponent)
|
||||
# uiClass is Generic (not mapped to cover as this is a Generic device class)
|
||||
OverkizCoverDescription(
|
||||
@@ -273,15 +421,12 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
OverkizCoverDescription(
|
||||
key=UIClass.PERGOLA,
|
||||
device_class=CoverDeviceClass.AWNING,
|
||||
current_position_state=OverkizState.CORE_CLOSURE,
|
||||
set_position_command=OverkizCommand.SET_CLOSURE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED,
|
||||
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
|
||||
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
|
||||
open_tilt_command=OverkizCommand.OPEN_SLATS,
|
||||
close_tilt_command=OverkizCommand.CLOSE_SLATS,
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
|
||||
),
|
||||
OverkizCoverDescription(
|
||||
key=UIClass.ROLLER_SHUTTER,
|
||||
@@ -292,6 +437,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
|
||||
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
),
|
||||
OverkizCoverDescription(
|
||||
key=UIClass.SCREEN,
|
||||
@@ -326,9 +474,14 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
|
||||
OverkizCoverDescription(
|
||||
key=UIClass.VENETIAN_BLIND,
|
||||
device_class=CoverDeviceClass.BLIND,
|
||||
current_position_state=OverkizState.CORE_CLOSURE,
|
||||
set_position_command=OverkizCommand.SET_CLOSURE,
|
||||
open_command=OverkizCommand.OPEN,
|
||||
close_command=OverkizCommand.CLOSE,
|
||||
stop_command=OverkizCommand.STOP,
|
||||
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
|
||||
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
|
||||
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
|
||||
open_tilt_command=OverkizCommand.TILT_UP,
|
||||
close_tilt_command=OverkizCommand.TILT_DOWN,
|
||||
stop_tilt_command=OverkizCommand.STOP,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["qbittorrent"],
|
||||
"requirements": ["qbittorrent-api==2024.9.67"]
|
||||
"requirements": ["qbittorrent-api==2026.5.1"]
|
||||
}
|
||||
|
||||
@@ -29,29 +29,29 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"translation_key_0": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_clicks_cubic_meter": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_1": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_consumed_liters": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_2": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_leak_clicks": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_3": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_leak_volume": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_4": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_start_index": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_5": {
|
||||
"default": "mdi:abc"
|
||||
"flow_sensor_watering_clicks": {
|
||||
"default": "mdi:water-pump"
|
||||
},
|
||||
"translation_key_6": {
|
||||
"default": "mdi:abc"
|
||||
"last_leak_detected": {
|
||||
"default": "mdi:pipe-leak"
|
||||
},
|
||||
"translation_key_7": {
|
||||
"default": "mdi:abc"
|
||||
"rain_sensor_rain_start": {
|
||||
"default": "mdi:weather-pouring"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.5.8"]
|
||||
"requirements": ["renault-api==0.5.10"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==5.5.1",
|
||||
"python-roborock==5.12.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import hashlib
|
||||
@@ -40,6 +41,15 @@ from .utils import get_device_entry_gen
|
||||
CONTENT_TYPE_AUDIO = "audio"
|
||||
CONTENT_TYPE_RADIO = "radio"
|
||||
|
||||
ALLOWED_IMAGE_MIME_TYPES: Final = frozenset(
|
||||
{
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
}
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -104,6 +114,9 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
|
||||
_last_media_position: int | None = None
|
||||
_last_media_position_updated_at: datetime.datetime | None = None
|
||||
|
||||
_cached_thumb: str | None = None
|
||||
_cached_thumb_result: tuple[bytes, str] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
@@ -217,9 +230,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_image_hash(self) -> str | None:
|
||||
"""Hash value for media image."""
|
||||
if (thumb := self._media_meta.get("thumb")) and thumb.startswith("data"):
|
||||
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
|
||||
return super().media_image_hash
|
||||
thumb = self._media_meta.get("thumb")
|
||||
if not thumb or self._decode_image_data(thumb) is None:
|
||||
return super().media_image_hash
|
||||
|
||||
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
def _get_updated_media_position(self) -> int | None:
|
||||
"""Return the current playback position and update its timestamp."""
|
||||
@@ -237,15 +252,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
|
||||
"""Fetch media image of current playing track."""
|
||||
thumb = self._media_meta["thumb"]
|
||||
try:
|
||||
prefix, image_data = thumb.split(",", 1)
|
||||
image = base64.b64decode(image_data, validate=True)
|
||||
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
|
||||
except binascii.Error, ValueError:
|
||||
thumb = self._media_meta.get("thumb")
|
||||
if not thumb or (result := self._decode_image_data(thumb)) is None:
|
||||
return await super().async_get_media_image()
|
||||
|
||||
return image, mime
|
||||
return result
|
||||
|
||||
@rpc_call
|
||||
async def async_media_play(self) -> None:
|
||||
@@ -436,3 +447,25 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
|
||||
translation_key="unsupported_media_type",
|
||||
translation_placeholders={"media_type": str(media_type)},
|
||||
)
|
||||
|
||||
def _decode_image_data(self, thumb: str) -> tuple[bytes, str] | None:
|
||||
"""Return image_bytes and mime_type for a valid image data or None."""
|
||||
if thumb == self._cached_thumb:
|
||||
return self._cached_thumb_result
|
||||
|
||||
result: tuple[bytes, str] | None = None
|
||||
if thumb.startswith("data"):
|
||||
try:
|
||||
prefix, image_data = thumb.split(",", 1)
|
||||
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
|
||||
except IndexError, ValueError:
|
||||
pass
|
||||
else:
|
||||
if mime in ALLOWED_IMAGE_MIME_TYPES:
|
||||
with contextlib.suppress(binascii.Error):
|
||||
result = base64.b64decode(image_data, validate=True), mime
|
||||
|
||||
self._cached_thumb = thumb
|
||||
self._cached_thumb_result = result
|
||||
|
||||
return result
|
||||
|
||||
@@ -158,6 +158,8 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||
set_value_fn=_async_set_foot_warmer_time,
|
||||
get_name_fn=_get_foot_warming_name,
|
||||
get_unique_id_fn=_get_foot_warming_unique_id,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
),
|
||||
CORE_CLIMATE_TIMER: SleepIQNumberEntityDescription(
|
||||
key=CORE_CLIMATE_TIMER,
|
||||
@@ -170,7 +172,7 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||
set_value_fn=_async_set_core_climate_time,
|
||||
get_name_fn=_get_core_climate_name,
|
||||
get_unique_id_fn=_get_core_climate_unique_id,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.COMPLETION_TIME,
|
||||
translation_key="completion_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=dt_util.parse_datetime,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
)
|
||||
],
|
||||
},
|
||||
@@ -449,7 +449,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.COMPLETION_TIME,
|
||||
translation_key="completion_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=dt_util.parse_datetime,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
)
|
||||
],
|
||||
},
|
||||
@@ -567,7 +567,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.GAS_METER_TIME,
|
||||
translation_key="gas_meter_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=dt_util.parse_datetime,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
)
|
||||
],
|
||||
Attribute.GAS_METER_VOLUME: [
|
||||
@@ -728,7 +728,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.COMPLETION_TIME,
|
||||
translation_key="completion_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=dt_util.parse_datetime,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
component_fn=lambda component: component == "cavity-01",
|
||||
component_translation_key={
|
||||
"cavity-01": "oven_completion_time_cavity_01",
|
||||
@@ -1198,7 +1198,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.COMPLETION_TIME,
|
||||
translation_key="completion_time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=dt_util.parse_datetime,
|
||||
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
|
||||
component_fn=lambda component: component == "sub",
|
||||
component_translation_key={
|
||||
"sub": "washer_sub_completion_time",
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"config_entry_not_ready": {
|
||||
"message": "Error while loading the config entry."
|
||||
},
|
||||
"update_error": {
|
||||
"update_failed": {
|
||||
"message": "Error while updating data from the API."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ async def async_setup_entry(
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
|
||||
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
|
||||
except TedeeLocalAuthException as ex:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentification_failed",
|
||||
translation_key="authentication_failed",
|
||||
) from ex
|
||||
|
||||
except TedeeDataUpdateException as ex:
|
||||
|
||||
@@ -92,16 +92,6 @@ class TeleinfoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
usb.get_serial_by_id, discovery_info.device
|
||||
)
|
||||
|
||||
# Validate by reading a real Teleinfo frame — silent abort on failure
|
||||
errors, decoded_data = await self._validate_serial_port(dev_path)
|
||||
if errors or decoded_data is None:
|
||||
return self.async_abort(reason="not_teleinfo_device")
|
||||
|
||||
# Use ADCO (meter serial number) as unique_id — same as manual entry
|
||||
adco = decoded_data["ADCO"]
|
||||
await self.async_set_unique_id(adco)
|
||||
self._abort_if_unique_id_configured(updates={CONF_SERIAL_PORT: dev_path})
|
||||
|
||||
self._discovered_device = dev_path
|
||||
self.context["title_placeholders"] = {
|
||||
"name": human_readable_device_name(
|
||||
@@ -122,6 +112,20 @@ class TeleinfoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if TYPE_CHECKING:
|
||||
assert self._discovered_device is not None
|
||||
if user_input is not None:
|
||||
# Validate by reading a real Teleinfo frame — silent abort on failure
|
||||
errors, decoded_data = await self._validate_serial_port(
|
||||
self._discovered_device
|
||||
)
|
||||
if errors or decoded_data is None:
|
||||
return self.async_abort(reason="not_teleinfo_device")
|
||||
|
||||
# Use ADCO (meter serial number) as unique_id — same as manual entry
|
||||
adco = decoded_data["ADCO"]
|
||||
await self.async_set_unique_id(adco)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_SERIAL_PORT: self._discovered_device}
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Teleinfo ({self._discovered_device})",
|
||||
data={CONF_SERIAL_PORT: self._discovered_device},
|
||||
|
||||
@@ -8,15 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyteleinfo==0.4.0"],
|
||||
"usb": [
|
||||
{
|
||||
"pid": "6015",
|
||||
"vid": "0403"
|
||||
},
|
||||
{
|
||||
"pid": "EA60",
|
||||
"vid": "10C4"
|
||||
}
|
||||
]
|
||||
"requirements": ["pyteleinfo==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
"default": "mdi:battery-clock"
|
||||
},
|
||||
"forward_collision_warning": {
|
||||
"default": "mdi:car-crash",
|
||||
"default": "mdi:car-emergency",
|
||||
"state": {
|
||||
"average": "mdi:alert-circle",
|
||||
"early": "mdi:alert-octagon",
|
||||
@@ -634,7 +634,7 @@
|
||||
"default": "mdi:key"
|
||||
},
|
||||
"pedal_position": {
|
||||
"default": "mdi:pedestal"
|
||||
"default": "mdi:gauge"
|
||||
},
|
||||
"powershare_hours_left": {
|
||||
"default": "mdi:clock-time-eight-outline"
|
||||
@@ -794,7 +794,7 @@
|
||||
"service": "mdi:calendar-plus"
|
||||
},
|
||||
"add_precondition_schedule": {
|
||||
"service": "mdi:hvac-outline"
|
||||
"service": "mdi:hvac"
|
||||
},
|
||||
"navigation_gps_request": {
|
||||
"service": "mdi:crosshairs-gps"
|
||||
@@ -803,7 +803,7 @@
|
||||
"service": "mdi:calendar-minus"
|
||||
},
|
||||
"remove_precondition_schedule": {
|
||||
"service": "mdi:hvac-off-outline"
|
||||
"service": "mdi:hvac-off"
|
||||
},
|
||||
"set_scheduled_charging": {
|
||||
"service": "mdi:timeline-clock-outline"
|
||||
|
||||
@@ -139,7 +139,7 @@ def ws_start_preview(
|
||||
name=name,
|
||||
lower=msg["user_input"].get(CONF_LOWER),
|
||||
upper=msg["user_input"].get(CONF_UPPER),
|
||||
hysteresis=msg["user_input"].get(CONF_HYSTERESIS),
|
||||
hysteresis=msg["user_input"].get(CONF_HYSTERESIS, DEFAULT_HYSTERESIS),
|
||||
device_class=None,
|
||||
unique_id=None,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
DEVICE_CLASS_STATE_CLASSES,
|
||||
DEVICE_CLASS_UNITS,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
@@ -408,11 +409,12 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
self._current_tz = None
|
||||
self._config_scheduler()
|
||||
|
||||
def _config_scheduler(self):
|
||||
def _config_scheduler(self, start_time: datetime | None = None) -> None:
|
||||
self.scheduler = (
|
||||
CronSim(
|
||||
self._cron_pattern,
|
||||
dt_util.now(
|
||||
start_time
|
||||
or dt_util.now(
|
||||
dt_util.get_default_time_zone()
|
||||
), # we need timezone for DST purposes (see issue #102984)
|
||||
)
|
||||
@@ -610,8 +612,6 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
# and we need to reconfigure the scheduler
|
||||
self._current_tz = self.hass.config.time_zone
|
||||
|
||||
await self._program_reset()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_RESET_METER, self.async_reset_meter
|
||||
@@ -630,6 +630,13 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
if last_sensor_data.status == COLLECTING:
|
||||
# Null lambda to allow cancelling the collection on tariff change
|
||||
self._collecting = lambda: None
|
||||
# Reconfigure the scheduler from the restored last_reset so that
|
||||
# next_reset is not shifted forward on entity restore/rename.
|
||||
self._config_scheduler(
|
||||
dt_util.as_local(self._last_reset) if self._last_reset else None
|
||||
)
|
||||
|
||||
await self._program_reset()
|
||||
|
||||
@callback
|
||||
def async_source_tracking(event):
|
||||
@@ -697,12 +704,18 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass:
|
||||
"""Return the device class of the sensor."""
|
||||
return (
|
||||
SensorStateClass.TOTAL
|
||||
if self._sensor_net_consumption
|
||||
else SensorStateClass.TOTAL_INCREASING
|
||||
)
|
||||
"""Return the state class of the sensor."""
|
||||
if self._sensor_net_consumption:
|
||||
return SensorStateClass.TOTAL
|
||||
if (
|
||||
self._input_device_class is not None
|
||||
and SensorStateClass.TOTAL_INCREASING
|
||||
not in DEVICE_CLASS_STATE_CLASSES.get(
|
||||
self._input_device_class, {SensorStateClass.TOTAL_INCREASING}
|
||||
)
|
||||
):
|
||||
return SensorStateClass.TOTAL
|
||||
return SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -65,6 +66,12 @@ class VerisureAlarm(
|
||||
self.coordinator.verisure.request, command_data
|
||||
)
|
||||
LOGGER.debug("Verisure set arm state %s", state)
|
||||
if arm_state is None or "data" not in arm_state:
|
||||
await self.coordinator.async_refresh()
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="arm_state_failed",
|
||||
)
|
||||
result = None
|
||||
attempts = 0
|
||||
while result is None:
|
||||
@@ -79,6 +86,8 @@ class VerisureAlarm(
|
||||
list(arm_state["data"].values())[0], state
|
||||
),
|
||||
)
|
||||
if transaction is None:
|
||||
continue
|
||||
result = (
|
||||
transaction.get("data", {})
|
||||
.get("installation", {})
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"arm_state_failed": {
|
||||
"message": "Failed to change alarm state. Verify your code is correct and that your account is not temporarily locked."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -185,8 +185,10 @@ class WeatherFlowWindCoordinator(BaseWebsocketCoordinator[EventDataRapidWind]):
|
||||
"""Create rapid wind listen message."""
|
||||
return RapidWindListenStartMessage(device_id=str(device_id))
|
||||
|
||||
async def _handle_websocket_message(self, data: RapidWindWS) -> None:
|
||||
async def _handle_websocket_message(self, data: RapidWindWS | None) -> None:
|
||||
"""Handle rapid wind websocket data."""
|
||||
if data is None:
|
||||
return
|
||||
device_id = data.device_id
|
||||
station_id = self.device_to_station_map[device_id]
|
||||
|
||||
@@ -204,8 +206,12 @@ class WeatherFlowObservationCoordinator(BaseWebsocketCoordinator[WebsocketObserv
|
||||
"""Create observation listen message."""
|
||||
return ListenStartMessage(device_id=str(device_id))
|
||||
|
||||
async def _handle_websocket_message(self, data: ObservationTempestWS) -> None:
|
||||
async def _handle_websocket_message(
|
||||
self, data: ObservationTempestWS | None
|
||||
) -> None:
|
||||
"""Handle observation websocket data."""
|
||||
if data is None:
|
||||
return
|
||||
device_id = data.device_id
|
||||
station_id = self.device_to_station_map[device_id]
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"state": {
|
||||
"lightning": "mdi:weather-lightning-rainy",
|
||||
"rain": "mdi:weather-rainy",
|
||||
"rain_snow": "mdi:weather-snoy-rainy",
|
||||
"rain_snow": "mdi:weather-snowy-rainy",
|
||||
"snow": "mdi:weather-snowy"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["wled==0.22.0"],
|
||||
"requirements": ["wled==0.23.0"],
|
||||
"zeroconf": ["_wled._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
return
|
||||
|
||||
if event.type == assist_pipeline.PipelineEventType.RUN_START:
|
||||
if event.data and (tts_output := event.data["tts_output"]):
|
||||
if event.data and (tts_output := event.data.get("tts_output")):
|
||||
# Get stream token early.
|
||||
# If "tts_start_streaming" is True in INTENT_PROGRESS event, we
|
||||
# can start streaming TTS before the TTS_END event.
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
from zha.application.const import BAUD_RATES, RadioType
|
||||
from zha.application.gateway import Gateway
|
||||
from zha.application.helpers import ZHAData
|
||||
@@ -32,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import homeassistant_hardware, repairs, websocket_api
|
||||
from .config_flow import ZhaConfigFlowHandler
|
||||
from .const import (
|
||||
CONF_BAUDRATE,
|
||||
CONF_CUSTOM_QUIRKS_PATH,
|
||||
@@ -43,6 +45,7 @@ from .const import (
|
||||
CONF_ZIGPY,
|
||||
DATA_ZHA,
|
||||
DOMAIN,
|
||||
LEGACY_ZEROCONF_PORT,
|
||||
)
|
||||
from .helpers import (
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
@@ -301,7 +304,15 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > ZhaConfigFlowHandler.VERSION:
|
||||
# This means the user has downgraded from a future major version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
data = {
|
||||
@@ -361,5 +372,24 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
version=5,
|
||||
)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", config_entry.version)
|
||||
if config_entry.version == 5 and config_entry.minor_version < 2:
|
||||
data = {**config_entry.data, CONF_DEVICE: {**config_entry.data[CONF_DEVICE]}}
|
||||
device_path = data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
|
||||
if device_path.startswith(("socket://", "tcp://")):
|
||||
url = URL(device_path)
|
||||
if url.explicit_port is None:
|
||||
data[CONF_DEVICE][CONF_DEVICE_PATH] = str(
|
||||
url.with_port(LEGACY_ZEROCONF_PORT)
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, data=data, version=5, minor_version=2
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -47,7 +47,13 @@ from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN
|
||||
from .const import (
|
||||
CONF_BAUDRATE,
|
||||
CONF_FLOW_CONTROL,
|
||||
CONF_RADIO_TYPE,
|
||||
DOMAIN,
|
||||
LEGACY_ZEROCONF_PORT,
|
||||
)
|
||||
from .helpers import get_config_entry_unique_id, get_zha_gateway
|
||||
from .radio_manager import (
|
||||
DEVICE_SCHEMA,
|
||||
@@ -89,7 +95,6 @@ UPLOADED_BACKUP_FILE = "uploaded_backup_file"
|
||||
|
||||
REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/"
|
||||
|
||||
LEGACY_ZEROCONF_PORT = 6638
|
||||
LEGACY_ZEROCONF_ESPHOME_API_PORT = 6053
|
||||
|
||||
ZEROCONF_SERVICE_TYPE = "_zigbee-coordinator._tcp.local."
|
||||
@@ -760,6 +765,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 5
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def _set_unique_id_and_update_ignored_flow(
|
||||
self, unique_id: str, device_path: str
|
||||
|
||||
@@ -64,6 +64,8 @@ DEVICE_PAIRING_STATUS = "pairing_status"
|
||||
|
||||
DOMAIN = "zha"
|
||||
|
||||
LEGACY_ZEROCONF_PORT = 6638
|
||||
|
||||
GROUP_ID = "group_id"
|
||||
|
||||
|
||||
|
||||
@@ -1265,19 +1265,6 @@ def async_add_entities(
|
||||
entities.clear()
|
||||
|
||||
|
||||
def _clean_serial_port_path(path: str) -> str:
|
||||
"""Clean the serial port path, applying corrections where necessary."""
|
||||
|
||||
if path.startswith("socket://"):
|
||||
path = path.strip()
|
||||
|
||||
# Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4)
|
||||
if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path):
|
||||
path = path.replace("[", "").replace("]", "")
|
||||
|
||||
return path
|
||||
|
||||
|
||||
CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All(
|
||||
@@ -1316,18 +1303,6 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
|
||||
assert ha_zha_data.config_entry is not None
|
||||
assert ha_zha_data.yaml_config is not None
|
||||
|
||||
# Remove brackets around IP addresses, this no longer works in CPython 3.11.4
|
||||
# This will be removed in 2023.11.0
|
||||
path = ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
cleaned_path = _clean_serial_port_path(path)
|
||||
|
||||
if path != cleaned_path:
|
||||
_LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path)
|
||||
ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path
|
||||
hass.config_entries.async_update_entry(
|
||||
ha_zha_data.config_entry, data=ha_zha_data.config_entry.data
|
||||
)
|
||||
|
||||
# deep copy the yaml config to avoid modifying the original and to safely
|
||||
# pass it to the ZHA library
|
||||
app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
Generated
-10
@@ -58,16 +58,6 @@ USB = [
|
||||
"pid": "0003",
|
||||
"vid": "04B4",
|
||||
},
|
||||
{
|
||||
"domain": "teleinfo",
|
||||
"pid": "6015",
|
||||
"vid": "0403",
|
||||
},
|
||||
{
|
||||
"domain": "teleinfo",
|
||||
"pid": "EA60",
|
||||
"vid": "10C4",
|
||||
},
|
||||
{
|
||||
"domain": "velbus",
|
||||
"pid": "0B1B",
|
||||
|
||||
@@ -1722,9 +1722,14 @@ def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType
|
||||
|
||||
|
||||
async def async_validate_condition_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
hass: HomeAssistant, config: ConfigType | str
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
if isinstance(config, str):
|
||||
config = {
|
||||
CONF_CONDITION: "template",
|
||||
CONF_VALUE_TEMPLATE: cv.dynamic_template(config),
|
||||
}
|
||||
condition_key: str = config[CONF_CONDITION]
|
||||
|
||||
if condition_key in ("and", "not", "or"):
|
||||
|
||||
@@ -37,6 +37,27 @@ from .context import (
|
||||
template_context_manager,
|
||||
template_cv,
|
||||
)
|
||||
from .extensions import (
|
||||
AreaExtension,
|
||||
Base64Extension,
|
||||
CollectionExtension,
|
||||
ConfigEntryExtension,
|
||||
CryptoExtension,
|
||||
DateTimeExtension,
|
||||
DeviceExtension,
|
||||
EntityExtension,
|
||||
FloorExtension,
|
||||
FunctionalExtension,
|
||||
IssuesExtension,
|
||||
LabelExtension,
|
||||
MathExtension,
|
||||
RegexExtension,
|
||||
SerializationExtension,
|
||||
StateExtension,
|
||||
StringExtension,
|
||||
TypeCastExtension,
|
||||
VersionExtension,
|
||||
)
|
||||
from .helpers import result_as_boolean as result_as_boolean
|
||||
from .render_info import RenderInfo, render_info_cv
|
||||
from .states import (
|
||||
@@ -722,37 +743,25 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
] = weakref.WeakValueDictionary()
|
||||
self.add_extension("jinja2.ext.loopcontrols")
|
||||
self.add_extension("jinja2.ext.do")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.AreaExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.Base64Extension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.CollectionExtension"
|
||||
)
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.ConfigEntryExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.DateTimeExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.EntityExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.FunctionalExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.IssuesExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.SerializationExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.StateExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.TypeCastExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.VersionExtension")
|
||||
self.add_extension(AreaExtension)
|
||||
self.add_extension(Base64Extension)
|
||||
self.add_extension(CollectionExtension)
|
||||
self.add_extension(ConfigEntryExtension)
|
||||
self.add_extension(CryptoExtension)
|
||||
self.add_extension(DateTimeExtension)
|
||||
self.add_extension(DeviceExtension)
|
||||
self.add_extension(EntityExtension)
|
||||
self.add_extension(FloorExtension)
|
||||
self.add_extension(FunctionalExtension)
|
||||
self.add_extension(IssuesExtension)
|
||||
self.add_extension(LabelExtension)
|
||||
self.add_extension(MathExtension)
|
||||
self.add_extension(RegexExtension)
|
||||
self.add_extension(SerializationExtension)
|
||||
self.add_extension(StateExtension)
|
||||
self.add_extension(StringExtension)
|
||||
self.add_extension(TypeCastExtension)
|
||||
self.add_extension(VersionExtension)
|
||||
|
||||
if hass is not None:
|
||||
# This environment has access to hass, attach its loader
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
aiodns==4.0.0
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.5.2"
|
||||
version = "2026.5.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.14.2"
|
||||
dependencies = [
|
||||
"aiodns==4.0.0",
|
||||
"aiodns==4.0.4",
|
||||
# aiogithubapi is needed by frontend; frontend is unconditionally imported at
|
||||
# module level in `bootstrap.py` and its requirements thus need to be in
|
||||
# requirements.txt to ensure they are always installed
|
||||
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@
|
||||
-c homeassistant/package_constraints.txt
|
||||
|
||||
# Home Assistant Core
|
||||
aiodns==4.0.0
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
|
||||
Generated
+8
-8
@@ -233,7 +233,7 @@ aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.0
|
||||
aiodns==4.0.4
|
||||
|
||||
# homeassistant.components.eafm
|
||||
aioeafm==0.1.2
|
||||
@@ -294,7 +294,7 @@ aiohue==4.8.1
|
||||
aioimaplib==2.0.1
|
||||
|
||||
# homeassistant.components.immich
|
||||
aioimmich==0.14.0
|
||||
aioimmich==0.14.1
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.10.0
|
||||
@@ -321,7 +321,7 @@ aiolifx==1.2.1
|
||||
aiolookin==1.0.0
|
||||
|
||||
# homeassistant.components.lyric
|
||||
aiolyric==2.0.2
|
||||
aiolyric==2.1.1
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==1.2.4
|
||||
@@ -2192,7 +2192,7 @@ pyinsteon==1.6.4
|
||||
pyintelliclima==0.3.1
|
||||
|
||||
# homeassistant.components.intesishome
|
||||
pyintesishome==1.8.7
|
||||
pyintesishome==1.8.8
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.9
|
||||
@@ -2675,7 +2675,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==5.5.1
|
||||
python-roborock==5.12.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
@@ -2796,7 +2796,7 @@ pyzbar==0.1.7
|
||||
pyzerproc==0.4.8
|
||||
|
||||
# homeassistant.components.qbittorrent
|
||||
qbittorrent-api==2024.9.67
|
||||
qbittorrent-api==2026.5.1
|
||||
|
||||
# homeassistant.components.qbus
|
||||
qbusmqttapi==1.4.3
|
||||
@@ -2835,7 +2835,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.8
|
||||
renault-api==0.5.10
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -3328,7 +3328,7 @@ wiim==0.1.2
|
||||
wirelesstagpy==0.8.1
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.22.0
|
||||
wled==0.23.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.48
|
||||
|
||||
Generated
+7
-7
@@ -224,7 +224,7 @@ aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.0
|
||||
aiodns==4.0.4
|
||||
|
||||
# homeassistant.components.eafm
|
||||
aioeafm==0.1.2
|
||||
@@ -282,7 +282,7 @@ aiohue==4.8.1
|
||||
aioimaplib==2.0.1
|
||||
|
||||
# homeassistant.components.immich
|
||||
aioimmich==0.14.0
|
||||
aioimmich==0.14.1
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.10.0
|
||||
@@ -306,7 +306,7 @@ aiolifx==1.2.1
|
||||
aiolookin==1.0.0
|
||||
|
||||
# homeassistant.components.lyric
|
||||
aiolyric==2.0.2
|
||||
aiolyric==2.1.1
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==1.2.4
|
||||
@@ -2280,7 +2280,7 @@ python-qube-heatpump==1.8.0
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==5.5.1
|
||||
python-roborock==5.12.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
@@ -2386,7 +2386,7 @@ pyyardian==1.1.1
|
||||
pyzerproc==0.4.8
|
||||
|
||||
# homeassistant.components.qbittorrent
|
||||
qbittorrent-api==2024.9.67
|
||||
qbittorrent-api==2026.5.1
|
||||
|
||||
# homeassistant.components.qbus
|
||||
qbusmqttapi==1.4.3
|
||||
@@ -2419,7 +2419,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.8
|
||||
renault-api==0.5.10
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -2828,7 +2828,7 @@ wiffi==1.1.2
|
||||
wiim==0.1.2
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.22.0
|
||||
wled==0.23.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.48
|
||||
|
||||
@@ -285,8 +285,6 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
|
||||
"lacrosse": {"homeassistant": {"pylacrosse"}},
|
||||
# ???
|
||||
"linode": {"homeassistant": {"linode-api"}},
|
||||
# https://github.com/timmo001/aiolyric
|
||||
"lyric": {"homeassistant": {"aiolyric"}},
|
||||
# https://github.com/microBeesTech/pythonSDK/
|
||||
"microbees": {
|
||||
"homeassistant": {"microbeespy"},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for Apple TV binary sensor."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pyatv.const import DeviceModel, KeyboardFocusState, Protocol
|
||||
|
||||
from homeassistant.components.apple_tv.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import create_conf, mrp_service
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_keyboard_focus_entity_created_on_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_async_zeroconf: MagicMock,
|
||||
) -> None:
|
||||
"""Test the keyboard focus binary sensor is created when the device supports it.
|
||||
|
||||
Regression test for https://github.com/home-assistant/core/issues/170075 — the
|
||||
initial SIGNAL_CONNECTED dispatch happens in async_first_connect (before platform
|
||||
forwarding), so the binary_sensor platform must also handle the already-connected
|
||||
case rather than relying solely on the dispatcher signal.
|
||||
"""
|
||||
atv = AsyncMock()
|
||||
atv.close = MagicMock()
|
||||
atv.features = MagicMock()
|
||||
atv.features.in_state = MagicMock(return_value=True)
|
||||
atv.keyboard = AsyncMock()
|
||||
atv.keyboard.text_focus_state = KeyboardFocusState.Unfocused
|
||||
atv.push_updater = MagicMock()
|
||||
atv.device_info.model = DeviceModel.Gen4K
|
||||
atv.device_info.raw_model = "AppleTV6,2"
|
||||
atv.device_info.version = "15.0"
|
||||
atv.device_info.mac = "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Living Room",
|
||||
unique_id="mrpid",
|
||||
data={
|
||||
CONF_ADDRESS: "127.0.0.1",
|
||||
CONF_NAME: "Living Room",
|
||||
"credentials": {str(Protocol.MRP.value): "mrp_creds"},
|
||||
"identifiers": ["mrpid"],
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
scan_result = create_conf("127.0.0.1", "Living Room", mrp_service())
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.apple_tv.scan", return_value=[scan_result]),
|
||||
patch("homeassistant.components.apple_tv.connect", return_value=atv),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.living_room_keyboard_focus")
|
||||
assert state is not None
|
||||
@@ -1,13 +1,15 @@
|
||||
"""BleBox light entities tests."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, PropertyMock
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
import blebox_uniapi
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.blebox.const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
@@ -329,6 +331,73 @@ def wlightbox_fixture():
|
||||
return (feature, "light.my_wlightbox_wlightbox_color")
|
||||
|
||||
|
||||
@pytest.fixture(name="wlightbox_ct")
|
||||
def wlightbox_ct_fixture() -> tuple[MagicMock, str]:
|
||||
"""Return a default light entity mock for color temperature testing."""
|
||||
|
||||
feature = mock_feature(
|
||||
"lights",
|
||||
blebox_uniapi.light.Light,
|
||||
unique_id="BleBox-wLightBox-1afe34e750b8-color",
|
||||
full_name="wLightBox-ct",
|
||||
device_class=None,
|
||||
is_on=None,
|
||||
supports_color=True,
|
||||
supports_white=True,
|
||||
white_value=None,
|
||||
rgbw_hex=None,
|
||||
color_mode=blebox_uniapi.light.BleboxColorMode.CT,
|
||||
effect="NONE",
|
||||
effect_list=["NONE", "PL", "POLICE"],
|
||||
)
|
||||
product = feature.product
|
||||
type(product).name = PropertyMock(return_value="My wLightBox")
|
||||
type(product).model = PropertyMock(return_value="wLightBox")
|
||||
return feature, "light.my_wlightbox_wlightbox_ct"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kelvin_requested", [1000, 2700, 3000, 4000, 5000, 6500, 8000])
|
||||
async def test_wlightbox_on_color_temp(
|
||||
hass: HomeAssistant,
|
||||
wlightbox_ct: tuple[MagicMock, str],
|
||||
kelvin_requested: int,
|
||||
) -> None:
|
||||
"""Test light on with color temperature change."""
|
||||
|
||||
feature_mock, entity_id = wlightbox_ct
|
||||
|
||||
# Capture the native scale value passed to the device to verify the
|
||||
# conversion is correct without depending on blebox_uniapi internals.
|
||||
transient_temp: int = -1
|
||||
|
||||
def return_color_temp_with_brightness(value: int, _brightness: int) -> list[int]:
|
||||
nonlocal transient_temp
|
||||
transient_temp = value
|
||||
return [0x00, 0x39, 0xB0, 0xFF]
|
||||
|
||||
def turn_on(_: list[int]) -> None:
|
||||
feature_mock.is_on = True
|
||||
feature_mock.color_temp = transient_temp
|
||||
|
||||
feature_mock.return_color_temp_with_brightness = return_color_temp_with_brightness
|
||||
feature_mock.async_on = AsyncMock(side_effect=turn_on)
|
||||
|
||||
await async_setup_entity(hass, entity_id)
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
SERVICE_TURN_ON,
|
||||
{"entity_id": entity_id, ATTR_COLOR_TEMP_KELVIN: kelvin_requested},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
assert 0 <= transient_temp <= 255
|
||||
|
||||
kelvin_actual = state.attributes[ATTR_COLOR_TEMP_KELVIN]
|
||||
assert LIGHT_MIN_KELVINS <= kelvin_actual <= LIGHT_MAX_KELVINS
|
||||
|
||||
|
||||
async def test_wlightbox_init(
|
||||
wlightbox, hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
@@ -461,6 +530,48 @@ async def test_wlightbox_on_to_last_color(wlightbox, hass: HomeAssistant) -> Non
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_wlightbox_turn_on_with_zero_brightness_turns_off(
|
||||
wlightbox, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test that setting brightness to 0 turns the light off instead of raising ValueError."""
|
||||
|
||||
feature_mock, entity_id = wlightbox
|
||||
|
||||
def initial_update():
|
||||
feature_mock.is_on = True
|
||||
feature_mock.rgbw_hex = "c1d2f3c7"
|
||||
feature_mock.white_value = 0xC7
|
||||
|
||||
feature_mock.async_update = AsyncMock(side_effect=initial_update)
|
||||
await async_setup_entity(hass, entity_id)
|
||||
feature_mock.async_update = AsyncMock()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
feature_mock.apply_brightness = MagicMock(return_value=[0, 0, 0, 0])
|
||||
|
||||
def turn_off():
|
||||
feature_mock.is_on = False
|
||||
feature_mock.white_value = 0x0
|
||||
feature_mock.rgbw_hex = "00000000"
|
||||
|
||||
feature_mock.async_off = AsyncMock(side_effect=turn_off)
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
SERVICE_TURN_ON,
|
||||
{"entity_id": entity_id, ATTR_BRIGHTNESS: 0},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
feature_mock.async_off.assert_called_once()
|
||||
feature_mock.async_on.assert_not_called()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_wlightbox_off(wlightbox, hass: HomeAssistant) -> None:
|
||||
"""Test light off."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
|
||||
import datetime
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
import zoneinfo
|
||||
|
||||
from caldav.objects import Event
|
||||
@@ -1063,13 +1063,67 @@ async def test_get_events_custom_calendars(
|
||||
"summary": "This is a normal event",
|
||||
"location": "Hamburg",
|
||||
"description": "Surprisingly rainy",
|
||||
"uid": None,
|
||||
"uid": "0",
|
||||
"recurrence_id": None,
|
||||
"rrule": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def test_get_events_with_recurrence_id(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test that uid and recurrence_id are populated from VEVENT data."""
|
||||
vevent_with_recurrence_id = """BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//E-Corp.//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
UID:original-event-uid
|
||||
RECURRENCE-ID:20171127T170000Z
|
||||
DTSTAMP:20171125T000000Z
|
||||
DTSTART:20171127T180000Z
|
||||
DTEND:20171127T190000Z
|
||||
SUMMARY:Modified occurrence
|
||||
LOCATION:Hamburg
|
||||
DESCRIPTION:This occurrence was moved
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
calendar = Mock()
|
||||
calendar.name = "Example"
|
||||
calendar.get_supported_components = MagicMock(return_value=["VEVENT"])
|
||||
calendar.search = MagicMock(
|
||||
return_value=[
|
||||
Event(
|
||||
None, "0.ics", vevent_with_recurrence_id, calendar, "original-event-uid"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.caldav.calendar.caldav.DAVClient"
|
||||
) as mock_client:
|
||||
mock_client.return_value.principal.return_value.calendars.return_value = [
|
||||
calendar
|
||||
]
|
||||
assert await async_setup_component(
|
||||
hass, "calendar", {"calendar": CALDAV_CONFIG}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(
|
||||
f"/api/calendars/{TEST_ENTITY}?start=2017-11-27&end=2017-11-28"
|
||||
)
|
||||
assert response.status == HTTPStatus.OK
|
||||
events = await response.json()
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0]["uid"] == "original-event-uid"
|
||||
assert events[0]["recurrence_id"] == "2017-11-27 17:00:00+00:00"
|
||||
assert events[0]["summary"] == "Modified occurrence"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("calendars"),
|
||||
[
|
||||
|
||||
@@ -372,6 +372,10 @@ async def test_sync_google_on_home_assistant_start(
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_sync.mock_calls) == 0
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
|
||||
|
||||
|
||||
@@ -385,6 +385,40 @@ async def test_climate_missing_variables(
|
||||
assert state.attributes["temperature"] == 68.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_climate_variables",
|
||||
[
|
||||
{
|
||||
123: {
|
||||
"HVAC_STATE": "Off",
|
||||
"HVAC_MODE": "Heat",
|
||||
"TEMPERATURE_F": 72.0,
|
||||
"HUMIDITY": "Undefined",
|
||||
"COOL_SETPOINT_F": 75.0,
|
||||
"HEAT_SETPOINT_F": 68.0,
|
||||
"SCALE": "FAHRENHEIT",
|
||||
}
|
||||
}
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_c4_account",
|
||||
"mock_c4_director",
|
||||
"mock_climate_update_variables",
|
||||
"init_integration",
|
||||
)
|
||||
async def test_climate_undefined_humidity(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test climate entity handles 'Undefined' humidity string gracefully."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.HEAT
|
||||
assert state.attributes.get("current_temperature") == 72.0
|
||||
assert state.attributes.get("current_humidity") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_climate_variables",
|
||||
[
|
||||
|
||||
@@ -367,7 +367,7 @@ def test_master_state(hass: HomeAssistant) -> None:
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES
|
||||
assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED]
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2
|
||||
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == 12322
|
||||
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == "12322"
|
||||
assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC
|
||||
assert state.attributes[ATTR_MEDIA_DURATION] == 0.05
|
||||
assert state.attributes[ATTR_MEDIA_POSITION] == 0.005
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Test the GoodWe initialization."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from goodwe import InverterError
|
||||
|
||||
from homeassistant.components.goodwe import CONF_MODEL_FAMILY, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -34,3 +37,34 @@ async def test_migration(
|
||||
assert config_entry.data[CONF_HOST] == TEST_HOST
|
||||
assert config_entry.data[CONF_MODEL_FAMILY] == "MagicMock"
|
||||
assert config_entry.data[CONF_PORT] == TEST_PORT
|
||||
|
||||
|
||||
async def test_setup_connect_not_ready(hass: HomeAssistant) -> None:
|
||||
"""Test that setup raises ConfigEntryNotReady when inverter is unreachable."""
|
||||
config_entry = MockConfigEntry(
|
||||
version=2,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_PORT: TEST_PORT,
|
||||
CONF_MODEL_FAMILY: "ET",
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.goodwe.connect",
|
||||
side_effect=InverterError,
|
||||
) as mock_connect,
|
||||
patch(
|
||||
"homeassistant.components.goodwe.config_flow.connect",
|
||||
side_effect=InverterError,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
# Verify connect is called with limited retries to avoid blocking startup
|
||||
assert mock_connect.call_args.kwargs["retries"] <= 3
|
||||
|
||||
@@ -28,8 +28,12 @@ from homeassistant.components.google_assistant.http import (
|
||||
_get_homegraph_token,
|
||||
async_get_users,
|
||||
)
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.const import (
|
||||
CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
)
|
||||
from homeassistant.core import CoreState, HomeAssistant, State
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -60,6 +64,25 @@ MOCK_HEADER = {
|
||||
}
|
||||
|
||||
|
||||
async def test_sync_google_does_not_block_startup(hass: HomeAssistant) -> None:
|
||||
"""Test that Google entity sync runs after startup, not during."""
|
||||
hass.set_state(CoreState.not_running)
|
||||
config = GoogleConfig(hass, DUMMY_CONFIG)
|
||||
|
||||
with patch.object(config, "async_sync_entities_all") as mock_sync:
|
||||
await config.async_initialize()
|
||||
|
||||
# Fire EVENT_HOMEASSISTANT_START - sync should NOT run yet
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
mock_sync.assert_not_called()
|
||||
|
||||
# Fire EVENT_HOMEASSISTANT_STARTED - now sync should run
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
mock_sync.assert_called_once()
|
||||
|
||||
|
||||
async def test_get_jwt(hass: HomeAssistant) -> None:
|
||||
"""Test signing of key."""
|
||||
|
||||
|
||||
@@ -125,6 +125,29 @@ async def test_sensors_classic_api(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-10-21")
|
||||
async def test_mix_empty_chart_data(
|
||||
hass: HomeAssistant,
|
||||
mock_growatt_classic_api,
|
||||
mock_config_entry_classic: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test mix device handles empty chart data without crashing."""
|
||||
mock_growatt_classic_api.device_list.return_value = [
|
||||
{"deviceSn": "MIX123456", "deviceType": "mix"}
|
||||
]
|
||||
mock_growatt_classic_api.mix_detail.return_value = {
|
||||
"deviceSn": "MIX123456",
|
||||
"chartData": {},
|
||||
}
|
||||
|
||||
with patch("homeassistant.components.growatt_server.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry_classic)
|
||||
|
||||
# Should not crash - entities should still be created
|
||||
states = hass.states.async_entity_ids("sensor")
|
||||
assert len(states) > 0
|
||||
|
||||
|
||||
async def test_sensor_coordinator_updates(
|
||||
hass: HomeAssistant,
|
||||
mock_growatt_v1_api,
|
||||
|
||||
@@ -1766,6 +1766,12 @@ async def test_create_todo(
|
||||
},
|
||||
Task(everyX=5),
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_INTERVAL: 0,
|
||||
},
|
||||
Task(everyX=0),
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_FREQUENCY: "weekly",
|
||||
|
||||
@@ -717,6 +717,52 @@ async def test_if_fires_using_at_sensor_with_offset(
|
||||
)
|
||||
|
||||
|
||||
async def test_if_fires_using_at_sensor_dict_without_offset(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_calls: list[ServiceCall],
|
||||
) -> None:
|
||||
"""Test for firing at sensor time using dict format without offset."""
|
||||
now = dt_util.now()
|
||||
|
||||
trigger_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2)
|
||||
|
||||
hass.states.async_set(
|
||||
"sensor.next_alarm",
|
||||
trigger_dt.isoformat(),
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP},
|
||||
)
|
||||
|
||||
time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1)
|
||||
|
||||
freezer.move_to(dt_util.as_utc(time_that_will_not_match_right_away))
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
"platform": "time",
|
||||
"at": {
|
||||
"entity_id": "sensor.next_alarm",
|
||||
},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"some": "{{ trigger.entity_id }}"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["some"] == "sensor.next_alarm"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf",
|
||||
[
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from homeassistant.components.homeassistant_connect_zbt2.const import DOMAIN
|
||||
from homeassistant.components.usb import DOMAIN as USB_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -65,3 +66,66 @@ async def test_hardware_info(
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def test_hardware_info_ignored_entry(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info
|
||||
) -> None:
|
||||
"""Test ignored discovery entries don't crash hardware info.
|
||||
|
||||
Regression test for https://github.com/home-assistant/core/issues/170270
|
||||
"""
|
||||
assert await async_setup_component(hass, USB_DOMAIN, {})
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
|
||||
# Setup the normal entry so the hardware platform is loaded
|
||||
normal_entry = MockConfigEntry(
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Connect ZBT-2",
|
||||
unique_id="normal_1",
|
||||
version=1,
|
||||
minor_version=1,
|
||||
)
|
||||
normal_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(normal_entry.entry_id)
|
||||
|
||||
# Setup an ignored config entry without USB data
|
||||
ignored_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Connect ZBT-2",
|
||||
unique_id="ignored_1",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
source="ignore",
|
||||
)
|
||||
ignored_entry.add_to_hass(hass)
|
||||
assert ignored_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "hardware/info"})
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == 1
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"hardware": [
|
||||
{
|
||||
"board": None,
|
||||
"config_entries": [normal_entry.entry_id],
|
||||
"dongle": {
|
||||
"vid": "303A",
|
||||
"pid": "4001",
|
||||
"serial_number": "80B54EEFAE18",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"description": "ZBT-2",
|
||||
},
|
||||
"name": "Home Assistant Connect ZBT-2",
|
||||
"url": "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -116,3 +116,30 @@ async def test_get_triggers(
|
||||
]
|
||||
|
||||
assert triggers == unordered(expected_triggers)
|
||||
|
||||
|
||||
async def test_get_triggers_for_removed_device(
|
||||
hass: HomeAssistant,
|
||||
mock_bridge_v2: Mock,
|
||||
v2_resources_test_data: JsonArrayType,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test triggers for a device removed from the bridge.
|
||||
|
||||
Regression test for https://github.com/home-assistant/core/issues/152937
|
||||
"""
|
||||
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
|
||||
await setup_platform(
|
||||
hass, mock_bridge_v2, [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
)
|
||||
|
||||
# Create a device entry with a Hue ID that doesn't exist on the bridge
|
||||
orphaned_device = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_bridge_v2.config_entry.entry_id,
|
||||
identifiers={(hue.DOMAIN, "non-existent-hue-device-id")},
|
||||
)
|
||||
|
||||
triggers = await async_get_device_automations(
|
||||
hass, DeviceAutomationType.TRIGGER, orphaned_device.id
|
||||
)
|
||||
assert triggers == []
|
||||
|
||||
@@ -6,6 +6,7 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceThrottledException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import (
|
||||
@@ -16,6 +17,7 @@ from iaqualink.systems.iaqua.device import (
|
||||
IaquaThermostat,
|
||||
)
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
@@ -46,7 +48,9 @@ async def _advance_coordinator_time(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Advance time to trigger coordinator update interval."""
|
||||
freezer.tick(delta=UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"])
|
||||
update_interval = UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"]
|
||||
|
||||
freezer.tick(delta=update_interval)
|
||||
async_fire_time_changed(hass, dt_util.utcnow())
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -104,6 +108,57 @@ async def test_system_refresh_failure_marks_entities_unavailable(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_system_rate_limited_keeps_entities_available(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a rate-limited update keeps entities at their last known state."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
devices = {light.name: light}
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
entity_id = entity_ids[0]
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
system.update = AsyncMock(side_effect=AqualinkServiceThrottledException)
|
||||
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
assert "Rate limited by iAquaLink" in caplog.text
|
||||
|
||||
|
||||
async def test_light_service_calls_update_entity_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Tests for the Lutron Caseta binary sensors."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pylutron_caseta import BridgeResponseError
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.lutron_caseta.binary_sensor import SCAN_INTERVAL
|
||||
@@ -114,3 +115,32 @@ async def test_battery_sensor_updates_on_schedule(
|
||||
assert unknown_state is not None
|
||||
assert unknown_state.state == STATE_UNKNOWN
|
||||
assert instance.get_battery_status.await_count == 3
|
||||
|
||||
|
||||
async def test_battery_sensor_handles_bridge_response_error(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test battery sensor handles BridgeResponseError gracefully.
|
||||
|
||||
Regression test for https://github.com/home-assistant/core/issues/169965
|
||||
"""
|
||||
instance = MockBridge()
|
||||
|
||||
def factory(*args: Any, **kwargs: Any) -> MockBridge:
|
||||
"""Return the mock bridge instance."""
|
||||
return instance
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.Header.StatusCode = "404 NotFound"
|
||||
instance.get_battery_status = AsyncMock(
|
||||
side_effect=BridgeResponseError(mock_response)
|
||||
)
|
||||
|
||||
await async_setup_integration(hass, factory)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(
|
||||
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
|
||||
)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
@@ -1338,6 +1338,33 @@ async def test_restore_state(hass: HomeAssistant, expected_state) -> None:
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_restore_state_invalid(hass: HomeAssistant) -> None:
|
||||
"""Ensure invalid restored state does not crash entity setup."""
|
||||
mock_restore_cache(hass, (State("alarm_control_panel.test", "unknown"),))
|
||||
|
||||
hass.set_state(CoreState.starting)
|
||||
mock_component(hass, "recorder")
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
alarm_control_panel.DOMAIN,
|
||||
{
|
||||
"alarm_control_panel": {
|
||||
"platform": "manual",
|
||||
"name": "test",
|
||||
"arming_time": 0,
|
||||
"trigger_time": 0,
|
||||
"disarm_after_trigger": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("alarm_control_panel.test")
|
||||
assert state
|
||||
assert state.state == AlarmControlPanelState.DISARMED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected_state",
|
||||
[
|
||||
|
||||
@@ -659,6 +659,52 @@ async def test_web_search(
|
||||
assert mock_create_stream.mock_calls[1][2]["input"][1:] == snapshot
|
||||
|
||||
|
||||
async def test_web_search_remove_citations_gpt5(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_init_component,
|
||||
mock_create_stream,
|
||||
mock_chat_log: MockChatLog, # noqa: F811
|
||||
) -> None:
|
||||
"""Test that citations are stripped for GPT-5 models with inline_citations disabled."""
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={
|
||||
**subentry.data,
|
||||
CONF_CHAT_MODEL: "gpt-5-mini",
|
||||
CONF_WEB_SEARCH: True,
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS: False,
|
||||
},
|
||||
)
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
|
||||
message = [
|
||||
"The match ended 0-2",
|
||||
" ([legaseriea.it](https://www.legaseriea.it/))",
|
||||
".",
|
||||
]
|
||||
mock_create_stream.return_value = [
|
||||
(
|
||||
*create_web_search_item(id="ws_A", output_index=0),
|
||||
*create_message_item(id="msg_A", text=message, output_index=1),
|
||||
)
|
||||
]
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass,
|
||||
"What was the score?",
|
||||
mock_chat_log.conversation_id,
|
||||
Context(),
|
||||
agent_id="conversation.openai_conversation",
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
# Citation should be stripped from the response
|
||||
assert result.response.speech["plain"]["speech"] == "The match ended 0-2."
|
||||
|
||||
|
||||
async def test_code_interpreter(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -116,3 +116,54 @@ async def test_host_updated(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
assert entry.data[CONF_HOST] == MOCK_SSDP_LOCATION
|
||||
|
||||
|
||||
async def test_ssdp_udn_as_list(hass: HomeAssistant) -> None:
|
||||
"""Test SSDP discovery when UDN is a list instead of a string.
|
||||
|
||||
Regression test for https://github.com/home-assistant/core/issues/171837
|
||||
"""
|
||||
list_udn_discovery = SsdpServiceInfo(
|
||||
ssdp_usn="usn",
|
||||
ssdp_st="st",
|
||||
ssdp_location=MOCK_SSDP_LOCATION,
|
||||
upnp={
|
||||
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
|
||||
ATTR_UPNP_UDN: [MOCK_UDN, "uuid:other"],
|
||||
},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_SSDP},
|
||||
data=list_udn_discovery,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
assert result["description_placeholders"] == {CONF_NAME: MOCK_FRIENDLY_NAME}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == MOCK_FRIENDLY_NAME
|
||||
assert result2["data"] == {CONF_HOST: MOCK_SSDP_LOCATION}
|
||||
|
||||
|
||||
async def test_ssdp_udn_as_empty_list(hass: HomeAssistant) -> None:
|
||||
"""Test SSDP discovery when UDN is an empty list."""
|
||||
empty_udn_discovery = SsdpServiceInfo(
|
||||
ssdp_usn="usn",
|
||||
ssdp_st="st",
|
||||
ssdp_location=MOCK_SSDP_LOCATION,
|
||||
upnp={
|
||||
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
|
||||
ATTR_UPNP_UDN: [],
|
||||
},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_SSDP},
|
||||
data=empty_udn_discovery,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "incomplete_discovery"
|
||||
|
||||
@@ -464,6 +464,86 @@
|
||||
"type": 1,
|
||||
"oid": "3496a041-cafc-4d5d-ab3b-7947985812dc",
|
||||
"uiClass": "VenetianBlind"
|
||||
},
|
||||
{
|
||||
"creationTime": 1613676710000,
|
||||
"lastUpdateTime": 1613676710000,
|
||||
"label": "Kitchen Sheer Screen",
|
||||
"deviceURL": "rts://1234-1234-6362/16753206",
|
||||
"shortcut": false,
|
||||
"controllableName": "rts:SheerBlindRTSComponent",
|
||||
"definition": {
|
||||
"commands": [
|
||||
{
|
||||
"commandName": "open",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "up",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "tiltPositive",
|
||||
"nparams": 2
|
||||
},
|
||||
{
|
||||
"commandName": "tiltNegative",
|
||||
"nparams": 2
|
||||
},
|
||||
{
|
||||
"commandName": "down",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "rest",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "openConfiguration",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "test",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "close",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "identify",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "moveOf",
|
||||
"nparams": 1
|
||||
},
|
||||
{
|
||||
"commandName": "my",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "stop",
|
||||
"nparams": 0
|
||||
}
|
||||
],
|
||||
"states": [],
|
||||
"dataProperties": [],
|
||||
"widgetName": "UpDownSheerScreen",
|
||||
"uiProfiles": ["OpenCloseBlind", "OpenClose"],
|
||||
"uiClass": "VenetianBlind",
|
||||
"qualifiedName": "rts:SheerBlindRTSComponent",
|
||||
"type": "ACTUATOR"
|
||||
},
|
||||
"states": [],
|
||||
"attributes": [],
|
||||
"available": true,
|
||||
"enabled": true,
|
||||
"placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2",
|
||||
"widget": "UpDownSheerScreen",
|
||||
"type": 1,
|
||||
"oid": "c198bcdd-8b8b-4dc6-a2b0-f86f7dc7c001",
|
||||
"uiClass": "VenetianBlind"
|
||||
}
|
||||
],
|
||||
"zones": [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1674,6 +1674,706 @@
|
||||
],
|
||||
"uiClass": "Light"
|
||||
}
|
||||
},
|
||||
{
|
||||
"deviceURL": "io://1234-5678-3293/12745774",
|
||||
"available": true,
|
||||
"synced": true,
|
||||
"type": 1,
|
||||
"states": [
|
||||
{
|
||||
"type": 3,
|
||||
"name": "core:StatusState",
|
||||
"value": "available"
|
||||
},
|
||||
{
|
||||
"type": 11,
|
||||
"name": "core:CommandLockLevelsState",
|
||||
"value": []
|
||||
},
|
||||
{
|
||||
"type": 3,
|
||||
"name": "core:DiscreteRSSILevelState",
|
||||
"value": "good"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"name": "core:RSSILevelState",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"type": 3,
|
||||
"name": "core:OpenClosedUnknownState",
|
||||
"value": "closed"
|
||||
},
|
||||
{
|
||||
"type": 3,
|
||||
"name": "core:NameState",
|
||||
"value": "Garage Door Rollixo"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"name": "core:PriorityLockTimerState",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"type": 3,
|
||||
"name": "io:PriorityLockOriginatorState",
|
||||
"value": "unknown"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"name": "core:Manufacturer",
|
||||
"type": 3,
|
||||
"value": "Somfy"
|
||||
},
|
||||
{
|
||||
"name": "core:FirmwareRevision",
|
||||
"type": 3,
|
||||
"value": "5105491C15"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"label": "Garage Door Rollixo",
|
||||
"controllableName": "io:DiscreteGarageOpenerIOComponent",
|
||||
"subsystemId": 0,
|
||||
"definition": {
|
||||
"commands": [
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "up"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "close"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "addLockLevel",
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "resetLockLevels"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "removeLockLevel",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "executeManufacturerProcedure",
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "writeManufacturerData",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "readManufacturerData",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "unpairAllOneWayControllers"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "stopIdentify"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "startIdentify"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "pairOneWayController",
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "delayedStopIdentify",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "sendIOKey"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "unpairAllOneWayControllersAndDeleteNode"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "wink",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "setConfigState",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "advancedRefresh",
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "setName",
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"nparams": 1,
|
||||
"commandName": "unpairOneWayController",
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"nparams": 2,
|
||||
"commandName": "runManufacturerSettingsCommand",
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "getName"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "stop"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "open"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "keepOneWayControllersAndDeleteNode"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "identify"
|
||||
},
|
||||
{
|
||||
"nparams": 0,
|
||||
"commandName": "down"
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
{
|
||||
"name": "core:AdditionalStatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:PriorityLockTimerState"
|
||||
},
|
||||
{
|
||||
"name": "io:PriorityLockLevelState"
|
||||
},
|
||||
{
|
||||
"name": "io:PriorityLockOriginatorState"
|
||||
},
|
||||
{
|
||||
"name": "core:StatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:ManufacturerSettingsState"
|
||||
},
|
||||
{
|
||||
"name": "core:ManufacturerDiagnosticsState"
|
||||
},
|
||||
{
|
||||
"name": "core:CommandLockLevelsState"
|
||||
},
|
||||
{
|
||||
"name": "core:NameState"
|
||||
},
|
||||
{
|
||||
"name": "core:DiscreteRSSILevelState"
|
||||
},
|
||||
{
|
||||
"name": "core:RSSILevelState"
|
||||
},
|
||||
{
|
||||
"name": "core:OpenClosedUnknownState"
|
||||
}
|
||||
],
|
||||
"widgetName": "DiscretePositionableGarageDoor",
|
||||
"uiClass": "GarageDoor",
|
||||
"type": "ACTUATOR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name": "zigbee:Role",
|
||||
"type": 3,
|
||||
"value": "endDevice"
|
||||
},
|
||||
{
|
||||
"name": "core:ManufacturerId",
|
||||
"type": 3,
|
||||
"value": 4640
|
||||
},
|
||||
{
|
||||
"name": "zigbee:NotificationEnable",
|
||||
"type": 6,
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"name": "zigbee:InputClusters",
|
||||
"type": 10,
|
||||
"value": ["identify", "scenes", "windowCovering"]
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationPhysicalUpperBound",
|
||||
"type": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"name": "core:Compatibilities",
|
||||
"type": 10,
|
||||
"value": ["homekit"]
|
||||
},
|
||||
{
|
||||
"name": "zigbee:SomfyEndProduct",
|
||||
"type": 3,
|
||||
"value": "2DInteriorBlind"
|
||||
},
|
||||
{
|
||||
"name": "core:MacAddress",
|
||||
"type": 3,
|
||||
"value": "dXsY/v8Gwkw="
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationPhysicalLowerBound",
|
||||
"type": 1,
|
||||
"value": -90
|
||||
}
|
||||
],
|
||||
"available": true,
|
||||
"definition": {
|
||||
"uiClass": "VenetianBlind",
|
||||
"commands": [
|
||||
{
|
||||
"commandName": "my",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "setPosition",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "close",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "removeLockLevel",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "setPositionByStep",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "setOrientation",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "advancedRefresh",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "resetFactoryDefaultsAndConfiguration",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "storeCurrentPositionToMy",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "open",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "leaveNetworkOnly",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "ping",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "setTiltingRangeConfiguration",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "stopIdentify",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "addLockLevel",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1,*p2"
|
||||
},
|
||||
{
|
||||
"commandName": "unbind",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "resetLockLevels",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "setPositionNoLimit",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "stop",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "reverseDirection",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "bind",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "identify",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "setPositionLimitation",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "setClosureAndOrientation",
|
||||
"nparams": 2,
|
||||
"paramsSig": "p1,p2"
|
||||
},
|
||||
{
|
||||
"commandName": "setName",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
},
|
||||
{
|
||||
"commandName": "getName",
|
||||
"nparams": 0
|
||||
},
|
||||
{
|
||||
"commandName": "setClosure",
|
||||
"nparams": 1,
|
||||
"paramsSig": "p1"
|
||||
}
|
||||
],
|
||||
"type": "ACTUATOR",
|
||||
"widgetName": "PositionableVenetianBlind",
|
||||
"states": [
|
||||
{
|
||||
"name": "core:OperationalStatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:SlateOrientationState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:DirectionState"
|
||||
},
|
||||
{
|
||||
"name": "core:TargetClosureState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:EncoderLiftState"
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationOperationalStatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:TargetOrientationState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:EncoderTiltState"
|
||||
},
|
||||
{
|
||||
"name": "core:OpenLimitTiltState"
|
||||
},
|
||||
{
|
||||
"name": "core:ClosedLimitTiltState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorRunModeState"
|
||||
},
|
||||
{
|
||||
"name": "core:ClosureOperationalStatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:OpenLimitLiftState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorRunningModeState"
|
||||
},
|
||||
{
|
||||
"name": "core:ErrorState"
|
||||
},
|
||||
{
|
||||
"name": "core:ConfigStatusState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorLEDFeedbackState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ControlTiltState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ControlLiftState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ReversalLiftState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:NetworkOnlineState"
|
||||
},
|
||||
{
|
||||
"name": "core:ClosureState"
|
||||
},
|
||||
{
|
||||
"name": "core:ClosedLimitLiftState"
|
||||
},
|
||||
{
|
||||
"name": "core:StatusState"
|
||||
},
|
||||
{
|
||||
"name": "core:DiscreteRSSILevelState"
|
||||
},
|
||||
{
|
||||
"name": "core:RSSILevelState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:LinkQualityIndicatorState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ZigbeeUpdateDownloadProgressState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ZigbeeUpdateState"
|
||||
},
|
||||
{
|
||||
"name": "core:FirmwareRevisionState"
|
||||
},
|
||||
{
|
||||
"name": "core:CommandLockLevelsState"
|
||||
},
|
||||
{
|
||||
"name": "core:ProductSoftwareVersionState"
|
||||
},
|
||||
{
|
||||
"name": "core:NameState"
|
||||
},
|
||||
{
|
||||
"name": "core:ProductHardwareVersionState"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:PowerSourceState"
|
||||
},
|
||||
{
|
||||
"name": "core:ManufacturerNameState"
|
||||
},
|
||||
{
|
||||
"name": "core:ProductSoftwareBuildIdState"
|
||||
},
|
||||
{
|
||||
"name": "core:MotorBoardSoftwareVersionState"
|
||||
},
|
||||
{
|
||||
"name": "core:MotorBoardSoftwareBuildIdState"
|
||||
},
|
||||
{
|
||||
"name": "core:MotorBoardHardwareVersionState"
|
||||
},
|
||||
{
|
||||
"name": "core:ProductModelNameState"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"name": "zigbee:Role"
|
||||
},
|
||||
{
|
||||
"name": "core:ManufacturerId"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:NotificationEnable"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:InputClusters"
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationPhysicalUpperBound"
|
||||
},
|
||||
{
|
||||
"name": "core:Compatibilities"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:SomfyEndProduct"
|
||||
},
|
||||
{
|
||||
"name": "core:MacAddress"
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationPhysicalLowerBound"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:OutputClusters"
|
||||
}
|
||||
]
|
||||
},
|
||||
"controllableName": "zigbee:SomfyVenetianBlindComponent",
|
||||
"deviceURL": "zigbee://1234-5678-3293/16730099",
|
||||
"states": [
|
||||
{
|
||||
"name": "core:CommandLockLevelsState",
|
||||
"type": 11,
|
||||
"value": []
|
||||
},
|
||||
{
|
||||
"name": "core:ClosureOperationalStatusState",
|
||||
"type": 3,
|
||||
"value": "not moving"
|
||||
},
|
||||
{
|
||||
"name": "core:OrientationOperationalStatusState",
|
||||
"type": 3,
|
||||
"value": "not moving"
|
||||
},
|
||||
{
|
||||
"name": "core:OperationalStatusState",
|
||||
"type": 3,
|
||||
"value": "not moving"
|
||||
},
|
||||
{
|
||||
"name": "core:DiscreteRSSILevelState",
|
||||
"type": 3,
|
||||
"value": "verylow"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:LinkQualityIndicatorState",
|
||||
"type": 1,
|
||||
"value": 54
|
||||
},
|
||||
{
|
||||
"name": "core:RSSILevelState",
|
||||
"type": 1,
|
||||
"value": -86
|
||||
},
|
||||
{
|
||||
"name": "core:StatusState",
|
||||
"type": 3,
|
||||
"value": "available"
|
||||
},
|
||||
{
|
||||
"name": "core:SlateOrientationState",
|
||||
"type": 1,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"name": "core:ClosureState",
|
||||
"type": 1,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorRunningModeState",
|
||||
"type": 3,
|
||||
"value": "motor is running normally"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorLEDFeedbackState",
|
||||
"type": 3,
|
||||
"value": "LEDs are off"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:DirectionState",
|
||||
"type": 3,
|
||||
"value": "counterClockwise"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:MotorRunModeState",
|
||||
"type": 3,
|
||||
"value": "run in normal mode"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:EncoderTiltState",
|
||||
"type": 3,
|
||||
"value": "encoder controlled"
|
||||
},
|
||||
{
|
||||
"name": "core:ConfigStatusState",
|
||||
"type": 3,
|
||||
"value": "operational"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ControlLiftState",
|
||||
"type": 3,
|
||||
"value": "lift control is closed loop"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ReversalLiftState",
|
||||
"type": 3,
|
||||
"value": "not reversal lift commands"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:EncoderLiftState",
|
||||
"type": 3,
|
||||
"value": "encoder controlled"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:NetworkOnlineState",
|
||||
"type": 3,
|
||||
"value": "online"
|
||||
},
|
||||
{
|
||||
"name": "zigbee:ControlTiltState",
|
||||
"type": 3,
|
||||
"value": "tilt control is closed loop"
|
||||
},
|
||||
{
|
||||
"name": "core:NameState",
|
||||
"type": 3,
|
||||
"value": "Living Room Venetian Blind"
|
||||
},
|
||||
{
|
||||
"name": "core:TargetOrientationState",
|
||||
"type": 1,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"name": "core:TargetClosureState",
|
||||
"type": 1,
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"name": "core:ErrorState",
|
||||
"type": 3,
|
||||
"value": "no error"
|
||||
}
|
||||
],
|
||||
"label": "Living Room Venetian Blind",
|
||||
"enabled": true,
|
||||
"subsystemId": 0,
|
||||
"synced": true,
|
||||
"type": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user