Compare commits

...

27 Commits
2026.5.0 ... rc

Author SHA1 Message Date
Franck Nijhof
dd0cdc4fc4 Bump version to 2026.5.1 2026-05-08 18:54:08 +00:00
Mick Vleeshouwer
18ea40c46d Fix tilt support for UpDownVenetianBlind (rts:VenetianBlindRTSComponent) in Overkiz (#170047) 2026-05-08 18:53:57 +00:00
Mick Vleeshouwer
a23131efc8 Fix is_closed state for DynamicGate covers in Overkiz (#170130) 2026-05-08 18:53:10 +00:00
bkobus-bbx
4940a0abae Bump blebox_uniapi to v2.5.3 (#170115) 2026-05-08 18:53:08 +00:00
Willem-Jan van Rootselaar
5f98d5ae52 Bump python-bsblan to 5.2.1 (#170100) 2026-05-08 18:53:06 +00:00
TheJulianJES
ba18cded30 Bump ZHA to 1.3.1 (#170095) 2026-05-08 18:53:04 +00:00
TheJulianJES
fb7504e9df Fix Z-Wave discovery crash with unknown node firmware version (#170090) 2026-05-08 18:53:02 +00:00
Mick Vleeshouwer
106f815a1e Fix sensors getting wrong unit from MeasuredValueType attribute in Overkiz (#170088) 2026-05-08 18:53:00 +00:00
Mick Vleeshouwer
167757762b Set is_closed state to None when a cover state returns "unknown" in Overkiz (#170081) 2026-05-08 18:52:58 +00:00
Robert Resch
3a902e1a16 Bump deebot-client to 18.3.0 (#170066) 2026-05-08 18:52:56 +00:00
Mick Vleeshouwer
85c11672d8 Bump pyOverkiz to 1.20.3 (#170060) 2026-05-08 18:52:54 +00:00
Mick Vleeshouwer
89649df20d Fix cover controls for UpDownBioclimaticPergola in Overkiz (#170058) 2026-05-08 18:52:52 +00:00
Mick Vleeshouwer
7b749b95ce Fix tilt controls for TiltOnlyVenetianBlind in Overkiz (#170055)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 18:52:50 +00:00
Mick Vleeshouwer
cc140be85c Fix is_closed state for DynamicGarageDoor in Overkiz (#170052) 2026-05-08 18:52:47 +00:00
Robert Svensson
e1ad765414 Fix websocket certificate verification Bump axis to v70 (#170038) 2026-05-08 18:48:55 +00:00
Michael
44b1fea745 Proper handling of malformed data during FRITZ!Box Tools setup (#170030) 2026-05-08 18:48:54 +00:00
Ronald van der Meer
5dd04363b2 Bump python-duco-client to 0.4.1 (#169991) 2026-05-08 18:48:51 +00:00
Ronald van der Meer
03aa979309 Bump python-duco-client to 0.4.0 (#169776) 2026-05-08 18:48:49 +00:00
Daniel Hjelseth Høyer
6fabbb354b Bump pyTibber to 0.37.5 (#169981)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-05-08 18:45:50 +00:00
Erik Montnemery
f644448d0f Add support for options to todo triggers (#169947) 2026-05-08 18:45:48 +00:00
G Johansson
4e61581cd8 Bump holidays to 0.96 (#169939) 2026-05-08 18:45:47 +00:00
puddly
6f87d02b72 Bump serialx to 1.7.1 (#169928) 2026-05-08 18:45:45 +00:00
Joakim Plate
348f6149b4 Update gardena ble to 2.8.1 (#169914) 2026-05-08 18:45:43 +00:00
Stefan Agner
a4227ef1bc Fix hassio auth IndexError on Supervisor Unix socket requests (#169911) 2026-05-08 18:45:41 +00:00
Jeef
aac49a567f Fix IntelliFire setup recovery (#169739) 2026-05-08 18:45:39 +00:00
Rob Treacy
76b878b136 Fix WiZ Light config flow timeout by properly closing UDP connections (#168456) 2026-05-08 18:45:37 +00:00
th3spis
2d05931683 Added wfsens as a occupancy source in wiz (#166799)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-08 18:45:35 +00:00
50 changed files with 1643 additions and 122 deletions

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["serialx==1.7.0"]
"requirements": ["serialx==1.7.1"]
}

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==69"],
"requirements": ["axis==70"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.2"],
"requirements": ["blebox-uniapi==2.5.3"],
"zeroconf": ["_bbxsrv._tcp.local."]
}

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.2.0"],
"requirements": ["python-bsblan==5.2.1"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.10"],
"requirements": ["python-duco-client==0.4.1"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==18.2.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==18.3.0"]
}

View File

@@ -10,6 +10,7 @@ from functools import partial
import logging
import re
from typing import Any, TypedDict, cast
from xml.etree.ElementTree import ParseError
from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import FritzActionError
@@ -26,7 +27,7 @@ from homeassistant.components.device_tracker import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -228,7 +229,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection)
self.fritz_status = FritzStatus(fc=self.connection)
self.fritz_call = FritzCall(fc=self.connection)
info = self.fritz_status.get_device_info()
try:
info = self.fritz_status.get_device_info()
except ParseError as ex:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="error_parse_device_info",
) from ex
_LOGGER.debug(
"gathered device info of %s %s",

View File

@@ -185,6 +185,9 @@
"config_entry_not_found": {
"message": "Failed to perform action \"{service}\". Config entry for target not found"
},
"error_parse_device_info": {
"message": "Error parsing device info. Please check the system event log of your FRITZ!Box for malformed data and clear the event list."
},
"error_refresh_hosts_info": {
"message": "Error refreshing hosts info"
},

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==2.4.0"]
"requirements": ["gardena-bluetooth==2.8.1"]
}

View File

@@ -12,6 +12,7 @@ import voluptuous as vol
from homeassistant.auth.models import User
from homeassistant.auth.providers import homeassistant as auth_ha
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView
from homeassistant.components.http.const import is_supervisor_unix_socket_request
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
@@ -41,14 +42,18 @@ class HassIOBaseAuth(HomeAssistantView):
def _check_access(self, request: web.Request) -> None:
"""Check if this call is from Supervisor."""
# Check caller IP
hassio_ip = os.environ["SUPERVISOR"].split(":")[0]
assert request.transport
if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address(
hassio_ip
):
_LOGGER.error("Invalid auth request from %s", request.remote)
raise HTTPUnauthorized
# Requests over the Supervisor Unix socket are authenticated by the
# http auth middleware as the Supervisor user, so the caller-IP check
# below does not apply (and would crash, since `peername` is empty for
# Unix sockets). The user-ID check still runs to ensure only the
# Supervisor user can reach this endpoint.
if not is_supervisor_unix_socket_request(request):
hassio_ip = os.environ["SUPERVISOR"].split(":")[0]
assert request.transport
peername = request.transport.get_extra_info("peername")
if not peername or ip_address(peername[0]) != ip_address(hassio_ip):
_LOGGER.error("Invalid auth request from %s", request.remote)
raise HTTPUnauthorized
# Check caller token
if request[KEY_HASS_USER].id != self.user.id:

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.95", "babel==2.15.0"]
"requirements": ["holidays==0.96", "babel==2.15.0"]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.4.0"]
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.8.1"]
}

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import aiohttp
from intellifire4py import UnifiedFireplace
from intellifire4py.cloud_interface import IntelliFireCloudInterface
from intellifire4py.const import IntelliFireApiMode
@@ -155,6 +156,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry)
raise ConfigEntryNotReady(
"Initialization of fireplace timed out after 10 minutes"
) from err
except (aiohttp.ClientConnectionError, ConnectionError) as err:
raise ConfigEntryNotReady(
"Error communicating with fireplace during initialization"
) from err
# Construct coordinator
data_update_coordinator = IntellifireDataUpdateCoordinator(hass, entry, fireplace)

View File

@@ -15,6 +15,7 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
@@ -289,10 +290,8 @@ class IntelliFireOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
if user_input is not None:
# Validate connectivity for requested modes if runtime data is available
coordinator = self.config_entry.runtime_data
if coordinator is not None:
fireplace = coordinator.fireplace
if self.config_entry.state is ConfigEntryState.LOADED:
fireplace = self.config_entry.runtime_data.fireplace
# Refresh connectivity status before validating
await fireplace.async_validate_connectivity()

View File

@@ -101,14 +101,21 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
close_tilt_command=OverkizCommand.LOWER_CLOSE,
stop_tilt_command=OverkizCommand.STOP,
),
# Needs override to remove open/close commands
# Needs override to add support for very specific tilt commands
# uiClass is VenetianBlind
OverkizCoverDescription(
key=UIWidget.TILT_ONLY_VENETIAN_BLIND,
device_class=CoverDeviceClass.BLIND,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
# Position commands fully open/close the tilt
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
# Tilt commands move the tilt with a few degrees
open_tilt_command=OverkizCommand.TILT_POSITIVE,
open_tilt_command_args=(1, 0),
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
close_tilt_command_args=(1, 0),
stop_tilt_command=OverkizCommand.STOP,
),
# Needs override to support very specific tilt commands (rts:ExteriorVenetianBlindRTSComponent)
@@ -125,6 +132,57 @@ 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:VenetianBlindRTSComponent)
# uiClass is VenetianBlind
OverkizCoverDescription(
key=UIWidget.UP_DOWN_VENETIAN_BLIND,
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 PositionableGarageDoor reports
# core:OpenClosedUnknownState instead of core:OpenClosedState
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.POSITIONABLE_GARAGE_DOOR,
device_class=CoverDeviceClass.GARAGE,
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_UNKNOWN,
),
# Needs override since PositionableGarageDoorWithPartialPosition reports
# core:OpenClosedPartialState instead of core:OpenClosedState
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.POSITIONABLE_GARAGE_DOOR_WITH_PARTIAL_POSITION,
device_class=CoverDeviceClass.GARAGE,
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_PARTIAL,
),
# Needs override since DiscreteGateWithPedestrianPosition reports
# core:OpenClosedPedestrianState instead of core:OpenClosedState
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.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 to support this Generic device (rts:GenericRTSComponent)
# uiClass is Generic (not mapped to cover as this is a Generic device class)
OverkizCoverDescription(
@@ -201,7 +259,7 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
set_position_command=OverkizCommand.SET_CLOSURE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
stop_command=OverkizCommand.STOP,
),
OverkizCoverDescription(
@@ -209,12 +267,15 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
stop_command=OverkizCommand.STOP,
),
OverkizCoverDescription(
key=UIClass.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,
@@ -392,6 +453,8 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
"""Return if the cover is closed."""
if is_closed_state := self.entity_description.is_closed_state:
if state := self.device.states.get(is_closed_state):
if state.value == OverkizCommandParam.UNKNOWN:
return None
return state.value == OverkizCommandParam.CLOSED
if (position := self.current_cover_position) is not None:

View File

@@ -22,6 +22,8 @@ COMMANDS_WITHOUT_DELAY = [
OverkizCommand.ON,
OverkizCommand.ON_WITH_TIMER,
OverkizCommand.TEST,
OverkizCommand.TILT_POSITIVE,
OverkizCommand.TILT_NEGATIVE,
]

View File

@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.20.0"],
"requirements": ["pyoverkiz==1.20.3"],
"zeroconf": [
{
"name": "gateway*",

View File

@@ -10,6 +10,7 @@ from pyoverkiz.enums import OverkizAttribute, OverkizState, UIWidget
from pyoverkiz.types import StateType as OverkizStateType
from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -606,10 +607,25 @@ class OverkizStateSensor(OverkizDescriptiveEntity, SensorEntity):
if (unit := attrs[OverkizAttribute.CORE_MEASURED_VALUE_TYPE]) and (
unit_value := unit.value_as_str
):
return OVERKIZ_UNIT_TO_HA.get(unit_value, default_unit)
ha_unit = OVERKIZ_UNIT_TO_HA.get(unit_value, default_unit)
if self._is_unit_valid_for_device_class(ha_unit):
return ha_unit
return default_unit
def _is_unit_valid_for_device_class(self, unit: str) -> bool:
"""Check if a unit is valid for this sensor's device class.
The device-level core:MeasuredValueType attribute describes the primary
sensor (e.g. luminance/temperature), but must not override the unit of
unrelated sensors on the same device (e.g. RSSI).
"""
if not (device_class := self.entity_description.device_class):
return True
if (valid_units := DEVICE_CLASS_UNITS.get(device_class)) is None:
return True
return unit in valid_units
class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity):
"""Representation of an Overkiz HomeKit Setup Code."""

View File

@@ -4,5 +4,5 @@
"codeowners": ["@fabaff"],
"documentation": "https://www.home-assistant.io/integrations/serial",
"iot_class": "local_polling",
"requirements": ["serialx==1.7.0"]
"requirements": ["serialx==1.7.1"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"requirements": ["pyTibber==0.37.4"]
"requirements": ["pyTibber==0.37.5"]
}

View File

@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, cast, override
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET
from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -25,6 +25,7 @@ from .const import DATA_COMPONENT, DOMAIN, TodoItemStatus
ITEM_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
vol.Required(CONF_OPTIONS, default={}): {},
}
)

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.0"]
"requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.1"]
}

View File

@@ -6,7 +6,6 @@ import logging
from typing import Any
from pywizlight import PilotParser, wizlight
from pywizlight.bulb import PIR_SOURCE
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
@@ -20,6 +19,7 @@ from .const import (
DISCOVER_SCAN_TIMEOUT,
DISCOVERY_INTERVAL,
DOMAIN,
OCCUPANCY_SOURCES,
SIGNAL_WIZ_PIR,
WIZ_CONNECT_EXCEPTIONS,
)
@@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WizConfigEntry) -> bool:
"""Receive a push update."""
_LOGGER.debug("%s: Got push update: %s", bulb.mac, state.pilotResult)
coordinator.async_set_updated_data(coordinator.data)
if state.get_source() == PIR_SOURCE:
if state.get_source() in OCCUPANCY_SOURCES:
async_dispatcher_send(hass, SIGNAL_WIZ_PIR.format(bulb.mac))
await bulb.start_push(_async_push_update)

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
from collections.abc import Callable
from pywizlight.bulb import PIR_SOURCE
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
@@ -16,7 +14,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, SIGNAL_WIZ_PIR
from .const import DOMAIN, OCCUPANCY_SOURCES, SIGNAL_WIZ_PIR
from .coordinator import WizConfigEntry, WizData
from .entity import WizEntity
@@ -75,5 +73,5 @@ class WizOccupancyEntity(WizEntity, BinarySensorEntity):
@callback
def _async_update_attrs(self) -> None:
"""Handle updating _attr values."""
if self._device.state.get_source() == PIR_SOURCE:
if self._device.state.get_source() in OCCUPANCY_SOURCES:
self._attr_is_on = self._device.status

View File

@@ -81,6 +81,8 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN):
exc_info=True,
)
raise AbortFlow("cannot_connect") from ex
finally:
await bulb.async_close()
self._name = name_from_bulb_type_and_mac(bulbtype, device.mac_address)
async def async_step_discovery_confirm(
@@ -118,6 +120,8 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN):
bulbtype = await bulb.get_bulbtype()
except WIZ_CONNECT_EXCEPTIONS:
return self.async_abort(reason="cannot_connect")
finally:
await bulb.async_close()
return self.async_create_entry(
title=name_from_bulb_type_and_mac(bulbtype, device.mac_address),
@@ -182,6 +186,8 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN):
title=name,
data=user_input,
)
finally:
await bulb.async_close()
return self.async_show_form(
step_id="user",

View File

@@ -2,6 +2,7 @@
from datetime import timedelta
from pywizlight.bulb import PIR_SOURCE
from pywizlight.exceptions import (
WizLightConnectionError,
WizLightNotKnownBulb,
@@ -24,3 +25,4 @@ WIZ_EXCEPTIONS = (
WIZ_CONNECT_EXCEPTIONS = (WizLightNotKnownBulb, *WIZ_EXCEPTIONS)
SIGNAL_WIZ_PIR = "wiz_pir_{}"
OCCUPANCY_SOURCES = frozenset({PIR_SOURCE, "wfsens"})

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.95"]
"requirements": ["holidays==0.96"]
}

View File

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.3.0"],
"requirements": ["zha==1.3.1"],
"usb": [
{
"description": "*2652*",

View File

@@ -1284,19 +1284,19 @@ def async_discover_single_value(
continue
# check firmware_version_range
if schema.firmware_version_range is not None and (
(
if schema.firmware_version_range is not None:
# skip schema if device firmware version is unknown
if value.node.firmware_version is None:
continue
node_firmware = AwesomeVersion(value.node.firmware_version)
if (
schema.firmware_version_range.min is not None
and schema.firmware_version_range.min_ver
> AwesomeVersion(value.node.firmware_version)
)
or (
and schema.firmware_version_range.min_ver > node_firmware
) or (
schema.firmware_version_range.max is not None
and schema.firmware_version_range.max_ver
< AwesomeVersion(value.node.firmware_version)
)
):
continue
and schema.firmware_version_range.max_ver < node_firmware
):
continue
# check device_class_generic
# If the value has an endpoint but it is missing on the node

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__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)

View File

@@ -63,7 +63,7 @@ PyTurboJPEG==1.8.3
PyYAML==6.0.3
requests==2.33.1
securetar==2026.4.1
serialx==1.7.0
serialx==1.7.1
SQLAlchemy==2.0.49
standard-aifc==3.13.0
standard-telnetlib==3.13.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.5.0"
version = "2026.5.1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

22
requirements_all.txt generated
View File

@@ -600,7 +600,7 @@ avea==1.6.1
# avion==0.10
# homeassistant.components.axis
axis==69
axis==70
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7
@@ -654,7 +654,7 @@ bleak-retry-connector==4.6.0
bleak==2.1.1
# homeassistant.components.blebox
blebox-uniapi==2.5.2
blebox-uniapi==2.5.3
# homeassistant.components.blink
blinkpy==0.25.2
@@ -794,7 +794,7 @@ debugpy==1.8.17
decora-wifi==1.4
# homeassistant.components.ecovacs
deebot-client==18.2.0
deebot-client==18.3.0
# homeassistant.components.ihc
# homeassistant.components.ohmconnect
@@ -1048,7 +1048,7 @@ gTTS==2.5.3
# homeassistant.components.gardena_bluetooth
# homeassistant.components.husqvarna_automower_ble
gardena-bluetooth==2.4.0
gardena-bluetooth==2.8.1
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.14
@@ -1242,7 +1242,7 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.95
holidays==0.96
# homeassistant.components.frontend
home-assistant-frontend==20260429.3
@@ -1941,7 +1941,7 @@ pyRFXtrx==0.31.1
pySDCP==1
# homeassistant.components.tibber
pyTibber==0.37.4
pyTibber==0.37.5
# homeassistant.components.dlink
pyW215==0.8.0
@@ -2386,7 +2386,7 @@ pyotgw==2.2.3
pyotp==2.9.0
# homeassistant.components.overkiz
pyoverkiz==1.20.0
pyoverkiz==1.20.3
# homeassistant.components.palazzetti
pypalazzetti==0.1.20
@@ -2566,7 +2566,7 @@ python-awair==0.2.5
python-blockchain-api==0.0.2
# homeassistant.components.bsblan
python-bsblan==5.2.0
python-bsblan==5.2.1
# homeassistant.components.citybikes
python-citybikes==0.3.3
@@ -2581,7 +2581,7 @@ python-digitalocean==1.13.2
python-dropbox-api==0.1.3
# homeassistant.components.duco
python-duco-client==0.3.10
python-duco-client==0.4.1
# homeassistant.components.ecobee
python-ecobee-api==0.3.2
@@ -2945,7 +2945,7 @@ sentry-sdk==2.48.0
# homeassistant.components.acer_projector
# homeassistant.components.serial
# homeassistant.components.usb
serialx==1.7.0
serialx==1.7.1
# homeassistant.components.sfr_box
sfrbox-api==0.1.1
@@ -3404,7 +3404,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.3.0
zha==1.3.1
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -552,7 +552,7 @@ autoskope_client==1.4.1
av==16.0.1
# homeassistant.components.axis
axis==69
axis==70
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.7
@@ -591,7 +591,7 @@ bleak-retry-connector==4.6.0
bleak==2.1.1
# homeassistant.components.blebox
blebox-uniapi==2.5.2
blebox-uniapi==2.5.3
# homeassistant.components.blink
blinkpy==0.25.2
@@ -706,7 +706,7 @@ debugpy==1.8.17
decora-wifi==1.4
# homeassistant.components.ecovacs
deebot-client==18.2.0
deebot-client==18.3.0
# homeassistant.components.ihc
# homeassistant.components.ohmconnect
@@ -930,7 +930,7 @@ gTTS==2.5.3
# homeassistant.components.gardena_bluetooth
# homeassistant.components.husqvarna_automower_ble
gardena-bluetooth==2.4.0
gardena-bluetooth==2.8.1
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.14
@@ -1106,7 +1106,7 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.95
holidays==0.96
# homeassistant.components.frontend
home-assistant-frontend==20260429.3
@@ -1684,7 +1684,7 @@ pyHomee==1.3.8
pyRFXtrx==0.31.1
# homeassistant.components.tibber
pyTibber==0.37.4
pyTibber==0.37.5
# homeassistant.components.dlink
pyW215==0.8.0
@@ -2045,7 +2045,7 @@ pyotgw==2.2.3
pyotp==2.9.0
# homeassistant.components.overkiz
pyoverkiz==1.20.0
pyoverkiz==1.20.3
# homeassistant.components.palazzetti
pypalazzetti==0.1.20
@@ -2198,7 +2198,7 @@ python-MotionMount==2.3.0
python-awair==0.2.5
# homeassistant.components.bsblan
python-bsblan==5.2.0
python-bsblan==5.2.1
# homeassistant.components.citybikes
python-citybikes==0.3.3
@@ -2207,7 +2207,7 @@ python-citybikes==0.3.3
python-dropbox-api==0.1.3
# homeassistant.components.duco
python-duco-client==0.3.10
python-duco-client==0.4.1
# homeassistant.components.ecobee
python-ecobee-api==0.3.2
@@ -2511,7 +2511,7 @@ sentry-sdk==2.48.0
# homeassistant.components.acer_projector
# homeassistant.components.serial
# homeassistant.components.usb
serialx==1.7.0
serialx==1.7.1
# homeassistant.components.sfr_box
sfrbox-api==0.1.1
@@ -2895,7 +2895,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.3.0
zha==1.3.1
# homeassistant.components.zinvolt
zinvolt==0.4.3

View File

@@ -4,10 +4,12 @@
'board_info': dict({
'box_name': 'SILENT_CONNECT',
'box_sub_type_name': 'Eu',
'public_api_version': None,
'serial_board_box': '**REDACTED**',
'serial_board_comm': '**REDACTED**',
'serial_duco_box': '**REDACTED**',
'serial_duco_comm': '**REDACTED**',
'software_version': None,
}),
'duco_diagnostics': list([
dict({

View File

@@ -2,6 +2,7 @@
import re
from unittest.mock import patch
from xml.etree.ElementTree import ParseError
from freezegun.api import FrozenDateTimeFactory
import pytest
@@ -116,6 +117,25 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None:
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_fail_parse_error(hass: HomeAssistant, fc_class_mock) -> None:
"""Test setup failure due to parse error while fetching device data."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA)
entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.fritz.coordinator.FritzStatus.get_device_info"
) as fs_mock,
):
fs_mock.side_effect = ParseError("boom")
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert entry.error_reason_translation_key == "error_parse_device_info"
async def test_upnp_missing(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,

View File

@@ -1,11 +1,17 @@
"""The tests for the hassio component."""
from contextlib import AbstractContextManager, ExitStack as DefaultContext
from http import HTTPStatus
from unittest.mock import Mock, patch
from unittest.mock import MagicMock, Mock, patch
from aiohttp.test_utils import TestClient
from aiohttp.web_exceptions import HTTPUnauthorized
import pytest
from homeassistant.auth.providers.homeassistant import InvalidAuth
from homeassistant.components.hassio.auth import HassIOBaseAuth
from homeassistant.components.hassio.const import DATA_CONFIG_STORE
from homeassistant.core import HomeAssistant
async def test_auth_success(hassio_client_supervisor: TestClient) -> None:
@@ -162,6 +168,45 @@ async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None:
assert resp.status == HTTPStatus.UNAUTHORIZED
@pytest.mark.parametrize(
("peername", "unix_socket", "expectation"),
[
# Unix socket transports report an empty string for peername. Before
# the fix this raised IndexError on `peername[0]`.
("", True, DefaultContext()),
# Defensive: a TCP transport with no peername at all should be
# rejected, not crash.
(None, False, pytest.raises(HTTPUnauthorized)),
],
)
@pytest.mark.usefixtures("hassio_stubs")
async def test_check_access_unix_socket_or_missing_peername(
hass: HomeAssistant,
peername: str | None,
unix_socket: bool,
expectation: AbstractContextManager,
) -> None:
"""Test _check_access handles Unix socket requests and missing peername."""
hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user
assert hassio_user_id is not None
user = await hass.auth.async_get_user(hassio_user_id)
assert user is not None
auth_view = HassIOBaseAuth(hass, user)
request = MagicMock()
request.transport.get_extra_info.return_value = peername
request.__getitem__.return_value = user
with (
patch(
"homeassistant.components.hassio.auth.is_supervisor_unix_socket_request",
return_value=unix_socket,
),
expectation,
):
auth_view._check_access(request)
async def test_password_no_user(hassio_client_supervisor: TestClient) -> None:
"""Test changing password for invalid user."""
resp = await hassio_client_supervisor.post(

View File

@@ -272,6 +272,38 @@ async def test_options_flow(
}
async def test_options_flow_allows_submit_when_not_loaded(
hass: HomeAssistant,
mock_config_entry_current: MockConfigEntry,
) -> None:
"""Test options flow allows submit when runtime data is missing."""
config_entry = MockConfigEntry(
domain=DOMAIN,
version=mock_config_entry_current.version,
minor_version=mock_config_entry_current.minor_version,
data=dict(mock_config_entry_current.data),
options=dict(mock_config_entry_current.options),
unique_id=mock_config_entry_current.unique_id,
state=config_entries.ConfigEntryState.SETUP_ERROR,
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{CONF_READ_MODE: API_MODE_CLOUD, CONF_CONTROL_MODE: API_MODE_LOCAL},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_READ_MODE: API_MODE_CLOUD,
CONF_CONTROL_MODE: API_MODE_LOCAL,
}
async def test_options_flow_local_read_unavailable(
hass: HomeAssistant,
mock_config_entry_current: MockConfigEntry,

View File

@@ -1,8 +1,10 @@
"""Test the IntelliFire config flow."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import aiohttp
from intellifire4py.const import IntelliFireApiMode
import pytest
from homeassistant.components.intellifire import CONF_USER_ID
from homeassistant.components.intellifire.const import (
@@ -159,22 +161,28 @@ async def test_init_with_no_username(hass: HomeAssistant, mock_apis_single_fp) -
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_connectivity_bad(
@pytest.mark.parametrize(
"setup_error",
[aiohttp.ClientConnectionError, ConnectionError, TimeoutError],
)
async def test_connectivity_error_during_setup_retries(
hass: HomeAssistant,
mock_config_entry_current,
mock_apis_single_fp,
mock_config_entry_current: MockConfigEntry,
mock_apis_single_fp: tuple[AsyncMock, AsyncMock, MagicMock],
setup_error: type[Exception],
) -> None:
"""Test a timeout error on the setup flow."""
"""Test a connection error during setup retries the config entry."""
with patch(
"homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common",
new_callable=AsyncMock,
side_effect=TimeoutError,
side_effect=setup_error,
):
mock_config_entry_current.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_current.entry_id)
await hass.async_block_till_done()
assert mock_config_entry_current.state is ConfigEntryState.SETUP_RETRY
assert len(hass.states.async_all()) == 0

View File

@@ -323,6 +323,147 @@
"type": 1,
"oid": "ef870fe7-daf3-4f33-b91e-62b2a809ef4e",
"uiClass": "RollerShutter"
},
{
"creationTime": 1613676700000,
"lastUpdateTime": 1613676700000,
"label": "Jaloezie",
"deviceURL": "rts://1234-1234-6362/16730044",
"shortcut": false,
"controllableName": "rts:TiltOnlyVenetianBlindRTSComponent",
"definition": {
"commands": [
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "stop",
"nparams": 0
},
{
"commandName": "tiltPositive",
"nparams": 2
},
{
"commandName": "tiltNegative",
"nparams": 2
},
{
"commandName": "my",
"nparams": 1
},
{
"commandName": "identify",
"nparams": 0
}
],
"states": [],
"attributes": [],
"dataProperties": [],
"widgetName": "TiltOnlyVenetianBlind",
"uiProfiles": ["OpenClose"],
"uiClass": "VenetianBlind",
"qualifiedName": "rts:TiltOnlyVenetianBlindRTSComponent",
"type": "ACTUATOR"
},
"states": [],
"attributes": [],
"available": true,
"enabled": true,
"placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2",
"widget": "TiltOnlyVenetianBlind",
"type": 1,
"oid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"uiClass": "VenetianBlind"
},
{
"creationTime": 1613676700000,
"lastUpdateTime": 1613676700000,
"label": "Office Venetian Blind",
"deviceURL": "rts://1234-1234-6362/16747291",
"shortcut": false,
"controllableName": "rts:VenetianBlindRTSComponent",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 1
},
{
"commandName": "down",
"nparams": 1
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "my",
"nparams": 1
},
{
"commandName": "open",
"nparams": 1
},
{
"commandName": "rest",
"nparams": 1
},
{
"commandName": "stop",
"nparams": 1
},
{
"commandName": "test",
"nparams": 0
},
{
"commandName": "up",
"nparams": 1
},
{
"commandName": "moveOf",
"nparams": 2
},
{
"commandName": "openConfiguration",
"nparams": 1
},
{
"commandName": "tiltNegative",
"nparams": 2
},
{
"commandName": "tiltPositive",
"nparams": 2
}
],
"states": [],
"dataProperties": [
{
"value": "0",
"qualifiedName": "core:identifyInterval"
}
],
"widgetName": "UpDownVenetianBlind",
"uiProfiles": ["OpenCloseBlind", "OpenClose"],
"uiClass": "VenetianBlind",
"qualifiedName": "rts:VenetianBlindRTSComponent",
"type": "ACTUATOR"
},
"attributes": [],
"available": true,
"enabled": true,
"placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2",
"widget": "UpDownVenetianBlind",
"type": 1,
"oid": "3496a041-cafc-4d5d-ab3b-7947985812dc",
"uiClass": "VenetianBlind"
}
],
"zones": [],

View File

@@ -644,6 +644,76 @@
"oid": "0df95043-359c-4b3d-9147-f49b2b35053c",
"uiClass": "GarageDoor"
},
{
"creationTime": 1521964729000,
"lastUpdateTime": 1521964729000,
"label": "Basement Garage Door",
"deviceURL": "io://1234-1234-6233/1166864",
"shortcut": false,
"controllableName": "io:GarageOpenerIOComponent",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "setClosure",
"nparams": 1
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [
{
"type": "ContinuousState",
"qualifiedName": "core:ClosureState"
},
{
"type": "DiscreteState",
"values": ["closed", "open", "unknown"],
"qualifiedName": "core:OpenClosedUnknownState"
}
],
"widgetName": "PositionableGarageDoor",
"uiProfiles": [
"StatefulCloseableGarageOpener",
"StatefulCloseable",
"Closeable",
"OpenClose"
],
"uiClass": "GarageDoor",
"qualifiedName": "io:GarageOpenerIOComponent",
"type": "ACTUATOR"
},
"states": [
{
"name": "core:OpenClosedUnknownState",
"type": 3,
"value": "unknown"
}
],
"attributes": [
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
}
],
"available": true,
"enabled": true,
"placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3",
"widget": "PositionableGarageDoor",
"type": 1,
"oid": "1df95043-359c-4b3d-9147-f49b2b35053d",
"uiClass": "GarageDoor"
},
{
"creationTime": 1552163547000,
"lastUpdateTime": 1552163547000,
@@ -7370,6 +7440,338 @@
"widget": "DiscreteGateWithPedestrianPosition",
"oid": "6ba9b1de-8037-41d7-9150-21f7d5f49a3f",
"uiClass": "Gate"
},
{
"creationTime": 1521964729000,
"lastUpdateTime": 1521964729000,
"label": "Garage Door",
"deviceURL": "io://1234-1234-6233/16730050",
"shortcut": false,
"controllableName": "io:DynamicGarageDoorComponent",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "getName",
"nparams": 0
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [
{
"type": "DiscreteState",
"values": ["closed", "open"],
"qualifiedName": "core:OpenClosedState"
},
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:StatusState"
}
],
"widgetName": "DynamicGarageDoor",
"uiProfiles": ["OpenClose"],
"uiClass": "GarageDoor",
"qualifiedName": "io:DynamicGarageDoorComponent",
"type": "ACTUATOR"
},
"states": [
{
"name": "core:OpenClosedState",
"type": 3,
"value": "closed"
},
{
"name": "core:StatusState",
"type": 3,
"value": "available"
}
],
"attributes": [
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
}
],
"available": true,
"enabled": true,
"placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3",
"widget": "DynamicGarageDoor",
"type": 1,
"oid": "a5d47351-52d0-4a4c-a52d-abcdef123456",
"uiClass": "GarageDoor"
},
{
"creationTime": 1521964729000,
"lastUpdateTime": 1521964729000,
"label": "OGP Garage Door",
"deviceURL": "ogp://1234-1234-6233/6632544",
"shortcut": false,
"controllableName": "ogp:GarageDoor",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:AvailabilityState"
},
{
"type": "DiscreteState",
"values": ["closed", "open"],
"qualifiedName": "core:OpenClosedState"
},
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:StatusState"
}
],
"widgetName": "DynamicGarageDoor",
"uiProfiles": ["StatefulOpenClose", "OpenClose"],
"uiClass": "GarageDoor",
"qualifiedName": "ogp:GarageDoor",
"type": "ACTUATOR"
},
"states": [
{
"name": "core:AvailabilityState",
"type": 3,
"value": "available"
},
{
"name": "core:StatusState",
"type": 3,
"value": "available"
},
{
"name": "core:OpenClosedState",
"type": 3,
"value": "closed"
}
],
"attributes": [
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
},
{
"name": "core:Technology",
"type": 3,
"value": "io2way"
}
],
"available": true,
"enabled": true,
"placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3",
"widget": "DynamicGarageDoor",
"type": 1,
"oid": "2492f7ae-3711-4160-9dae-e8910b708ce1",
"uiClass": "GarageDoor"
},
{
"creationTime": 1521964729000,
"lastUpdateTime": 1521964729000,
"label": "Partial Garage Door",
"deviceURL": "io://1234-1234-6233/7433515",
"shortcut": false,
"controllableName": "io:DiscreteGarageOpenerWithPartialPositionIOComponent",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "partialPosition",
"nparams": 0
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [
{
"type": "DiscreteState",
"values": ["closed", "open", "partial"],
"qualifiedName": "core:OpenClosedPartialState"
},
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:StatusState"
}
],
"widgetName": "PositionableGarageDoorWithPartialPosition",
"uiProfiles": ["OpenClose"],
"uiClass": "GarageDoor",
"qualifiedName": "io:DiscreteGarageOpenerWithPartialPositionIOComponent",
"type": "ACTUATOR"
},
"states": [
{
"name": "core:StatusState",
"type": 3,
"value": "available"
},
{
"name": "core:OpenClosedPartialState",
"type": 3,
"value": "closed"
}
],
"attributes": [
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
}
],
"available": true,
"enabled": true,
"placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3",
"widget": "PositionableGarageDoorWithPartialPosition",
"type": 1,
"oid": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"uiClass": "GarageDoor"
},
{
"creationTime": 1660551260000,
"lastUpdateTime": 1660551260000,
"label": "OGP Gate",
"deviceURL": "ogp://1234-1234-6233/10410217",
"shortcut": false,
"controllableName": "ogp:Gate",
"definition": {
"commands": [
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "setName",
"nparams": 1
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:AvailabilityState"
},
{
"type": "DataState",
"qualifiedName": "core:NameState"
},
{
"type": "DiscreteState",
"values": ["closed", "open"],
"qualifiedName": "core:OpenClosedState"
},
{
"type": "DiscreteState",
"values": ["available", "unavailable"],
"qualifiedName": "core:StatusState"
}
],
"dataProperties": [],
"widgetName": "DynamicGate",
"uiProfiles": ["StatefulOpenClose", "OpenClose"],
"uiClass": "Gate",
"qualifiedName": "ogp:Gate",
"type": "ACTUATOR"
},
"states": [
{
"name": "core:NameState",
"type": 3,
"value": "OGP Gate"
},
{
"name": "core:AvailabilityState",
"type": 3,
"value": "available"
},
{
"name": "core:StatusState",
"type": 3,
"value": "available"
},
{
"name": "core:OpenClosedState",
"type": 3,
"value": "open"
}
],
"attributes": [
{
"name": "core:Technology",
"type": 3,
"value": "io2way"
},
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
}
],
"available": true,
"enabled": true,
"placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3",
"type": 1,
"widget": "DynamicGate",
"oid": "a8d3e9f1-4b2c-4d5e-8f6a-1234567890ab",
"uiClass": "Gate"
}
],
"zones": [],

