This commit is contained in:
Franck Nijhof 2023-11-04 14:27:14 +01:00 committed by GitHub
commit ce12d82624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 4981 additions and 532 deletions

View File

@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.1"]
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.2"]
}

View File

@ -5,5 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecoforest",
"iot_class": "local_polling",
"loggers": ["pyecoforest"],
"requirements": ["pyecoforest==0.3.0"]
}

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.13.1"],
"requirements": ["pyenphase==1.14.1"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import (
@ -204,7 +204,7 @@ class FroniusSolarNet:
# Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
if self.config_entry.state == ConfigEntryState.LOADED:
dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
_LOGGER.debug(
"New inverter added (UID: %s)",

View File

@ -661,7 +661,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
if new_value is None:
return self.entity_description.default_value
if self.entity_description.invalid_when_falsy and not new_value:
raise ValueError(f"Ignoring zero value for {self.entity_id}.")
return None
if isinstance(new_value, float):
return round(new_value, 4)
return new_value
@ -671,10 +671,9 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
"""Handle updated data from the coordinator."""
try:
self._attr_native_value = self._get_entity_value()
except (KeyError, ValueError):
except KeyError:
# sets state to `None` if no default_value is defined in entity description
# KeyError: raised when omitted in response - eg. at night when no production
# ValueError: raised when invalid zero value received
self._attr_native_value = self.entity_description.default_value
self.async_write_ha_state()

View File

@ -88,7 +88,6 @@ DESCRIPTIONS = (
GardenaBluetoothSensorEntityDescription(
key=Sensor.measurement_timestamp.uuid,
translation_key="sensor_measurement_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.measurement_timestamp,

View File

@ -353,6 +353,11 @@ class HoneywellUSThermostat(ClimateEntity):
if mode == "heat":
await self._device.set_setpoint_heat(temperature)
except UnexpectedResponse as err:
raise HomeAssistantError(
"Honeywell set temperature failed: Invalid Response"
) from err
except SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
raise ValueError(
@ -369,6 +374,11 @@ class HoneywellUSThermostat(ClimateEntity):
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
await self._device.set_setpoint_heat(temperature)
except UnexpectedResponse as err:
raise HomeAssistantError(
"Honeywell set temperature failed: Invalid Response"
) from err
except SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
raise ValueError(

View File

@ -78,7 +78,7 @@ class IslamicPrayerTimeSensor(
"""Initialize the Islamic prayer time sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = description.key
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=NAME,

View File

@ -1,12 +1,15 @@
"""Matter lock."""
from __future__ import annotations
from enum import IntFlag
from typing import Any
from chip.clusters import Objects as clusters
from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.components.lock import (
LockEntity,
LockEntityDescription,
LockEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CODE, Platform
from homeassistant.core import HomeAssistant, callback
@ -17,6 +20,8 @@ from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
DoorLockFeature = clusters.DoorLock.Bitmaps.Feature
async def async_setup_entry(
hass: HomeAssistant,
@ -61,6 +66,14 @@ class MatterLock(MatterEntity, LockEntity):
return bool(self.features & DoorLockFeature.kDoorPositionSensor)
@property
def supports_unbolt(self) -> bool:
"""Return True if the lock supports unbolt."""
if self.features is None:
return False
return bool(self.features & DoorLockFeature.kUnbolt)
async def send_device_command(
self,
command: clusters.ClusterCommand,
@ -92,6 +105,25 @@ class MatterLock(MatterEntity, LockEntity):
self._lock_option_default_code,
)
code_bytes = code.encode() if code else None
if self.supports_unbolt:
# if the lock reports it has separate unbolt support,
# the unlock command should unbolt only on the unlock command
# and unlatch on the HA 'open' command.
await self.send_device_command(
command=clusters.DoorLock.Commands.UnboltDoor(code_bytes)
)
else:
await self.send_device_command(
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
)
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
code: str = kwargs.get(
ATTR_CODE,
self._lock_option_default_code,
)
code_bytes = code.encode() if code else None
await self.send_device_command(
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
)
@ -104,6 +136,8 @@ class MatterLock(MatterEntity, LockEntity):
self.features = int(
self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
)
if self.supports_unbolt:
self._attr_supported_features = LockEntityFeature.OPEN
lock_state = self.get_matter_attribute_value(
clusters.DoorLock.Attributes.LockState
@ -144,26 +178,6 @@ class MatterLock(MatterEntity, LockEntity):
)
class DoorLockFeature(IntFlag):
"""Temp enum that represents the features of a door lock.
Should be replaced by the library provided one once that is released.
"""
kPinCredential = 0x1 # noqa: N815
kRfidCredential = 0x2 # noqa: N815
kFingerCredentials = 0x4 # noqa: N815
kLogging = 0x8 # noqa: N815
kWeekDayAccessSchedules = 0x10 # noqa: N815
kDoorPositionSensor = 0x20 # noqa: N815
kFaceCredentials = 0x40 # noqa: N815
kCredentialsOverTheAirAccess = 0x80 # noqa: N815
kUser = 0x100 # noqa: N815
kNotification = 0x200 # noqa: N815
kYearDayAccessSchedules = 0x400 # noqa: N815
kHolidaySchedules = 0x800 # noqa: N815
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LOCK,

View File

@ -31,13 +31,21 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import entity_registry as er, sun
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import MetDataUpdateCoordinator
from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP
from .const import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
ATTR_MAP,
CONDITIONS_MAP,
CONF_TRACK_HOME,
DOMAIN,
FORECAST_MAP,
)
DEFAULT_NAME = "Met.no"
@ -141,6 +149,10 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
condition = self.coordinator.data.current_weather_data.get("condition")
if condition is None:
return None
if condition == ATTR_CONDITION_SUNNY and not sun.is_up(self.hass):
condition = ATTR_CONDITION_CLEAR_NIGHT
return format_condition(condition)
@property

View File

@ -42,6 +42,7 @@ from .mixins import (
MqttAvailability,
MqttEntity,
async_setup_entity_entry_helper,
validate_sensor_entity_category,
write_state_on_attr_change,
)
from .models import MqttValueTemplate, ReceiveMessage
@ -55,7 +56,7 @@ DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_FORCE_UPDATE = False
CONF_EXPIRE_AFTER = "expire_after"
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
@ -67,7 +68,12 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
DISCOVERY_SCHEMA = vol.All(
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
)
PLATFORM_SCHEMA_MODERN = vol.All(validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE)
async def async_setup_entry(

View File

@ -232,16 +232,16 @@ TOPIC_KEYS = (
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the preset mode reset payload is not one of the preset modes."""
if PRESET_NONE in config[CONF_PRESET_MODES_LIST]:
raise ValueError("preset_modes must not include preset mode 'none'")
raise vol.Invalid("preset_modes must not include preset mode 'none'")
return config
def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
"""Validate a target_humidity range configuration, throws otherwise."""
if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]:
raise ValueError("target_humidity_max must be > target_humidity_min")
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
if config[CONF_HUMIDITY_MAX] > 100:
raise ValueError("max_humidity must be <= 100")
raise vol.Invalid("max_humidity must be <= 100")
return config

View File

@ -116,16 +116,16 @@ _LOGGER = logging.getLogger(__name__)
def valid_speed_range_configuration(config: ConfigType) -> ConfigType:
"""Validate that the fan speed_range configuration is valid, throws if it isn't."""
if config[CONF_SPEED_RANGE_MIN] == 0:
raise ValueError("speed_range_min must be > 0")
raise vol.Invalid("speed_range_min must be > 0")
if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]:
raise ValueError("speed_range_max must be > speed_range_min")
raise vol.Invalid("speed_range_max must be > speed_range_min")
return config
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the preset mode reset payload is not one of the preset modes."""
if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]:
raise ValueError("preset_modes must not contain payload_reset_preset_mode")
raise vol.Invalid("preset_modes must not contain payload_reset_preset_mode")
return config

View File

@ -102,7 +102,7 @@ _LOGGER = logging.getLogger(__name__)
def valid_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the mode reset payload is not one of the available modes."""
if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]:
raise ValueError("modes must not contain payload_reset_mode")
raise vol.Invalid("modes must not contain payload_reset_mode")
return config
@ -113,9 +113,9 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
throws if it isn't.
"""
if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]:
raise ValueError("target_humidity_max must be > target_humidity_min")
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
if config[CONF_TARGET_HUMIDITY_MAX] > 100:
raise ValueError("max_humidity must be <= 100")
raise vol.Invalid("max_humidity must be <= 100")
return config

View File

@ -9,7 +9,6 @@ import logging
from typing import TYPE_CHECKING, Any, Protocol, cast, final
import voluptuous as vol
import yaml
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -28,6 +27,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
EntityCategory,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import (
@ -63,6 +63,7 @@ from homeassistant.helpers.typing import (
UndefinedType,
)
from homeassistant.util.json import json_loads
from homeassistant.util.yaml import dump as yaml_dump
from . import debug_info, subscription
from .client import async_publish
@ -207,6 +208,16 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType
)
def validate_sensor_entity_category(config: ConfigType) -> ConfigType:
"""Check the sensor's entity category is not set to `config` which is invalid for sensors."""
if (
CONF_ENTITY_CATEGORY in config
and config[CONF_ENTITY_CATEGORY] == EntityCategory.CONFIG
):
raise vol.Invalid("Entity category `config` is invalid")
return config
MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(
cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE),
vol.Schema(
@ -404,8 +415,8 @@ async def async_setup_entity_entry_helper(
error = str(ex)
config_file = getattr(yaml_config, "__config_file__", "?")
line = getattr(yaml_config, "__line__", "?")
issue_id = hex(hash(frozenset(yaml_config.items())))
yaml_config_str = yaml.dump(dict(yaml_config))
issue_id = hex(hash(frozenset(yaml_config)))
yaml_config_str = yaml_dump(yaml_config)
learn_more_url = (
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
)
@ -427,7 +438,7 @@ async def async_setup_entity_entry_helper(
translation_key="invalid_platform_config",
)
_LOGGER.error(
"%s for manual configured MQTT %s item, in %s, line %s Got %s",
"%s for manually configured MQTT %s item, in %s, line %s Got %s",
error,
domain,
config_file,

View File

@ -44,6 +44,7 @@ from .mixins import (
MqttAvailability,
MqttEntity,
async_setup_entity_entry_helper,
validate_sensor_entity_category,
write_state_on_attr_change,
)
from .models import (
@ -70,7 +71,6 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_FORCE_UPDATE = False
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
@ -88,6 +88,7 @@ PLATFORM_SCHEMA_MODERN = vol.All(
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
# Removed in HA Core 2023.6.0
cv.removed(CONF_LAST_RESET_TOPIC),
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE,
)
@ -95,6 +96,7 @@ DISCOVERY_SCHEMA = vol.All(
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
# Removed in HA Core 2023.6.0
cv.removed(CONF_LAST_RESET_TOPIC),
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
)

View File

@ -71,9 +71,9 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset(
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
"""Validate that the text length configuration is valid, throws if it isn't."""
if config[CONF_MIN] >= config[CONF_MAX]:
raise ValueError("text length min must be >= max")
raise vol.Invalid("text length min must be >= max")
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
return config

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling",
"loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==1.0.0"]
"requirements": ["py-nextbusnext==1.0.2"]
}

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.0.38"]
"requirements": ["opower==0.0.39"]
}

View File

@ -40,7 +40,7 @@ SELECT_TYPES = (
key="select_schedule",
translation_key="select_schedule",
icon="mdi:calendar-clock",
command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt),
options_key="available_schedules",
),
PlugwiseSelectEntityDescription(

View File

@ -10,6 +10,7 @@ from typing import Literal
from reolink_aio.api import RETRY_ATTEMPTS
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
from reolink_aio.software_version import NewSoftwareVersion
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
@ -45,7 +46,9 @@ class ReolinkData:
host: ReolinkHost
device_coordinator: DataUpdateCoordinator[None]
firmware_coordinator: DataUpdateCoordinator[str | Literal[False]]
firmware_coordinator: DataUpdateCoordinator[
str | Literal[False] | NewSoftwareVersion
]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
@ -86,7 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
await host.renew()
async def async_check_firmware_update() -> str | Literal[False]:
async def async_check_firmware_update() -> str | Literal[
False
] | NewSoftwareVersion:
"""Check for firmware updates."""
if not host.api.supported(None, "update"):
return False
@ -153,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update the configuration of the host entity."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -32,7 +32,7 @@ from .entity import ReolinkChannelCoordinatorEntity
class ReolinkBinarySensorEntityDescriptionMixin:
"""Mixin values for Reolink binary sensor entities."""
value: Callable[[Host, int | None], bool]
value: Callable[[Host, int], bool]
@dataclass
@ -43,7 +43,7 @@ class ReolinkBinarySensorEntityDescription(
icon: str = "mdi:motion-sensor"
icon_off: str = "mdi:motion-sensor-off"
supported: Callable[[Host, int | None], bool] = lambda host, ch: True
supported: Callable[[Host, int], bool] = lambda host, ch: True
BINARY_SENSORS = (
@ -169,6 +169,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
)
)
async def _async_handle_event(self, event):
async def _async_handle_event(self, event: str) -> None:
"""Handle incoming event for motion detection."""
self.async_write_ha_state()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from typing import Any, Literal
import aiohttp
from aiohttp.web import Request
@ -81,7 +81,7 @@ class ReolinkHost:
return self._unique_id
@property
def api(self):
def api(self) -> Host:
"""Return the API object."""
return self._api
@ -313,7 +313,7 @@ class ReolinkHost:
"""Call the API of the camera device to update the internal states."""
await self._api.get_states()
async def disconnect(self):
async def disconnect(self) -> None:
"""Disconnect from the API, so the connection will be released."""
try:
await self._api.unsubscribe()
@ -335,7 +335,7 @@ class ReolinkHost:
err,
)
async def _async_start_long_polling(self, initial=False):
async def _async_start_long_polling(self, initial=False) -> None:
"""Start ONVIF long polling task."""
if self._long_poll_task is None:
try:
@ -364,7 +364,7 @@ class ReolinkHost:
self._lost_subscription = False
self._long_poll_task = asyncio.create_task(self._async_long_polling())
async def _async_stop_long_polling(self):
async def _async_stop_long_polling(self) -> None:
"""Stop ONVIF long polling task."""
if self._long_poll_task is not None:
self._long_poll_task.cancel()
@ -372,7 +372,7 @@ class ReolinkHost:
await self._api.unsubscribe(sub_type=SubType.long_poll)
async def stop(self, event=None):
async def stop(self, event=None) -> None:
"""Disconnect the API."""
if self._cancel_poll is not None:
self._cancel_poll()
@ -433,7 +433,7 @@ class ReolinkHost:
else:
self._lost_subscription = False
async def _renew(self, sub_type: SubType) -> None:
async def _renew(self, sub_type: Literal[SubType.push, SubType.long_poll]) -> None:
"""Execute the renew of the subscription."""
if not self._api.subscribed(sub_type):
_LOGGER.debug(
@ -512,8 +512,10 @@ class ReolinkHost:
_LOGGER.debug("Registered webhook: %s", event_id)
def unregister_webhook(self):
def unregister_webhook(self) -> None:
"""Unregister the webhook for motion events."""
if self.webhook_id is None:
return
_LOGGER.debug("Unregistering webhook %s", self.webhook_id)
webhook.async_unregister(self._hass, self.webhook_id)
self.webhook_id = None

View File

@ -38,8 +38,8 @@ class ReolinkLightEntityDescription(
"""A class that describes light entities."""
supported_fn: Callable[[Host, int], bool] = lambda api, ch: True
get_brightness_fn: Callable[[Host, int], int] | None = None
set_brightness_fn: Callable[[Host, int, float], Any] | None = None
get_brightness_fn: Callable[[Host, int], int | None] | None = None
set_brightness_fn: Callable[[Host, int, int], Any] | None = None
LIGHT_ENTITIES = (
@ -127,13 +127,13 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
if self.entity_description.get_brightness_fn is None:
return None
return round(
255
* (
self.entity_description.get_brightness_fn(self._host.api, self._channel)
/ 100.0
)
bright_pct = self.entity_description.get_brightness_fn(
self._host.api, self._channel
)
if bright_pct is None:
return None
return round(255 * bright_pct / 100.0)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn light off."""

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.7.12"]
"requirements": ["reolink-aio==0.7.14"]
}

View File

@ -26,7 +26,7 @@ from .entity import ReolinkChannelCoordinatorEntity
class ReolinkNumberEntityDescriptionMixin:
"""Mixin values for Reolink number entities."""
value: Callable[[Host, int], float]
value: Callable[[Host, int], float | None]
method: Callable[[Host, int, float], Any]
@ -354,7 +354,7 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
)
@property
def native_value(self) -> float:
def native_value(self) -> float | None:
"""State of the number entity."""
return self.entity_description.value(self._host.api, self._channel)

View File

@ -44,7 +44,7 @@ class ReolinkSensorEntityDescription(
class ReolinkHostSensorEntityDescriptionMixin:
"""Mixin values for Reolink host sensor entities."""
value: Callable[[Host], int]
value: Callable[[Host], int | None]
@dataclass

View File

@ -35,7 +35,8 @@ async def async_setup_entry(
class ReolinkUpdateEntity(
ReolinkBaseCoordinatorEntity[str | Literal[False]], UpdateEntity
ReolinkBaseCoordinatorEntity[str | Literal[False] | NewSoftwareVersion],
UpdateEntity,
):
"""Update entity for a Netgear device."""

View File

@ -119,7 +119,7 @@ class IRobotEntity(Entity):
@property
def battery_stats(self):
"""Return the battery stats."""
return self.vacuum_state.get("bbchg3")
return self.vacuum_state.get("bbchg3", {})
@property
def _robot_state(self):

View File

@ -106,7 +106,7 @@ SENSORS: list[RoombaSensorEntityDescription] = [
),
RoombaSensorEntityDescription(
key="scrubs_count",
translation_key="scrubs",
translation_key="scrubs_count",
icon="mdi:counter",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="Scrubs",

View File

@ -5,7 +5,7 @@
"data": {
"token": "[%key:common::config_flow::data::api_token%]"
},
"description": "Please entry your API token from your [Todoist Settings page]({settings_url})"
"description": "Please enter your API token from your [Todoist Settings page]({settings_url})"
}
},
"error": {

View File

@ -50,7 +50,8 @@ WEBHOOK_SCHEMA = vol.Schema(
vol.Optional(ATTR_BEARING): vol.Coerce(float),
vol.Optional(ATTR_SPEED): vol.Coerce(float),
vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int),
}
},
extra=vol.REMOVE_EXTRA,
)

View File

@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==64"],
"requirements": ["aiounifi==65"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waqi",
"iot_class": "cloud_polling",
"loggers": ["aiowaqi"],
"requirements": ["aiowaqi==2.1.0"]
"requirements": ["aiowaqi==3.0.0"]
}

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.28"]
"requirements": ["holidays==0.35"]
}

View File

@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==2.3.1"]
"requirements": ["yalexs-ble==2.3.2"]
}

View File

@ -10,7 +10,20 @@ The Z-Wave integration uses a discovery mechanism to create the necessary entiti
In cases where an entity's functionality requires interaction with multiple Values, the discovery rule for that particular entity type is based on the primary Value, or the Value that must be there to indicate that this entity needs to be created, and then the rest of the Values required are discovered by the class instance for that entity. A good example of this is the discovery logic for the `climate` entity. Currently, the discovery logic is tied to the discovery of a Value with a property of `mode` and a command class of `Thermostat Mode`, but the actual entity uses many more Values than that to be fully functional as evident in the [code](./climate.py).
There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data (from device diagnostics) to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py).
There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py). To learn how to generate fixtures, see the following section.
### Generating device fixtures
To generate a device fixture, download a diagnostics dump of the device from your Home Assistant instance. The dumped data will need to be modified to match the expected format. You can always do this transformation by hand, but the integration provides a [helper script](scripts/convert_device_diagnostics_to_fixture.py) that will generate the appropriate fixture data from a device diagnostics dump for you. To use it, run the script with the path to the diagnostics dump you downloaded:
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump>`
The script will print the fixture data to standard output, and you can use Unix piping to create a file from the fixture data:
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump> > <path_to_fixture_output>`
You can alternatively pass the `--file` flag to the script and it will create the file for you in the [fixtures folder](../../../tests/components/zwave_js/fixtures):
`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py <path/to/diagnostics/dump> --file`
### Switching HA support for a device from one entity type to another.

View File

@ -18,7 +18,6 @@ from zwave_js_server.const.command_class.multilevel_switch import (
from zwave_js_server.const.command_class.window_covering import (
NO_POSITION_PROPERTY_KEYS,
NO_POSITION_SUFFIX,
WINDOW_COVERING_OPEN_PROPERTY,
SlatStates,
)
from zwave_js_server.model.driver import Driver
@ -370,7 +369,7 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin):
set_values_func(
value,
stop_value=self.get_zwave_value(
WINDOW_COVERING_OPEN_PROPERTY,
"levelChangeUp",
value_property_key=value.property_key,
),
)

View File

@ -0,0 +1 @@
"""Scripts module for Z-Wave JS."""

View File

@ -0,0 +1,93 @@
"""Script to convert a device diagnostics file to a fixture."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from homeassistant.util import slugify
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
parser = argparse.ArgumentParser(description="Z-Wave JS Fixture generator")
parser.add_argument(
"diagnostics_file", type=Path, help="Device diagnostics file to convert"
)
parser.add_argument(
"--file",
action="store_true",
help=(
"Dump fixture to file in fixtures folder. By default, the fixture will be "
"printed to standard output."
),
)
arguments = parser.parse_args()
return arguments
def get_fixtures_dir_path(data: dict) -> Path:
"""Get path to fixtures directory."""
device_config = data["deviceConfig"]
filename = slugify(
f"{device_config['manufacturer']}-{device_config['label']}_state"
)
path = Path(__file__).parents[1]
index = path.parts.index("homeassistant")
return Path(
*path.parts[:index],
"tests",
*path.parts[index + 1 :],
"fixtures",
f"{filename}.json",
)
def load_file(path: Path) -> Any:
"""Load file from path."""
return json.loads(path.read_text("utf8"))
def extract_fixture_data(diagnostics_data: Any) -> dict:
"""Extract fixture data from file."""
if (
not isinstance(diagnostics_data, dict)
or "data" not in diagnostics_data
or "state" not in diagnostics_data["data"]
):
raise ValueError("Invalid diagnostics file format")
state: dict = diagnostics_data["data"]["state"]
if not isinstance(state["values"], list):
values_dict: dict[str, dict] = state.pop("values")
state["values"] = list(values_dict.values())
if not isinstance(state["endpoints"], list):
endpoints_dict: dict[str, dict] = state.pop("endpoints")
state["endpoints"] = list(endpoints_dict.values())
return state
def create_fixture_file(path: Path, state_text: str) -> None:
"""Create a file for the state dump in the fixtures directory."""
path.write_text(state_text, "utf8")
def main() -> None:
"""Run the main script."""
args = get_arguments()
diagnostics_path: Path = args.diagnostics_file
diagnostics = load_file(diagnostics_path)
fixture_data = extract_fixture_data(diagnostics)
fixture_text = json.dumps(fixture_data, indent=2)
if args.file:
fixture_path = get_fixtures_dir_path(fixture_data)
create_fixture_file(fixture_path, fixture_text)
return
print(fixture_text) # noqa: T201
if __name__ == "__main__":
main()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections import Counter
from collections.abc import Callable
from dataclasses import asdict, dataclass
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Final
@ -54,7 +54,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the extra data."""
return {
ATTR_LATEST_VERSION_FIRMWARE: asdict(self.latest_version_firmware)
ATTR_LATEST_VERSION_FIRMWARE: self.latest_version_firmware.to_dict()
if self.latest_version_firmware
else None
}
@ -339,19 +339,25 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
and (latest_version := state.attributes.get(ATTR_LATEST_VERSION))
is not None
and (extra_data := await self.async_get_last_extra_data())
):
self._attr_latest_version = latest_version
self._latest_version_firmware = (
ZWaveNodeFirmwareUpdateExtraStoredData.from_dict(
and (
latest_version_firmware := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict(
extra_data.as_dict()
).latest_version_firmware
)
# If we have no state or latest version to restore, we can set the latest
):
self._attr_latest_version = latest_version
self._latest_version_firmware = latest_version_firmware
# If we have no state or latest version to restore, or the latest version is
# the same as the installed version, we can set the latest
# version to installed so that the entity starts as off. If we have partial
# restore data due to an upgrade to an HA version where this feature is released
# from one that is not the entity will start in an unknown state until we can
# correct on next update
elif not state or not latest_version:
elif (
not state
or not latest_version
or latest_version == self._attr_installed_version
):
self._attr_latest_version = self._attr_installed_version
# Spread updates out in 5 minute increments to avoid flooding the network

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 11
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, 11, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.11.0"
version = "2023.11.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -369,7 +369,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==64
aiounifi==65
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@ -378,7 +378,7 @@ aiovlc==0.1.0
aiovodafone==0.4.2
# homeassistant.components.waqi
aiowaqi==2.1.0
aiowaqi==3.0.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -1004,7 +1004,7 @@ hlk-sw16==0.0.9
hole==0.8.0
# homeassistant.components.workday
holidays==0.28
holidays==0.35
# homeassistant.components.frontend
home-assistant-frontend==20231030.1
@ -1394,7 +1394,7 @@ openwrt-luci-rpc==1.1.16
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.0.38
opower==0.0.39
# homeassistant.components.oralb
oralb-ble==0.17.6
@ -1521,7 +1521,7 @@ py-improv-ble-client==1.0.3
py-melissa-climate==2.1.4
# homeassistant.components.nextbus
py-nextbusnext==1.0.0
py-nextbusnext==1.0.2
# homeassistant.components.nightscout
py-nightscout==1.2.2
@ -1693,7 +1693,7 @@ pyedimax==0.2.1
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==1.13.1
pyenphase==1.14.1
# homeassistant.components.envisalink
pyenvisalink==4.6
@ -2319,7 +2319,7 @@ renault-api==0.2.0
renson-endura-delta==1.6.0
# homeassistant.components.reolink
reolink-aio==0.7.12
reolink-aio==0.7.14
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@ -2758,7 +2758,7 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august
# homeassistant.components.yalexs_ble
yalexs-ble==2.3.1
yalexs-ble==2.3.2
# homeassistant.components.august
yalexs==1.10.0

View File

@ -344,7 +344,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.6
# homeassistant.components.unifi
aiounifi==64
aiounifi==65
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@ -353,7 +353,7 @@ aiovlc==0.1.0
aiovodafone==0.4.2
# homeassistant.components.waqi
aiowaqi==2.1.0
aiowaqi==3.0.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -793,7 +793,7 @@ hlk-sw16==0.0.9
hole==0.8.0
# homeassistant.components.workday
holidays==0.28
holidays==0.35
# homeassistant.components.frontend
home-assistant-frontend==20231030.1
@ -1072,7 +1072,7 @@ openerz-api==0.2.0
openhomedevice==2.2.0
# homeassistant.components.opower
opower==0.0.38
opower==0.0.39
# homeassistant.components.oralb
oralb-ble==0.17.6
@ -1166,7 +1166,7 @@ py-improv-ble-client==1.0.3
py-melissa-climate==2.1.4
# homeassistant.components.nextbus
py-nextbusnext==1.0.0
py-nextbusnext==1.0.2
# homeassistant.components.nightscout
py-nightscout==1.2.2
@ -1275,7 +1275,7 @@ pyeconet==0.1.22
pyefergy==22.1.1
# homeassistant.components.enphase_envoy
pyenphase==1.13.1
pyenphase==1.14.1
# homeassistant.components.everlights
pyeverlights==0.1.0
@ -1730,7 +1730,7 @@ renault-api==0.2.0
renson-endura-delta==1.6.0
# homeassistant.components.reolink
reolink-aio==0.7.12
reolink-aio==0.7.14
# homeassistant.components.rflink
rflink==0.0.65
@ -2058,7 +2058,7 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august
# homeassistant.components.yalexs_ble
yalexs-ble==2.3.1
yalexs-ble==2.3.2
# homeassistant.components.august
yalexs==1.10.0

View File

@ -358,7 +358,24 @@ async def test_service_calls_off_mode(
device.set_setpoint_heat.assert_called_with(77)
assert "Invalid temperature" in caplog.text
device.set_setpoint_heat.reset_mock()
device.set_setpoint_heat.side_effect = aiosomecomfort.UnexpectedResponse
caplog.clear()
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: entity_id,
ATTR_TARGET_TEMP_LOW: 25.0,
ATTR_TARGET_TEMP_HIGH: 35.0,
},
blocking=True,
)
device.set_setpoint_cool.assert_called_with(95)
device.set_setpoint_heat.assert_called_with(77)
reset_mock(device)
await hass.services.async_call(
CLIMATE_DOMAIN,
@ -702,6 +719,17 @@ async def test_service_calls_heat_mode(
device.set_hold_heat.reset_mock()
assert "Invalid temperature" in caplog.text
device.set_hold_heat.side_effect = aiosomecomfort.UnexpectedResponse
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15},
blocking=True,
)
device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59)
device.set_hold_heat.reset_mock()
caplog.clear()
await hass.services.async_call(
CLIMATE_DOMAIN,

View File

@ -223,6 +223,16 @@ async def door_lock_fixture(
return await setup_integration_with_node_fixture(hass, "door-lock", matter_client)
@pytest.fixture(name="door_lock_with_unbolt")
async def door_lock_with_unbolt_fixture(
hass: HomeAssistant, matter_client: MagicMock
) -> MatterNode:
"""Fixture for a door lock node with unbolt feature."""
return await setup_integration_with_node_fixture(
hass, "door-lock-with-unbolt", matter_client
)
@pytest.fixture(name="eve_contact_sensor_node")
async def eve_contact_sensor_node_fixture(
hass: HomeAssistant, matter_client: MagicMock

View File

@ -0,0 +1,510 @@
{
"node_id": 1,
"date_commissioned": "2023-03-07T09:06:06.059454",
"last_interview": "2023-03-07T09:06:06.059456",
"interview_version": 2,
"available": true,
"attributes": {
"0/29/0": [
{
"deviceType": 22,
"revision": 1
}
],
"0/29/1": [
29, 31, 40, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 60, 62,
63, 64, 65
],
"0/29/2": [41],
"0/29/3": [1],
"0/29/65532": 0,
"0/29/65533": 1,
"0/29/65528": [],
"0/29/65529": [],
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533],
"0/31/0": [
{
"privilege": 5,
"authMode": 2,
"subjects": [112233],
"targets": null,
"fabricIndex": 1
}
],
"0/31/1": [],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65532": 0,
"0/31/65533": 1,
"0/31/65528": [],
"0/31/65529": [],
"0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533],
"0/40/0": 1,
"0/40/1": "TEST_VENDOR",
"0/40/2": 65521,
"0/40/3": "Mock Door Lock",
"0/40/4": 32769,
"0/40/5": "Mock Door Lock",
"0/40/6": "**REDACTED**",
"0/40/7": 0,
"0/40/8": "TEST_VERSION",
"0/40/9": 1,
"0/40/10": "1.0",
"0/40/11": "20200101",
"0/40/12": "",
"0/40/13": "",
"0/40/14": "",
"0/40/15": "TEST_SN",
"0/40/16": false,
"0/40/17": true,
"0/40/18": "mock-door-lock",
"0/40/19": {
"caseSessionsPerFabric": 3,
"subscriptionsPerFabric": 65535
},
"0/40/65532": 0,
"0/40/65533": 1,
"0/40/65528": [],
"0/40/65529": [],
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
65528, 65529, 65530, 65531, 65532, 65533
],
"0/42/0": [],
"0/42/1": true,
"0/42/2": 0,
"0/42/3": 0,
"0/42/65532": 0,
"0/42/65533": 1,
"0/42/65528": [],
"0/42/65529": [0],
"0/42/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533],
"0/43/0": "en-US",
"0/43/1": [
"en-US",
"de-DE",
"fr-FR",
"en-GB",
"es-ES",
"zh-CN",
"it-IT",
"ja-JP"
],
"0/43/65532": 0,
"0/43/65533": 1,
"0/43/65528": [],
"0/43/65529": [],
"0/43/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533],
"0/44/0": 0,
"0/44/1": 0,
"0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7],
"0/44/65532": 0,
"0/44/65533": 1,
"0/44/65528": [],
"0/44/65529": [],
"0/44/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533],
"0/46/0": [0, 1],
"0/46/65532": 0,
"0/46/65533": 1,
"0/46/65528": [],
"0/46/65529": [],
"0/46/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533],
"0/47/0": 1,
"0/47/1": 0,
"0/47/2": "USB",
"0/47/6": 0,
"0/47/65532": 1,
"0/47/65533": 1,
"0/47/65528": [],
"0/47/65529": [],
"0/47/65531": [0, 1, 2, 6, 65528, 65529, 65530, 65531, 65532, 65533],
"0/48/0": 0,
"0/48/1": {
"failSafeExpiryLengthSeconds": 60,
"maxCumulativeFailsafeSeconds": 900
},
"0/48/2": 0,
"0/48/3": 2,
"0/48/4": true,
"0/48/65532": 0,
"0/48/65533": 1,
"0/48/65528": [1, 3, 5],
"0/48/65529": [0, 2, 4],
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533],
"0/49/0": 1,
"0/49/1": [],
"0/49/2": 10,
"0/49/3": 20,
"0/49/4": true,
"0/49/5": null,
"0/49/6": null,
"0/49/7": null,
"0/49/65532": 2,
"0/49/65533": 1,
"0/49/65528": [1, 5, 7],
"0/49/65529": [0, 3, 4, 6, 8],
"0/49/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65530, 65531, 65532, 65533
],
"0/50/65532": 0,
"0/50/65533": 1,
"0/50/65528": [1],
"0/50/65529": [0],
"0/50/65531": [65528, 65529, 65530, 65531, 65532, 65533],
"0/51/0": [
{
"name": "eth0",
"isOperational": true,
"offPremiseServicesReachableIPv4": null,
"offPremiseServicesReachableIPv6": null,
"hardwareAddress": "/mQDt/2Q",
"IPv4Addresses": ["CjwBaQ=="],
"IPv6Addresses": [
"/VqgxiAxQib8ZAP//rf9kA==",
"IAEEcLs7AAb8ZAP//rf9kA==",
"/oAAAAAAAAD8ZAP//rf9kA=="
],
"type": 2
},
{
"name": "lo",
"isOperational": true,
"offPremiseServicesReachableIPv4": null,
"offPremiseServicesReachableIPv6": null,
"hardwareAddress": "AAAAAAAA",
"IPv4Addresses": ["fwAAAQ=="],
"IPv6Addresses": ["AAAAAAAAAAAAAAAAAAAAAQ=="],
"type": 0
}
],
"0/51/1": 1,
"0/51/2": 25,
"0/51/3": 0,
"0/51/4": 0,
"0/51/5": [],
"0/51/6": [],
"0/51/7": [],
"0/51/8": false,
"0/51/65532": 0,
"0/51/65533": 1,
"0/51/65528": [],
"0/51/65529": [0],
"0/51/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533
],
"0/52/0": [
{
"id": 26957,
"name": "26957",
"stackFreeCurrent": null,
"stackFreeMinimum": null,
"stackSize": null
},
{
"id": 26956,
"name": "26956",
"stackFreeCurrent": null,
"stackFreeMinimum": null,
"stackSize": null
},
{
"id": 26955,
"name": "26955",
"stackFreeCurrent": null,
"stackFreeMinimum": null,
"stackSize": null
},
{
"id": 26953,
"name": "26953",
"stackFreeCurrent": null,
"stackFreeMinimum": null,
"stackSize": null
},
{
"id": 26952,
"name": "26952",
"stackFreeCurrent": null,
"stackFreeMinimum": null,
"stackSize": null
}
],
"0/52/1": 351120,
"0/52/2": 529520,
"0/52/3": 529520,
"0/52/65532": 1,
"0/52/65533": 1,
"0/52/65528": [],
"0/52/65529": [0],
"0/52/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533],
"0/53/0": null,
"0/53/1": null,
"0/53/2": null,
"0/53/3": null,
"0/53/4": null,
"0/53/5": null,
"0/53/6": 0,
"0/53/7": [],
"0/53/8": [],
"0/53/9": null,
"0/53/10": null,
"0/53/11": null,
"0/53/12": null,
"0/53/13": null,
"0/53/14": 0,
"0/53/15": 0,
"0/53/16": 0,
"0/53/17": 0,
"0/53/18": 0,
"0/53/19": 0,
"0/53/20": 0,
"0/53/21": 0,
"0/53/22": 0,
"0/53/23": 0,
"0/53/24": 0,
"0/53/25": 0,
"0/53/26": 0,
"0/53/27": 0,
"0/53/28": 0,
"0/53/29": 0,
"0/53/30": 0,
"0/53/31": 0,
"0/53/32": 0,
"0/53/33": 0,
"0/53/34": 0,
"0/53/35": 0,
"0/53/36": 0,
"0/53/37": 0,
"0/53/38": 0,
"0/53/39": 0,
"0/53/40": 0,
"0/53/41": 0,
"0/53/42": 0,
"0/53/43": 0,
"0/53/44": 0,
"0/53/45": 0,
"0/53/46": 0,
"0/53/47": 0,
"0/53/48": 0,
"0/53/49": 0,
"0/53/50": 0,
"0/53/51": 0,
"0/53/52": 0,
"0/53/53": 0,
"0/53/54": 0,
"0/53/55": 0,
"0/53/56": null,
"0/53/57": null,
"0/53/58": null,
"0/53/59": null,
"0/53/60": null,
"0/53/61": null,
"0/53/62": [],
"0/53/65532": 15,
"0/53/65533": 1,
"0/53/65528": [],
"0/53/65529": [0],
"0/53/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 65528, 65529, 65530, 65531, 65532, 65533
],
"0/54/0": null,
"0/54/1": null,
"0/54/2": 3,
"0/54/3": null,
"0/54/4": null,
"0/54/5": null,
"0/54/6": null,
"0/54/7": null,
"0/54/8": null,
"0/54/9": null,
"0/54/10": null,
"0/54/11": null,
"0/54/12": null,
"0/54/65532": 3,
"0/54/65533": 1,
"0/54/65528": [],
"0/54/65529": [0],
"0/54/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65530, 65531,
65532, 65533
],
"0/55/0": null,
"0/55/1": false,
"0/55/2": 823,
"0/55/3": 969,
"0/55/4": 0,
"0/55/5": 0,
"0/55/6": 0,
"0/55/7": null,
"0/55/8": 25,
"0/55/65532": 3,
"0/55/65533": 1,
"0/55/65528": [],
"0/55/65529": [0],
"0/55/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533
],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65532": 0,
"0/60/65533": 1,
"0/60/65528": [],
"0/60/65529": [0, 1, 2],
"0/60/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533],
"0/62/0": [
{
"noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRARgkBwEkCAEwCUEE55h6CbNLPZH/uM3/rDdA+jeuuD2QSPN8gBeEB0bmGJqWz/gCT4/ySB77rK3XiwVWVAmJhJ/eMcTIA0XXWMqKPDcKNQEoARgkAgE2AwQCBAEYMAQUqnKiC76YFhcTHt4AQ/kAbtrZ2MowBRSL6EWyWm8+uC0Puc2/BncMqYbpmhgwC0AA05Z+y1mcyHUeOFJ5kyDJJMN/oNCwN5h8UpYN/868iuQArr180/fbaN1+db9lab4D2lf0HK7wgHIR3HsOa2w9GA==",
"icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE5R1DrUQE/L8tx95WR1g1dZJf4d+6LEB7JAYZN/nw9ZBUg5VOHDrB1xIw5KguYJzt10K+0KqQBBEbuwW+wLLobTcKNQEpARgkAmAwBBSL6EWyWm8+uC0Puc2/BncMqYbpmjAFFM0I6fPFzfOv2IWbX1huxb3eW0fqGDALQHXLE0TgIDW6XOnvtsOJCyKoENts8d4TQWBgTKviv1LF/+MS9eFYi+kO+1Idq5mVgwN+lH7eyecShQR0iqq6WLUY",
"fabricIndex": 1
}
],
"0/62/1": [
{
"rootPublicKey": "BJ/jL2MdDrdq9TahKSa5c/dBc166NRCU0W9l7hK2kcuVtN915DLqiS+RAJ2iPEvWK5FawZHF/QdKLZmTkZHudxY=",
"vendorId": 65521,
"fabricId": 1,
"nodeId": 1,
"label": "",
"fabricIndex": 1
}
],
"0/62/2": 16,
"0/62/3": 1,
"0/62/4": [
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEn+MvYx0Ot2r1NqEpJrlz90FzXro1EJTRb2XuEraRy5W033XkMuqJL5EAnaI8S9YrkVrBkcX9B0otmZORke53FjcKNQEpARgkAmAwBBTNCOnzxc3zr9iFm19YbsW93ltH6jAFFM0I6fPFzfOv2IWbX1huxb3eW0fqGDALQILjpR3BTSHHl6DQtvwzWkjmA+i5jjXdc3qjemFGFjFVAnV6dPLQo7tctC8Y0uL4ZNERga2/NZAt1gRD72S0YR4Y"
],
"0/62/5": 1,
"0/62/65532": 0,
"0/62/65533": 1,
"0/62/65528": [1, 3, 5, 8],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65530, 65531, 65532, 65533],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65532": 0,
"0/63/65533": 1,
"0/63/65528": [2, 5],
"0/63/65529": [0, 1, 3, 4],
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533],
"0/64/0": [
{
"label": "room",
"value": "bedroom 2"
},
{
"label": "orientation",
"value": "North"
},
{
"label": "floor",
"value": "2"
},
{
"label": "direction",
"value": "up"
}
],
"0/64/65532": 0,
"0/64/65533": 1,
"0/64/65528": [],
"0/64/65529": [],
"0/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533],
"0/65/0": [],
"0/65/65532": 0,
"0/65/65533": 1,
"0/65/65528": [],
"0/65/65529": [],
"0/65/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533],
"1/3/0": 0,
"1/3/1": 0,
"1/3/65532": 0,
"1/3/65533": 4,
"1/3/65528": [],
"1/3/65529": [0],
"1/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533],
"1/6/0": false,
"1/6/16384": true,
"1/6/16385": 0,
"1/6/16386": 0,
"1/6/16387": 0,
"1/6/65532": 0,
"1/6/65533": 4,
"1/6/65528": [],
"1/6/65529": [0, 1, 2],
"1/6/65531": [
0, 16384, 16385, 16386, 16387, 65528, 65529, 65530, 65531, 65532, 65533
],
"1/29/0": [
{
"deviceType": 10,
"revision": 1
}
],
"1/29/1": [3, 6, 29, 47, 257],
"1/29/2": [],
"1/29/3": [],
"1/29/65532": 0,
"1/29/65533": 1,
"1/29/65528": [],
"1/29/65529": [],
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533],
"1/47/0": 1,
"1/47/1": 1,
"1/47/2": "Battery",
"1/47/14": 0,
"1/47/15": false,
"1/47/16": 0,
"1/47/19": "",
"1/47/65532": 10,
"1/47/65533": 1,
"1/47/65528": [],
"1/47/65529": [],
"1/47/65531": [
0, 1, 2, 14, 15, 16, 19, 65528, 65529, 65530, 65531, 65532, 65533
],
"1/257/0": 1,
"1/257/1": 0,
"1/257/2": true,
"1/257/3": 1,
"1/257/17": 10,
"1/257/18": 10,
"1/257/19": 10,
"1/257/20": 10,
"1/257/21": 10,
"1/257/22": 10,
"1/257/23": 8,
"1/257/24": 6,
"1/257/25": 20,
"1/257/26": 10,
"1/257/27": 1,
"1/257/28": 5,
"1/257/33": "en",
"1/257/35": 60,
"1/257/36": 0,
"1/257/37": 0,
"1/257/38": 65526,
"1/257/41": false,
"1/257/43": false,
"1/257/48": 3,
"1/257/49": 10,
"1/257/51": false,
"1/257/65532": 7603,
"1/257/65533": 6,
"1/257/65528": [12, 15, 18, 28, 35, 37],
"1/257/65529": [
0, 1, 3, 11, 12, 13, 14, 15, 16, 17, 18, 19, 26, 27, 29, 34, 36, 38
],
"1/257/65531": [
0, 1, 2, 3, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 35, 36,
37, 38, 41, 43, 48, 49, 51, 65528, 65529, 65530, 65531, 65532, 65533
]
},
"attribute_subscriptions": []
}

View File

@ -10,6 +10,7 @@ from homeassistant.components.lock import (
STATE_LOCKING,
STATE_UNLOCKED,
STATE_UNLOCKING,
LockEntityFeature,
)
from homeassistant.const import ATTR_CODE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
@ -135,3 +136,51 @@ async def test_lock_requires_pin(
command=clusters.DoorLock.Commands.LockDoor(code.encode()),
timed_request_timeout_ms=1000,
)
# This tests needs to be adjusted to remove lingering tasks
@pytest.mark.parametrize("expected_lingering_tasks", [True])
async def test_lock_with_unbolt(
hass: HomeAssistant,
matter_client: MagicMock,
door_lock_with_unbolt: MatterNode,
) -> None:
"""Test door lock."""
state = hass.states.get("lock.mock_door_lock")
assert state
assert state.state == STATE_LOCKED
assert state.attributes["supported_features"] & LockEntityFeature.OPEN
# test unlock/unbolt
await hass.services.async_call(
"lock",
"unlock",
{
"entity_id": "lock.mock_door_lock",
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
# unlock should unbolt on a lock with unbolt feature
assert matter_client.send_device_command.call_args == call(
node_id=door_lock_with_unbolt.node_id,
endpoint_id=1,
command=clusters.DoorLock.Commands.UnboltDoor(),
timed_request_timeout_ms=1000,
)
matter_client.send_device_command.reset_mock()
# test open / unlatch
await hass.services.async_call(
"lock",
"open",
{
"entity_id": "lock.mock_door_lock",
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
assert matter_client.send_device_command.call_args == call(
node_id=door_lock_with_unbolt.node_id,
endpoint_id=1,
command=clusters.DoorLock.Commands.UnlockDoor(),
timed_request_timeout_ms=1000,
)

View File

@ -1297,7 +1297,7 @@ async def test_reload_after_invalid_config(
assert hass.states.get("alarm_control_panel.test") is None
assert (
"extra keys not allowed @ data['invalid_topic'] for "
"manual configured MQTT alarm_control_panel item, "
"manually configured MQTT alarm_control_panel item, "
"in ?, line ? Got {'name': 'test', 'invalid_topic': 'test-topic'}"
in caplog.text
)

View File

@ -139,7 +139,7 @@ async def test_preset_none_in_preset_modes(
) -> None:
"""Test the preset mode payload reset configuration."""
assert await mqtt_mock_entry()
assert "not a valid value" in caplog.text
assert "preset_modes must not include preset mode 'none'" in caplog.text
@pytest.mark.parametrize(

View File

@ -1788,7 +1788,7 @@ async def test_attributes(
},
False,
None,
"not a valid value",
"speed_range_max must be > speed_range_min",
),
(
"test14",
@ -1805,7 +1805,7 @@ async def test_attributes(
},
False,
None,
"not a valid value",
"speed_range_min must be > 0",
),
(
"test15",

View File

@ -2134,7 +2134,7 @@ async def test_setup_manual_mqtt_with_platform_key(
"""Test set up a manual MQTT item with a platform key."""
assert await mqtt_mock_entry()
assert (
"extra keys not allowed @ data['platform'] for manual configured MQTT light item"
"extra keys not allowed @ data['platform'] for manually configured MQTT light item"
in caplog.text
)
@ -2151,6 +2151,42 @@ async def test_setup_manual_mqtt_with_invalid_config(
assert "required key not provided" in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
"sensor": {
"name": "test",
"state_topic": "test-topic",
"entity_category": "config",
}
}
},
{
mqtt.DOMAIN: {
"binary_sensor": {
"name": "test",
"state_topic": "test-topic",
"entity_category": "config",
}
}
},
],
)
@patch(
"homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR, Platform.SENSOR]
)
async def test_setup_manual_mqtt_with_invalid_entity_category(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test set up a manual sensor item with an invalid entity category."""
assert await mqtt_mock_entry()
assert "Entity category `config` is invalid" in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [])
@pytest.mark.parametrize(
("mqtt_config_entry_data", "protocol"),

View File

@ -211,7 +211,7 @@ async def test_attribute_validation_max_greater_then_min(
) -> None:
"""Test the validation of min and max configuration attributes."""
assert await mqtt_mock_entry()
assert "not a valid value" in caplog.text
assert "text length min must be >= max" in caplog.text
@pytest.mark.parametrize(
@ -236,7 +236,7 @@ async def test_attribute_validation_max_not_greater_then_max_state_length(
) -> None:
"""Test the max value of of max configuration attribute."""
assert await mqtt_mock_entry()
assert "not a valid value" in caplog.text
assert "max text length must be <= 255" in caplog.text
@pytest.mark.parametrize(

View File

@ -40,5 +40,7 @@ async def test_adam_change_select_entity(
assert mock_smile_adam.set_schedule_state.call_count == 1
mock_smile_adam.set_schedule_state.assert_called_with(
"c50f167537524366a5af7aa3942feb1e", "Badkamer Schema", "on"
"c50f167537524366a5af7aa3942feb1e",
"on",
"Badkamer Schema",
)

View File

@ -153,6 +153,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None
"speed": 100,
"bearing": "105.32",
"altitude": 102,
"charge": "true",
}
req = await client.post(url, params=data)
@ -165,6 +166,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None
assert state.attributes["speed"] == 100.0
assert state.attributes["bearing"] == 105.32
assert state.attributes["altitude"] == 102.0
assert "charge" not in state.attributes
data = {
"lat": str(HOME_LATITUDE),

View File

@ -1,5 +1,5 @@
{
"nodeId": 12,
"nodeId": 131,
"index": 0,
"installerIcon": 6656,
"userIcon": 6656,
@ -7,12 +7,13 @@
"ready": true,
"isListening": false,
"isRouting": true,
"isSecure": true,
"isSecure": false,
"manufacturerId": 647,
"productId": 114,
"productType": 4,
"firmwareVersion": "3.12.1",
"zwavePlusVersion": 2,
"name": "Blind West Bed 1",
"deviceConfig": {
"filename": "/data/db/devices/0x0287/iblindsv3.json",
"isEmbedded": true,
@ -38,321 +39,61 @@
"associations": {},
"paramInformation": {
"_map": {}
},
"compat": {
"removeCCs": {}
}
},
"label": "iblinds V3",
"interviewAttempts": 1,
"endpoints": [
{
"nodeId": 12,
"index": 0,
"installerIcon": 6656,
"userIcon": 6656,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 7,
"label": "Motor Control Class C"
},
"mandatorySupportedCCs": [32, 38, 37, 114, 134],
"mandatoryControlledCCs": []
},
"commandClasses": [
{
"id": 38,
"name": "Multilevel Switch",
"version": 4,
"isSecure": true
},
{
"id": 37,
"name": "Binary Switch",
"version": 2,
"isSecure": true
},
{
"id": 114,
"name": "Manufacturer Specific",
"version": 2,
"isSecure": true
},
{
"id": 134,
"name": "Version",
"version": 3,
"isSecure": true
},
{
"id": 94,
"name": "Z-Wave Plus Info",
"version": 2,
"isSecure": false
},
{
"id": 133,
"name": "Association",
"version": 2,
"isSecure": true
},
{
"id": 89,
"name": "Association Group Information",
"version": 3,
"isSecure": true
},
{
"id": 85,
"name": "Transport Service",
"version": 2,
"isSecure": false
},
{
"id": 90,
"name": "Device Reset Locally",
"version": 1,
"isSecure": true
},
{
"id": 115,
"name": "Powerlevel",
"version": 1,
"isSecure": true
},
{
"id": 159,
"name": "Security 2",
"version": 1,
"isSecure": true
},
{
"id": 108,
"name": "Supervision",
"version": 1,
"isSecure": false
},
{
"id": 122,
"name": "Firmware Update Meta Data",
"version": 5,
"isSecure": true
},
{
"id": 128,
"name": "Battery",
"version": 1,
"isSecure": true
},
{
"id": 112,
"name": "Configuration",
"version": 4,
"isSecure": true
},
{
"id": 135,
"name": "Indicator",
"version": 3,
"isSecure": true
},
{
"id": 142,
"name": "Multi Channel Association",
"version": 3,
"isSecure": true
},
{
"id": 106,
"name": "Window Covering",
"version": 1,
"isSecure": true
},
{
"id": 152,
"name": "Security",
"version": 1,
"isSecure": true
}
]
"isFrequentListening": "1000ms",
"maxDataRate": 100000,
"supportedDataRates": [40000, 100000],
"protocolVersion": 3,
"supportsBeaming": true,
"supportsSecurity": false,
"nodeType": 1,
"zwavePlusNodeType": 0,
"zwavePlusRoleType": 7,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 7,
"label": "Motor Control Class C"
},
"mandatorySupportedCCs": [32, 38, 37, 114, 134],
"mandatoryControlledCCs": []
},
"interviewStage": "Complete",
"deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0287:0x0004:0x0072:3.12.1",
"statistics": {
"commandsTX": 95,
"commandsRX": 110,
"commandsDroppedRX": 0,
"commandsDroppedTX": 0,
"timeoutResponse": 0,
"rtt": 1295.6,
"lastSeen": "2023-11-02T18:41:40.552Z",
"rssi": -69,
"lwr": {
"protocolDataRate": 2,
"repeaters": [],
"rssi": -71,
"repeaterRSSI": []
}
],
},
"highestSecurityClass": -1,
"isControllerNode": false,
"keepAwake": false,
"lastSeen": "2023-11-02T18:41:40.552Z",
"values": [
{
"endpoint": 0,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 2,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": false,
"label": "Current value",
"stateful": true,
"secret": false
},
"value": false
},
{
"endpoint": 0,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 2,
"metadata": {
"type": "boolean",
"readable": true,
"writeable": true,
"label": "Target value",
"valueChangeOptions": ["transitionDuration"],
"stateful": true,
"secret": false
},
"value": false
},
{
"endpoint": 0,
"commandClass": 37,
"commandClassName": "Binary Switch",
"property": "duration",
"propertyName": "duration",
"ccVersion": 2,
"metadata": {
"type": "duration",
"readable": true,
"writeable": false,
"label": "Remaining duration",
"stateful": true,
"secret": false
},
"value": {
"value": 0,
"unit": "seconds"
}
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "targetValue",
"propertyName": "targetValue",
"ccVersion": 4,
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"label": "Target value",
"valueChangeOptions": ["transitionDuration"],
"min": 0,
"max": 99,
"stateful": true,
"secret": false
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "duration",
"propertyName": "duration",
"ccVersion": 4,
"metadata": {
"type": "duration",
"readable": true,
"writeable": false,
"label": "Remaining duration",
"stateful": true,
"secret": false
},
"value": {
"value": 0,
"unit": "seconds"
}
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "currentValue",
"propertyName": "currentValue",
"ccVersion": 4,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Current value",
"min": 0,
"max": 99,
"stateful": true,
"secret": false
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "Up",
"propertyName": "Up",
"ccVersion": 4,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Perform a level change (Up)",
"ccSpecific": {
"switchType": 2
},
"valueChangeOptions": ["transitionDuration"],
"stateful": true,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "Down",
"propertyName": "Down",
"ccVersion": 4,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Perform a level change (Down)",
"ccSpecific": {
"switchType": 2
},
"valueChangeOptions": ["transitionDuration"],
"stateful": true,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 38,
"commandClassName": "Multilevel Switch",
"property": "restorePrevious",
"propertyName": "restorePrevious",
"ccVersion": 4,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Restore previous value",
"stateful": true,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 106,
@ -361,7 +102,7 @@
"propertyKey": 23,
"propertyName": "currentValue",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"ccVersion": 1,
"metadata": {
"type": "number",
"readable": true,
@ -373,9 +114,9 @@
"min": 0,
"max": 99,
"states": {
"0": "Closed (up)",
"0": "Closed (up inside)",
"50": "Open",
"99": "Closed (down)"
"99": "Closed (down inside)"
},
"stateful": true,
"secret": false
@ -390,7 +131,7 @@
"propertyKey": 23,
"propertyName": "targetValue",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"ccVersion": 1,
"metadata": {
"type": "number",
"readable": true,
@ -403,14 +144,14 @@
"min": 0,
"max": 99,
"states": {
"0": "Closed (up)",
"0": "Closed (up inside)",
"50": "Open",
"99": "Closed (down)"
"99": "Closed (down inside)"
},
"stateful": true,
"secret": false
},
"value": 99
"value": 0
},
{
"endpoint": 0,
@ -420,7 +161,7 @@
"propertyKey": 23,
"propertyName": "duration",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"ccVersion": 1,
"metadata": {
"type": "duration",
"readable": true,
@ -441,44 +182,24 @@
"endpoint": 0,
"commandClass": 106,
"commandClassName": "Window Covering",
"property": "open",
"property": "levelChangeUp",
"propertyKey": 23,
"propertyName": "open",
"propertyName": "levelChangeUp",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Open - Horizontal Slats Angle",
"label": "Change tilt (down inside) - Horizontal Slats Angle",
"ccSpecific": {
"parameter": 23
},
"valueChangeOptions": ["transitionDuration"],
"stateful": true,
"secret": false
},
"nodeId": 12,
"value": true
},
{
"endpoint": 0,
"commandClass": 106,
"commandClassName": "Window Covering",
"property": "close0",
"propertyKey": 23,
"propertyName": "close0",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Close Up - Horizontal Slats Angle",
"ccSpecific": {
"parameter": 23
"states": {
"true": "Start",
"false": "Stop"
},
"valueChangeOptions": ["transitionDuration"],
"stateful": true,
"secret": false
}
@ -487,25 +208,27 @@
"endpoint": 0,
"commandClass": 106,
"commandClassName": "Window Covering",
"property": "close99",
"property": "levelChangeDown",
"propertyKey": 23,
"propertyName": "close99",
"propertyName": "levelChangeDown",
"propertyKeyName": "Horizontal Slats Angle",
"ccVersion": 0,
"ccVersion": 1,
"metadata": {
"type": "boolean",
"readable": false,
"writeable": true,
"label": "Close Down - Horizontal Slats Angle",
"label": "Change tilt (up inside) - Horizontal Slats Angle",
"ccSpecific": {
"parameter": 23
},
"valueChangeOptions": ["transitionDuration"],
"states": {
"true": "Start",
"false": "Stop"
},
"stateful": true,
"secret": false
},
"nodeId": 12,
"value": true
}
},
{
"endpoint": 0,
@ -604,7 +327,7 @@
"allowManualEntry": true,
"isFromConfig": true
},
"value": 50
"value": 45
},
{
"endpoint": 0,
@ -656,6 +379,32 @@
},
"value": 0
},
{
"endpoint": 0,
"commandClass": 112,
"commandClassName": "Configuration",
"property": 11,
"propertyName": "MC",
"ccVersion": 4,
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"description": "MC",
"label": "MC",
"default": 1,
"min": 0,
"max": 1,
"valueSize": 1,
"format": 0,
"noBulkSupport": true,
"isAdvanced": false,
"requiresReInclusion": false,
"allowManualEntry": true,
"isFromConfig": false
},
"value": 1
},
{
"endpoint": 0,
"commandClass": 112,
@ -721,7 +470,9 @@
"format": 0,
"allowManualEntry": true,
"isFromConfig": true
}
},
"nodeId": 131,
"value": 99
},
{
"endpoint": 0,
@ -1169,7 +920,9 @@
"max": 255,
"stateful": true,
"secret": false
}
},
"nodeId": 131,
"value": 47
},
{
"endpoint": 0,
@ -1183,54 +936,209 @@
"readable": false,
"writeable": true,
"label": "Identify",
"states": {
"true": "Identify"
},
"stateful": true,
"secret": false
}
},
{
"endpoint": 0,
"commandClass": 135,
"commandClassName": "Indicator",
"property": "timeout",
"propertyName": "timeout",
"ccVersion": 3,
"metadata": {
"type": "string",
"readable": true,
"writeable": true,
"label": "Timeout",
"stateful": true,
"secret": false
}
},
{
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"property": "targetValue",
"endpoint": 0,
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"label": "Target value",
"valueChangeOptions": ["transitionDuration"],
"min": 0,
"max": 99,
"stateful": true,
"secret": false
},
"propertyName": "targetValue",
"nodeId": 131,
"value": 45
},
{
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"property": "duration",
"endpoint": 0,
"metadata": {
"type": "duration",
"readable": true,
"writeable": false,
"label": "Remaining duration",
"stateful": true,
"secret": false
},
"propertyName": "duration",
"nodeId": 131,
"value": {
"value": 0,
"unit": "seconds"
}
},
{
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"property": "currentValue",
"endpoint": 0,
"metadata": {
"type": "number",
"readable": true,
"writeable": false,
"label": "Current value",
"min": 0,
"max": 99,
"stateful": true,
"secret": false
},
"propertyName": "currentValue",
"nodeId": 131,
"value": 45
}
],
"isFrequentListening": "1000ms",
"maxDataRate": 100000,
"supportedDataRates": [40000, 100000],
"protocolVersion": 3,
"supportsBeaming": true,
"supportsSecurity": false,
"nodeType": 1,
"zwavePlusNodeType": 0,
"zwavePlusRoleType": 7,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 7,
"label": "Motor Control Class C"
},
"mandatorySupportedCCs": [32, 38, 37, 114, 134],
"mandatoryControlledCCs": []
},
"interviewStage": "Complete",
"deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0287:0x0004:0x0072:3.12.1",
"statistics": {
"commandsTX": 109,
"commandsRX": 101,
"commandsDroppedRX": 2,
"commandsDroppedTX": 0,
"timeoutResponse": 8,
"rtt": 1217.2,
"rssi": -43,
"lwr": {
"protocolDataRate": 2,
"repeaters": [],
"rssi": -45,
"repeaterRSSI": []
"endpoints": [
{
"nodeId": 131,
"index": 0,
"installerIcon": 6656,
"userIcon": 6656,
"deviceClass": {
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 17,
"label": "Multilevel Switch"
},
"specific": {
"key": 7,
"label": "Motor Control Class C"
},
"mandatorySupportedCCs": [32, 38, 37, 114, 134],
"mandatoryControlledCCs": []
},
"commandClasses": [
{
"id": 114,
"name": "Manufacturer Specific",
"version": 2,
"isSecure": false
},
{
"id": 134,
"name": "Version",
"version": 3,
"isSecure": false
},
{
"id": 94,
"name": "Z-Wave Plus Info",
"version": 2,
"isSecure": false
},
{
"id": 133,
"name": "Association",
"version": 2,
"isSecure": false
},
{
"id": 89,
"name": "Association Group Information",
"version": 3,
"isSecure": false
},
{
"id": 85,
"name": "Transport Service",
"version": 2,
"isSecure": false
},
{
"id": 90,
"name": "Device Reset Locally",
"version": 1,
"isSecure": false
},
{
"id": 115,
"name": "Powerlevel",
"version": 1,
"isSecure": false
},
{
"id": 159,
"name": "Security 2",
"version": 1,
"isSecure": true
},
{
"id": 108,
"name": "Supervision",
"version": 1,
"isSecure": false
},
{
"id": 122,
"name": "Firmware Update Meta Data",
"version": 5,
"isSecure": false
},
{
"id": 128,
"name": "Battery",
"version": 1,
"isSecure": false
},
{
"id": 112,
"name": "Configuration",
"version": 4,
"isSecure": false
},
{
"id": 135,
"name": "Indicator",
"version": 3,
"isSecure": false
},
{
"id": 142,
"name": "Multi Channel Association",
"version": 3,
"isSecure": false
},
{
"id": 106,
"name": "Window Covering",
"version": 1,
"isSecure": false
}
]
}
},
"highestSecurityClass": 1,
"isControllerNode": false,
"keepAwake": false
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
"""Tests for zwave_js scripts."""

View File

@ -0,0 +1,83 @@
"""Test convert_device_diagnostics_to_fixture script."""
import copy
import json
from pathlib import Path
import sys
from unittest.mock import patch
import pytest
from homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture import (
extract_fixture_data,
get_fixtures_dir_path,
load_file,
main,
)
from tests.common import load_fixture
def _minify(text: str) -> str:
"""Minify string by removing whitespace and new lines."""
return text.replace(" ", "").replace("\n", "")
def test_fixture_functions() -> None:
"""Test functions related to the fixture."""
diagnostics_data = json.loads(load_fixture("zwave_js/device_diagnostics.json"))
state = extract_fixture_data(copy.deepcopy(diagnostics_data))
assert isinstance(state["values"], list)
assert (
get_fixtures_dir_path(state)
== Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json"
)
old_diagnostics_format_data = copy.deepcopy(diagnostics_data)
old_diagnostics_format_data["data"]["state"]["values"] = list(
old_diagnostics_format_data["data"]["state"]["values"].values()
)
old_diagnostics_format_data["data"]["state"]["endpoints"] = list(
old_diagnostics_format_data["data"]["state"]["endpoints"].values()
)
assert (
extract_fixture_data(old_diagnostics_format_data)
== old_diagnostics_format_data["data"]["state"]
)
with pytest.raises(ValueError):
extract_fixture_data({})
def test_load_file() -> None:
"""Test load file."""
assert load_file(
Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json"
) == json.loads(load_fixture("zwave_js/device_diagnostics.json"))
def test_main(capfd: pytest.CaptureFixture[str]) -> None:
"""Test main function."""
fixture_str = load_fixture("zwave_js/zooz_zse44_state.json")
fixture_dict = json.loads(fixture_str)
# Test dump to stdout
args = [
"homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py",
str(Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json"),
]
with patch.object(sys, "argv", args):
main()
captured = capfd.readouterr()
assert _minify(captured.out) == _minify(fixture_str)
# Check file dump
args.append("--file")
with patch.object(sys, "argv", args), patch(
"homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture.Path.write_text"
) as write_text_mock:
main()
assert len(write_text_mock.call_args_list) == 1
assert write_text_mock.call_args[0][0] == json.dumps(fixture_dict, indent=2)

View File

@ -829,7 +829,7 @@ async def test_iblinds_v3_cover(
hass: HomeAssistant, client, iblinds_v3, integration
) -> None:
"""Test iBlinds v3 cover which uses Window Covering CC."""
entity_id = "cover.window_blind_controller_horizontal_slats_angle"
entity_id = "cover.blind_west_bed_1_horizontal_slats_angle"
state = hass.states.get(entity_id)
assert state
# This device has no state because there is no position value
@ -854,7 +854,7 @@ async def test_iblinds_v3_cover(
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 12
assert args["nodeId"] == 131
assert args["valueId"] == {
"endpoint": 0,
"commandClass": 106,
@ -875,7 +875,7 @@ async def test_iblinds_v3_cover(
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 12
assert args["nodeId"] == 131
assert args["valueId"] == {
"endpoint": 0,
"commandClass": 106,
@ -896,7 +896,7 @@ async def test_iblinds_v3_cover(
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 12
assert args["nodeId"] == 131
assert args["valueId"] == {
"endpoint": 0,
"commandClass": 106,
@ -917,11 +917,11 @@ async def test_iblinds_v3_cover(
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 12
assert args["nodeId"] == 131
assert args["valueId"] == {
"endpoint": 0,
"commandClass": 106,
"property": "open",
"property": "levelChangeUp",
"propertyKey": 23,
}
assert args["value"] is False