View File

@@ -741,6 +741,71 @@
"uiClass": "Pergola"
}
},
{
"deviceURL": "rts://1234-5678-3293/16757826",
"available": true,
"synced": true,
"type": 1,
"states": [],
"label": "Kitchen Pergola",
"subsystemId": 0,
"attributes": [],
"enabled": true,
"controllableName": "rts:BioclimaticPergolaRTSComponent",
"definition": {
"states": [],
"widgetName": "UpDownBioclimaticPergola",
"type": "ACTUATOR",
"attributes": [],
"commands": [
{
"commandName": "identify",
"nparams": 0
},
{
"nparams": 2,
"commandName": "tiltPositive",
"paramsSig": "p1,p2"
},
{
"nparams": 0,
"commandName": "my",
"paramsSig": "*p1"
},
{
"nparams": 0,
"commandName": "stop",
"paramsSig": "*p1"
},
{
"nparams": 2,
"commandName": "tiltNegative",
"paramsSig": "p1,p2"
},
{
"nparams": 0,
"commandName": "close",
"paramsSig": "*p1"
},
{
"nparams": 0,
"commandName": "down",
"paramsSig": "*p1"
},
{
"nparams": 0,
"commandName": "open",
"paramsSig": "*p1"
},
{
"nparams": 0,
"commandName": "up",
"paramsSig": "*p1"
}
],
"uiClass": "Pergola"
}
},
{
"creationTime": 1686173907452,
"deviceURL": "rts://1234-5678-3293/16757362",

View File

@@ -323,6 +323,60 @@
'state': 'open',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.jaloezie',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.BLIND: 'blind'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 123>,
'translation_key': None,
'unique_id': 'rts://1234-1234-6362/16730044',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'assumed_state': True,
'device_class': 'blind',
'friendly_name': 'Jaloezie',
'is_closed': None,
'supported_features': <CoverEntityFeature: 123>,
}),
'context': <ANY>,
'entity_id': 'cover.jaloezie',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.kitchen_shutter-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -377,6 +431,60 @@
'state': 'unknown',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.office_venetian_blind-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.office_venetian_blind',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.BLIND: 'blind'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 123>,
'translation_key': None,
'unique_id': 'rts://1234-1234-6362/16747291',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.office_venetian_blind-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'assumed_state': True,
'device_class': 'blind',
'friendly_name': 'Office Venetian Blind',
'is_closed': None,
'supported_features': <CoverEntityFeature: 123>,
}),
'context': <ANY>,
'entity_id': 'cover.office_venetian_blind',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.patio_shutter-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -919,6 +1027,59 @@
'state': 'open',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.basement_garage_door-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.basement_garage_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 15>,
'translation_key': None,
'unique_id': 'io://1234-1234-6233/1166864',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.basement_garage_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Basement Garage Door',
'is_closed': None,
'supported_features': <CoverEntityFeature: 15>,
}),
'context': <ANY>,
'entity_id': 'cover.basement_garage_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.bedroom_blinds-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1081,6 +1242,59 @@
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garage_door-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.garage_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': 'io://1234-1234-6233/16730050',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garage_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Garage Door',
'is_closed': True,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.garage_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garden_gate-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -1512,6 +1726,165 @@
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_garage_door-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.ogp_garage_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': 'ogp://1234-1234-6233/6632544',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_garage_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'OGP Garage Door',
'is_closed': True,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.ogp_garage_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_gate-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.ogp_gate',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GATE: 'gate'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': 'ogp://1234-1234-6233/10410217',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_gate-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'gate',
'friendly_name': 'OGP Gate',
'is_closed': False,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.ogp_gate',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.partial_garage_door-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.partial_garage_door',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': 'io://1234-1234-6233/7433515',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.partial_garage_door-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Partial Garage Door',
'is_closed': True,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.partial_garage_door',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.patio_shutter-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -3090,6 +3463,60 @@
'state': 'closed',
})
# ---
# name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.kitchen_pergola-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.kitchen_pergola',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.AWNING: 'awning'>,
'original_icon': None,
'original_name': None,
'platform': 'overkiz',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <CoverEntityFeature: 11>,
'translation_key': None,
'unique_id': 'rts://1234-5678-3293/16757826',
'unit_of_measurement': None,
})
# ---
# name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.kitchen_pergola-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'assumed_state': True,
'device_class': 'awning',
'friendly_name': 'Kitchen Pergola',
'is_closed': None,
'supported_features': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.kitchen_pergola',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.living_room_curtain-entry]
EntityRegistryEntrySnapshot({
'aliases': list([

View File

@@ -1810,7 +1810,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/15702199#2-core:RSSILevelState',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.garden_radiator_bathroom_temperature_sensor_rssi_level-state]
@@ -1819,7 +1819,7 @@
'device_class': 'signal_strength',
'friendly_name': 'Garden Radiator Bathroom Temperature Sensor RSSI level',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
}),
'context': <ANY>,
'entity_id': 'sensor.garden_radiator_bathroom_temperature_sensor_rssi_level',
@@ -2804,7 +2804,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/9253412#2-core:RSSILevelState',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.living_room_radiator_study_temp_probe_rssi_level-state]
@@ -2813,7 +2813,7 @@
'device_class': 'signal_strength',
'friendly_name': 'Living Room Radiator Study Temp Probe RSSI level',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_radiator_study_temp_probe_rssi_level',
@@ -4182,7 +4182,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'io://1234-5678-1698/9187218#2-core:RSSILevelState',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
})
# ---
# name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.study_radiator_nursery_temp_probe_rssi_level-state]
@@ -4191,7 +4191,7 @@
'device_class': 'signal_strength',
'friendly_name': 'Study Radiator Nursery Temp Probe RSSI level',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
'unit_of_measurement': 'dB',
}),
'context': <ANY>,
'entity_id': 'sensor.study_radiator_nursery_temp_probe_rssi_level',

View File

@@ -29,7 +29,12 @@ from homeassistant.components.cover import (
CoverEntityFeature,
CoverState,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -53,6 +58,11 @@ PERGOLA = FixtureDevice(
"io://1234-5678-3293/7614902",
"cover.garden_pergola",
)
UP_DOWN_BIOCLIMATIC_PERGOLA = FixtureDevice(
"setup/local_somfy_tahoma_v2_europe.json",
"rts://1234-5678-3293/16757826",
"cover.kitchen_pergola",
)
RTS = FixtureDevice(
"setup/cloud_somfy_connexoon_rts_asia.json",
"rts://1234-1234-6362/16730022",
@@ -90,6 +100,36 @@ POSITIONABLE_DUAL_ROLLER_SHUTTER = FixtureDevice(
"io://1234-5678-5010/12931361",
"cover.basement_roller_shutter",
)
TILT_ONLY_VENETIAN_BLIND = FixtureDevice(
"setup/cloud_somfy_connexoon_rts_asia.json",
"rts://1234-1234-6362/16730044",
"cover.jaloezie",
)
UP_DOWN_VENETIAN_BLIND = FixtureDevice(
"setup/cloud_somfy_connexoon_rts_asia.json",
"rts://1234-1234-6362/16747291",
"cover.office_venetian_blind",
)
DYNAMIC_GARAGE_DOOR = FixtureDevice(
"setup/cloud_somfy_tahoma_v2_europe.json",
"io://1234-1234-6233/16730050",
"cover.garage_door",
)
DYNAMIC_GARAGE_DOOR_OGP = FixtureDevice(
"setup/cloud_somfy_tahoma_v2_europe.json",
"ogp://1234-1234-6233/6632544",
"cover.ogp_garage_door",
)
PARTIAL_GARAGE_DOOR = FixtureDevice(
"setup/cloud_somfy_tahoma_v2_europe.json",
"io://1234-1234-6233/7433515",
"cover.partial_garage_door",
)
DYNAMIC_GATE = FixtureDevice(
"setup/cloud_somfy_tahoma_v2_europe.json",
"ogp://1234-1234-6233/10410217",
"cover.ogp_gate",
)
SNAPSHOT_FIXTURES = [
AWNING,
@@ -130,28 +170,148 @@ async def test_cover_entities_snapshot(
@pytest.mark.parametrize(
("device", "service", "command_name", "expected_state"),
("device", "service", "command_name", "parameters", "expected_state"),
[
(SHUTTER, SERVICE_OPEN_COVER, "open", CoverState.OPENING),
(AWNING, SERVICE_OPEN_COVER, "deploy", CoverState.OPENING),
(GARAGE, SERVICE_OPEN_COVER, "open", CoverState.OPENING),
(SHUTTER, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING),
(AWNING, SERVICE_CLOSE_COVER, "undeploy", CoverState.CLOSING),
(GARAGE, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING),
(SHUTTER, SERVICE_STOP_COVER, "stop", CoverState.CLOSED),
(AWNING, SERVICE_STOP_COVER, "stop", CoverState.CLOSED),
(GARAGE, SERVICE_STOP_COVER, "stop", CoverState.CLOSED),
(SHUTTER, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(AWNING, SERVICE_OPEN_COVER, "deploy", None, CoverState.OPENING),
(GARAGE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(DYNAMIC_GATE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING),
(
UP_DOWN_BIOCLIMATIC_PERGOLA,
SERVICE_OPEN_COVER,
"open",
[0],
CoverState.OPENING,
),
(TILT_ONLY_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING),
(UP_DOWN_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING),
(SHUTTER, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING),
(AWNING, SERVICE_CLOSE_COVER, "undeploy", None, CoverState.CLOSING),
(GARAGE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING),
(DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING),
(
DYNAMIC_GARAGE_DOOR_OGP,
SERVICE_CLOSE_COVER,
"close",
None,
CoverState.CLOSING,
),
(DYNAMIC_GATE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING),
(PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING),
(
UP_DOWN_BIOCLIMATIC_PERGOLA,
SERVICE_CLOSE_COVER,
"close",
[0],
CoverState.CLOSING,
),
(
TILT_ONLY_VENETIAN_BLIND,
SERVICE_CLOSE_COVER,
"close",
[0],
CoverState.CLOSING,
),
(UP_DOWN_VENETIAN_BLIND, SERVICE_CLOSE_COVER, "close", [0], CoverState.CLOSING),
(SHUTTER, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(AWNING, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(GARAGE, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(DYNAMIC_GATE, SERVICE_STOP_COVER, "stop", None, CoverState.OPEN),
(PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED),
(
UP_DOWN_BIOCLIMATIC_PERGOLA,
SERVICE_STOP_COVER,
"stop",
[0],
STATE_UNKNOWN,
),
(TILT_ONLY_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN),
(
TILT_ONLY_VENETIAN_BLIND,
SERVICE_OPEN_COVER_TILT,
"tiltPositive",
[1, 0],
CoverState.OPENING,
),
(
TILT_ONLY_VENETIAN_BLIND,
SERVICE_CLOSE_COVER_TILT,
"tiltNegative",
[1, 0],
CoverState.CLOSING,
),
(
TILT_ONLY_VENETIAN_BLIND,
SERVICE_STOP_COVER_TILT,
"stop",
[0],
STATE_UNKNOWN,
),
(UP_DOWN_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN),
(
UP_DOWN_VENETIAN_BLIND,
SERVICE_OPEN_COVER_TILT,
"tiltPositive",
[15, 1],
CoverState.OPENING,
),
(
UP_DOWN_VENETIAN_BLIND,
SERVICE_CLOSE_COVER_TILT,
"tiltNegative",
[15, 1],
CoverState.CLOSING,
),
(
UP_DOWN_VENETIAN_BLIND,
SERVICE_STOP_COVER_TILT,
"stop",
[0],
STATE_UNKNOWN,
),
],
ids=[
"open-roller-shutter",
"open-awning",
"open-garage-door",
"open-dynamic-garage-door",
"open-dynamic-garage-door-ogp",
"open-dynamic-gate",
"open-partial-garage-door",
"open-up-down-bioclimatic-pergola",
"open-tilt-only-venetian-blind",
"open-venetian-blind-rts",
"close-roller-shutter",
"close-awning",
"close-garage-door",
"close-dynamic-garage-door",
"close-dynamic-garage-door-ogp",
"close-dynamic-gate",
"close-partial-garage-door",
"close-up-down-bioclimatic-pergola",
"close-tilt-only-venetian-blind",
"close-venetian-blind-rts",
"stop-roller-shutter",
"stop-awning",
"stop-garage-door",
"stop-dynamic-garage-door",
"stop-dynamic-garage-door-ogp",
"stop-dynamic-gate",
"stop-partial-garage-door",
"stop-up-down-bioclimatic-pergola",
"stop-tilt-only-venetian-blind",
"open-tilt-tilt-only-venetian-blind",
"close-tilt-tilt-only-venetian-blind",
"stop-tilt-tilt-only-venetian-blind",
"stop-venetian-blind-rts",
"open-tilt-venetian-blind-rts",
"close-tilt-venetian-blind-rts",
"stop-tilt-venetian-blind-rts",
],
)
async def test_cover_service_actions(
@@ -161,9 +321,10 @@ async def test_cover_service_actions(
device: FixtureDevice,
service: str,
command_name: str,
expected_state: CoverState,
parameters: list[Any] | None,
expected_state: CoverState | str,
) -> None:
"""Test open, close, and stop cover services."""
"""Test open, close, and stop cover and tilt services."""
await setup_overkiz_integration(fixture=device.fixture)
await hass.services.async_call(
@@ -180,6 +341,7 @@ async def test_cover_service_actions(
mock_client,
device_url=device.device_url,
command_name=command_name,
parameters=parameters,
)

View File

@@ -36,6 +36,10 @@ from homeassistant.setup import async_setup_component
from . import MockTodoListEntity, create_mock_platform
from tests.common import async_mock_service, mock_device_registry
from tests.components.common import (
assert_trigger_gated_by_labs_flag,
assert_trigger_options_supported,
)
TODO_ENTITY_ID1 = "todo.list_one"
TODO_ENTITY_ID2 = "todo.list_two"
@@ -122,6 +126,47 @@ def service_calls(hass: HomeAssistant) -> list[ServiceCall]:
return async_mock_service(hass, "test", "item_added")
@pytest.mark.parametrize(
"trigger_key",
[
"todo.item_added",
"todo.item_completed",
"todo.item_removed",
],
)
async def test_todo_triggers_gated_by_labs_flag(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
) -> None:
"""Test the todo triggers are gated by the labs flag."""
await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key)
@pytest.mark.usefixtures("enable_labs_preview_features")
@pytest.mark.parametrize(
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
[
("todo.item_added", None, False, False),
("todo.item_completed", None, False, False),
("todo.item_removed", None, False, False),
],
)
async def test_todo_trigger_options_validation(
hass: HomeAssistant,
trigger_key: str,
base_options: dict[str, Any] | None,
supports_behavior: bool,
supports_duration: bool,
) -> None:
"""Test that todo triggers support the expected options."""
await assert_trigger_options_supported(
hass,
trigger_key,
base_options,
supports_behavior=supports_behavior,
supports_duration=supports_duration,
)
def _assert_service_calls(
service_calls: list[ServiceCall], expected_calls: list[dict[str, Any]]
) -> None:

View File

@@ -1,5 +1,7 @@
"""Tests for WiZ binary_sensor platform."""
import pytest
from homeassistant.components import wiz
from homeassistant.components.wiz.binary_sensor import OCCUPANCY_UNIQUE_ID
from homeassistant.config_entries import ConfigEntryState
@@ -21,20 +23,27 @@ from . import (
from tests.common import MockConfigEntry
@pytest.mark.parametrize("occupancy_source", ["pir", "wfsens"])
async def test_binary_sensor_created_from_push_updates(
hass: HomeAssistant, entity_registry: er.EntityRegistry
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
occupancy_source: str,
) -> None:
"""Test a binary sensor created from push updates."""
bulb, _ = await async_setup_integration(hass)
await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": True})
await async_push_update(
hass, bulb, {"mac": FAKE_MAC, "src": occupancy_source, "state": True}
)
entity_id = "binary_sensor.mock_title_occupancy"
assert entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_occupancy"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": False})
await async_push_update(
hass, bulb, {"mac": FAKE_MAC, "src": occupancy_source, "state": False}
)
state = hass.states.get(entity_id)
assert state.state == STATE_OFF

View File

@@ -47,6 +47,8 @@ INTEGRATION_DISCOVERY = {
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@@ -54,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["errors"] == {}
# Patch functions
with (
_patch_wizlight(),
_patch_wizlight(device=bulb),
patch(
"homeassistant.components.wiz.async_setup_entry",
return_value=True,
@@ -76,6 +78,7 @@ async def test_form(hass: HomeAssistant) -> None:
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
bulb.async_close.assert_awaited_once()
async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None:
@@ -137,10 +140,10 @@ async def test_user_form_exceptions(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.wiz.wizlight.getBulbConfig",
side_effect=side_effect,
):
bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB)
bulb.get_bulbtype = AsyncMock(side_effect=side_effect)
with _patch_wizlight(device=bulb):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
TEST_CONNECTION,
@@ -148,6 +151,7 @@ async def test_user_form_exceptions(
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": error_base}
bulb.async_close.assert_awaited_once()
async def test_form_updates_unique_id(hass: HomeAssistant) -> None:
@@ -185,10 +189,10 @@ async def test_discovered_by_dhcp_connection_fails(
hass: HomeAssistant, source, data
) -> None:
"""Test we abort on connection failure."""
with patch(
"homeassistant.components.wiz.wizlight.getBulbConfig",
side_effect=WizLightTimeOutError,
):
bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB)
bulb.get_bulbtype = AsyncMock(side_effect=WizLightTimeOutError)
with _patch_wizlight(device=bulb):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": source}, data=data
)
@@ -196,6 +200,7 @@ async def test_discovered_by_dhcp_connection_fails(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
bulb.async_close.assert_awaited_once()
@pytest.mark.parametrize(
@@ -263,9 +268,9 @@ async def test_discovered_by_dhcp_or_integration_discovery(
hass: HomeAssistant, source, data, bulb_type, extended_white_range, name
) -> None:
"""Test we can configure when discovered from dhcp or discovery."""
with _patch_wizlight(
device=None, extended_white_range=extended_white_range, bulb_type=bulb_type
):
bulb = _mocked_wizlight(None, extended_white_range, bulb_type)
with _patch_wizlight(device=bulb):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": source}, data=data
)
@@ -273,11 +278,12 @@ async def test_discovered_by_dhcp_or_integration_discovery(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
bulb.async_close.assert_awaited_once()
bulb.async_close.reset_mock()
with (
_patch_wizlight(
device=None, extended_white_range=extended_white_range, bulb_type=bulb_type
),
_patch_wizlight(device=bulb),
patch(
"homeassistant.components.wiz.async_setup_entry",
return_value=True,
@@ -299,6 +305,7 @@ async def test_discovered_by_dhcp_or_integration_discovery(
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
bulb.async_close.assert_awaited_once()
@pytest.mark.parametrize(
@@ -393,8 +400,10 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None:
assert result2["step_id"] == "pick_device"
assert not result2["errors"]
bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB)
with (
_patch_wizlight(),
_patch_wizlight(device=bulb),
patch(
"homeassistant.components.wiz.async_setup", return_value=True
) as mock_setup,
@@ -415,6 +424,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None:
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
bulb.async_close.assert_awaited_once()
# ignore configured devices
result = await hass.config_entries.flow.async_init(

View File

@@ -1513,3 +1513,15 @@ def fibaro_fgms001_v2_8_fixture(
node = Node(client, fibaro_fgms001_v2_8_state)
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="fibaro_fgms001_unknown_firmware")
def fibaro_fgms001_unknown_firmware_fixture(
client: MagicMock, fibaro_fgms001_v2_8_state: NodeDataType
) -> Node:
"""Load FGMS001 node with no reported firmware version."""
state = copy.deepcopy(fibaro_fgms001_v2_8_state)
state.pop("firmwareVersion", None)
node = Node(client, state)
client.driver.controller.nodes[node.node_id] = node
return node

View File

@@ -33,7 +33,7 @@ from homeassistant.components.zwave_js.discovery_data_template import (
DynamicCurrentTempClimateDataTemplate,
)
from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntryState
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@@ -666,6 +666,40 @@ async def test_nabu_casa_zwa2_legacy(
)
@pytest.mark.parametrize("platforms", [[Platform.BINARY_SENSOR, Platform.LIGHT]])
async def test_fibaro_fgms001_unknown_firmware_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
client: MagicMock,
fibaro_fgms001_unknown_firmware: Node,
integration: MockConfigEntry,
) -> None:
"""Test setup completes when an FGMS001 node has no firmware version.
Regression test for a crash where comparing AwesomeVersion(None) to a
schema's firmware_version_range raised AwesomeVersionCompareException
and aborted setup of the whole config entry.
"""
assert integration.state is ConfigEntryState.LOADED
device = device_registry.async_get_device(
identifiers={get_device_id(client.driver, fibaro_fgms001_unknown_firmware)}
)
assert device is not None
entries = er.async_entries_for_device(
entity_registry, device.id, include_disabled_entities=True
)
motion_entries = [
entry
for entry in entries
if entry.domain == BINARY_SENSOR_DOMAIN
and entry.original_device_class == BinarySensorDeviceClass.MOTION
]
assert motion_entries == []
@pytest.mark.parametrize("platforms", [[Platform.BINARY_SENSOR, Platform.LIGHT]])
async def test_fibaro_fgms001_v2_8_motion_discovery(
hass: HomeAssistant,