mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
2023.9.3 (#100755)
This commit is contained in:
commit
0cbd46592a
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
uses: home-assistant/builder@2023.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -205,8 +205,6 @@ jobs:
|
||||
--cosign \
|
||||
--target /data \
|
||||
--generic ${{ needs.init.outputs.version }}
|
||||
env:
|
||||
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
||||
|
||||
- name: Archive translations
|
||||
shell: bash
|
||||
@ -275,15 +273,13 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
uses: home-assistant/builder@2023.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
--target /data/machine \
|
||||
--cosign \
|
||||
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
||||
env:
|
||||
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
|
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@ -97,7 +97,7 @@ jobs:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
uses: home-assistant/wheels@2023.09.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@ -178,7 +178,7 @@ jobs:
|
||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||
|
||||
- name: Build wheels (part 1)
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
uses: home-assistant/wheels@2023.09.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@ -192,7 +192,7 @@ jobs:
|
||||
requirements: "requirements_all.txtaa"
|
||||
|
||||
- name: Build wheels (part 2)
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
uses: home-assistant/wheels@2023.09.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@ -206,7 +206,7 @@ jobs:
|
||||
requirements: "requirements_all.txtab"
|
||||
|
||||
- name: Build wheels (part 3)
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
uses: home-assistant/wheels@2023.09.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
@ -58,6 +58,16 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi
|
||||
"""Describes Airnow sensor entity."""
|
||||
|
||||
|
||||
def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Process extra attributes for station location (if available)."""
|
||||
if ATTR_API_STATION in data:
|
||||
return {
|
||||
"lat": data.get(ATTR_API_STATION_LATITUDE),
|
||||
"long": data.get(ATTR_API_STATION_LONGITUDE),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||
AirNowEntityDescription(
|
||||
key=ATTR_API_AQI,
|
||||
@ -93,10 +103,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||
translation_key="station",
|
||||
icon="mdi:blur",
|
||||
value_fn=lambda data: data.get(ATTR_API_STATION),
|
||||
extra_state_attributes_fn=lambda data: {
|
||||
"lat": data[ATTR_API_STATION_LATITUDE],
|
||||
"long": data[ATTR_API_STATION_LONGITUDE],
|
||||
},
|
||||
extra_state_attributes_fn=station_extra_attrs,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -144,7 +144,8 @@ def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
|
||||
not matching_reg_entry or "(" not in entry.unique_id
|
||||
):
|
||||
matching_reg_entry = entry
|
||||
if not matching_reg_entry:
|
||||
if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id:
|
||||
# Already has the newest unique id format
|
||||
return
|
||||
entity_id = matching_reg_entry.entity_id
|
||||
ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)
|
||||
|
@ -298,6 +298,26 @@ class Pipeline:
|
||||
|
||||
id: str = field(default_factory=ulid_util.ulid)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: dict[str, Any]) -> Pipeline:
|
||||
"""Create an instance from a JSON serialization.
|
||||
|
||||
This function was added in HA Core 2023.10, previous versions will raise
|
||||
if there are unexpected items in the serialized data.
|
||||
"""
|
||||
return cls(
|
||||
conversation_engine=data["conversation_engine"],
|
||||
conversation_language=data["conversation_language"],
|
||||
id=data["id"],
|
||||
language=data["language"],
|
||||
name=data["name"],
|
||||
stt_engine=data["stt_engine"],
|
||||
stt_language=data["stt_language"],
|
||||
tts_engine=data["tts_engine"],
|
||||
tts_language=data["tts_language"],
|
||||
tts_voice=data["tts_voice"],
|
||||
)
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""Return a JSON serializable representation for storage."""
|
||||
return {
|
||||
@ -1205,7 +1225,7 @@ class PipelineStorageCollection(
|
||||
|
||||
def _deserialize_item(self, data: dict) -> Pipeline:
|
||||
"""Create an item from its serialized representation."""
|
||||
return Pipeline(**data)
|
||||
return Pipeline.from_json(data)
|
||||
|
||||
def _serialize_item(self, item_id: str, item: Pipeline) -> dict:
|
||||
"""Return the serialized representation of an item for storing."""
|
||||
|
@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.8.0", "yalexs-ble==2.2.3"]
|
||||
"requirements": ["yalexs==1.9.0", "yalexs-ble==2.3.0"]
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions
|
||||
from aiocomelit import ComeliteSerialBridgeApi, exceptions as aiocomelit_exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core, exceptions
|
||||
@ -37,7 +37,7 @@ async def validate_input(
|
||||
) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN])
|
||||
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PIN])
|
||||
|
||||
try:
|
||||
await api.login()
|
||||
|
@ -3,11 +3,14 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import ComeliteSerialBridgeAPi
|
||||
from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject
|
||||
from aiocomelit.const import BRIDGE
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
@ -16,13 +19,15 @@ from .const import _LOGGER, DOMAIN
|
||||
class ComelitSerialBridge(DataUpdateCoordinator):
|
||||
"""Queries Comelit Serial Bridge."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None:
|
||||
"""Initialize the scanner."""
|
||||
|
||||
self._host = host
|
||||
self._pin = pin
|
||||
|
||||
self.api = ComeliteSerialBridgeAPi(host, pin)
|
||||
self.api = ComeliteSerialBridgeApi(host, pin)
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
@ -30,6 +35,38 @@ class ComelitSerialBridge(DataUpdateCoordinator):
|
||||
name=f"{DOMAIN}-{host}-coordinator",
|
||||
update_interval=timedelta(seconds=5),
|
||||
)
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={(DOMAIN, self.config_entry.entry_id)},
|
||||
model=BRIDGE,
|
||||
name=f"{BRIDGE} ({self.api.host})",
|
||||
**self.basic_device_info,
|
||||
)
|
||||
|
||||
@property
|
||||
def basic_device_info(self) -> dict:
|
||||
"""Set basic device info."""
|
||||
|
||||
return {
|
||||
"manufacturer": "Comelit",
|
||||
"hw_version": "20003101",
|
||||
}
|
||||
|
||||
def platform_device_info(
|
||||
self, device: ComelitSerialBridgeObject, platform: str
|
||||
) -> dr.DeviceInfo:
|
||||
"""Set platform device info."""
|
||||
|
||||
return dr.DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{self.config_entry.entry_id}-{platform}-{device.index}")
|
||||
},
|
||||
via_device=(DOMAIN, self.config_entry.entry_id),
|
||||
name=device.name,
|
||||
model=f"{BRIDGE} {platform}",
|
||||
**self.basic_device_info,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update router data."""
|
||||
|
@ -9,7 +9,6 @@ from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON
|
||||
from homeassistant.components.light import LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@ -37,27 +36,20 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
||||
"""Light device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ComelitSerialBridge,
|
||||
device: ComelitSerialBridgeObject,
|
||||
config_entry_unique_id: str | None,
|
||||
config_entry_unique_id: str,
|
||||
) -> None:
|
||||
"""Init light entity."""
|
||||
self._api = coordinator.api
|
||||
self._device = device
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = device.name
|
||||
self._attr_unique_id = f"{config_entry_unique_id}-{device.index}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, self._attr_unique_id),
|
||||
},
|
||||
manufacturer="Comelit",
|
||||
model="Serial Bridge",
|
||||
name=device.name,
|
||||
)
|
||||
self._attr_device_info = self.coordinator.platform_device_info(device, LIGHT)
|
||||
|
||||
async def _light_set_state(self, state: int) -> None:
|
||||
"""Set desired light state."""
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.0.5"]
|
||||
"requirements": ["aiocomelit==0.0.8"]
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"flow_title": "{host}",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "Please enter the correct PIN for VEDO system: {host}",
|
||||
"description": "Please enter the correct PIN for {host}",
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.8.2"]
|
||||
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"]
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ async def async_setup_entry( # noqa: C901
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
async with asyncio.timeout(30):
|
||||
return await device.device.async_check_firmware_available()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["devolo_plc_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["devolo-plc-api==1.4.0"],
|
||||
"requirements": ["devolo-plc-api==1.4.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_dvl-deviceapi._tcp.local.",
|
||||
|
@ -326,6 +326,7 @@ class Thermostat(ClimateEntity):
|
||||
self._attr_unique_id = self.thermostat["identifier"]
|
||||
self.vacation = None
|
||||
self._last_active_hvac_mode = HVACMode.HEAT_COOL
|
||||
self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL
|
||||
|
||||
self._attr_hvac_modes = []
|
||||
if self.settings["heatStages"] or self.settings["hasHeatPump"]:
|
||||
@ -541,13 +542,14 @@ class Thermostat(ClimateEntity):
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
_LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat")
|
||||
self._last_hvac_mode_before_aux_heat = self.hvac_mode
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
_LOGGER.debug("Setting HVAC mode to last mode to disable aux heat")
|
||||
self.set_hvac_mode(self._last_active_hvac_mode)
|
||||
self.set_hvac_mode(self._last_hvac_mode_before_aux_heat)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.11.0"],
|
||||
"requirements": ["pyenphase==1.11.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/fritz",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["fritzconnection"],
|
||||
"requirements": ["fritzconnection[qr]==1.12.2", "xmltodict==0.13.0"],
|
||||
"requirements": ["fritzconnection[qr]==1.13.2", "xmltodict==0.13.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["fritzconnection"],
|
||||
"requirements": ["fritzconnection[qr]==1.12.2"]
|
||||
"requirements": ["fritzconnection[qr]==1.13.2"]
|
||||
}
|
||||
|
@ -189,7 +189,11 @@ class FritzBoxCallMonitor:
|
||||
_LOGGER.debug("Setting up socket connection")
|
||||
try:
|
||||
self.connection = FritzMonitor(address=self.host, port=self.port)
|
||||
kwargs: dict[str, Any] = {"event_queue": self.connection.start()}
|
||||
kwargs: dict[str, Any] = {
|
||||
"event_queue": self.connection.start(
|
||||
reconnect_tries=50, reconnect_delay=120
|
||||
)
|
||||
}
|
||||
Thread(target=self._process_events, kwargs=kwargs).start()
|
||||
except OSError as err:
|
||||
self.connection = None
|
||||
|
@ -103,10 +103,7 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
||||
else:
|
||||
self._daily_forecast = None
|
||||
|
||||
if self._period == 1 or self._forecast_listeners["hourly"]:
|
||||
await self._update_forecast("hourly", 1, True)
|
||||
else:
|
||||
self._hourly_forecast = None
|
||||
|
||||
_LOGGER.debug(
|
||||
"Updated location %s based on %s, current observation %s",
|
||||
@ -139,8 +136,8 @@ class IPMAWeather(WeatherEntity, IPMADevice):
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
forecast = self._hourly_forecast or self._daily_forecast
|
||||
"""Return the current condition which is only available on the hourly forecast data."""
|
||||
forecast = self._hourly_forecast
|
||||
|
||||
if not forecast:
|
||||
return
|
||||
|
@ -196,9 +196,9 @@ async def async_setup_entry(
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
|
||||
coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
|
||||
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data[
|
||||
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
|
||||
COORDINATOR_ALERT
|
||||
]
|
||||
)
|
||||
|
||||
entities: list[MeteoFranceSensor[Any]] = [
|
||||
MeteoFranceSensor(coordinator_forecast, description)
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mill", "mill_local"],
|
||||
"requirements": ["millheater==0.11.2", "mill-local==0.2.0"]
|
||||
"requirements": ["millheater==0.11.5", "mill-local==0.2.0"]
|
||||
}
|
||||
|
@ -462,6 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
|
||||
|
||||
add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received)
|
||||
|
||||
@callback
|
||||
def _rgbx_received(
|
||||
msg: ReceiveMessage,
|
||||
template: str,
|
||||
@ -532,11 +533,26 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def rgbww_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle new MQTT messages for RGBWW."""
|
||||
|
||||
@callback
|
||||
def _converter(
|
||||
r: int, g: int, b: int, cw: int, ww: int
|
||||
) -> tuple[int, int, int]:
|
||||
min_kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
self.max_mireds
|
||||
)
|
||||
max_kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
self.min_mireds
|
||||
)
|
||||
return color_util.color_rgbww_to_rgb(
|
||||
r, g, b, cw, ww, min_kelvin, max_kelvin
|
||||
)
|
||||
|
||||
rgbww = _rgbx_received(
|
||||
msg,
|
||||
CONF_RGBWW_VALUE_TEMPLATE,
|
||||
ColorMode.RGBWW,
|
||||
color_util.color_rgbww_to_rgb,
|
||||
_converter,
|
||||
)
|
||||
if rgbww is None:
|
||||
return
|
||||
|
@ -190,8 +190,6 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
except CannotLoginException:
|
||||
errors["base"] = "config"
|
||||
|
||||
if errors:
|
||||
return await self._show_setup_form(user_input, errors)
|
||||
|
||||
config_data = {
|
||||
@ -204,6 +202,10 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check if already configured
|
||||
info = await self.hass.async_add_executor_job(api.get_info)
|
||||
if info is None:
|
||||
errors["base"] = "info"
|
||||
return await self._show_setup_form(user_input, errors)
|
||||
|
||||
await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(updates=config_data)
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynetgear"],
|
||||
"requirements": ["pynetgear==0.10.9"],
|
||||
"requirements": ["pynetgear==0.10.10"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "NETGEAR, Inc.",
|
||||
|
@ -11,7 +11,8 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"config": "Connection or login error: please check your configuration"
|
||||
"config": "Connection or login error: please check your configuration",
|
||||
"info": "Failed to get info from router"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
|
@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
options: dict[str, Any],
|
||||
) -> FlowResult:
|
||||
"""Create the config entry."""
|
||||
# Prevent devices with the same serial number. If the device does not have a serial number
|
||||
# then we can at least prevent configuring the same host twice.
|
||||
if serial_number:
|
||||
await self.async_set_unique_id(serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
self._async_abort_entries_match(data)
|
||||
return self.async_create_entry(
|
||||
title=data[CONF_HOST],
|
||||
data=data,
|
||||
|
@ -61,7 +61,8 @@ class ReolinkHost:
|
||||
)
|
||||
|
||||
self.webhook_id: str | None = None
|
||||
self._onvif_supported: bool = True
|
||||
self._onvif_push_supported: bool = True
|
||||
self._onvif_long_poll_supported: bool = True
|
||||
self._base_url: str = ""
|
||||
self._webhook_url: str = ""
|
||||
self._webhook_reachable: bool = False
|
||||
@ -97,7 +98,9 @@ class ReolinkHost:
|
||||
f"'{self._api.user_level}', only admin users can change camera settings"
|
||||
)
|
||||
|
||||
self._onvif_supported = self._api.supported(None, "ONVIF")
|
||||
onvif_supported = self._api.supported(None, "ONVIF")
|
||||
self._onvif_push_supported = onvif_supported
|
||||
self._onvif_long_poll_supported = onvif_supported
|
||||
|
||||
enable_rtsp = None
|
||||
enable_onvif = None
|
||||
@ -109,7 +112,7 @@ class ReolinkHost:
|
||||
)
|
||||
enable_rtsp = True
|
||||
|
||||
if not self._api.onvif_enabled and self._onvif_supported:
|
||||
if not self._api.onvif_enabled and onvif_supported:
|
||||
_LOGGER.debug(
|
||||
"ONVIF is disabled on %s, trying to enable it", self._api.nvr_name
|
||||
)
|
||||
@ -157,11 +160,11 @@ class ReolinkHost:
|
||||
|
||||
self._unique_id = format_mac(self._api.mac_address)
|
||||
|
||||
if self._onvif_supported:
|
||||
if self._onvif_push_supported:
|
||||
try:
|
||||
await self.subscribe()
|
||||
except NotSupportedError:
|
||||
self._onvif_supported = False
|
||||
self._onvif_push_supported = False
|
||||
self.unregister_webhook()
|
||||
await self._api.unsubscribe()
|
||||
else:
|
||||
@ -179,12 +182,27 @@ class ReolinkHost:
|
||||
self._cancel_onvif_check = async_call_later(
|
||||
self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif
|
||||
)
|
||||
if not self._onvif_supported:
|
||||
if not self._onvif_push_supported:
|
||||
_LOGGER.debug(
|
||||
"Camera model %s does not support ONVIF, using fast polling instead",
|
||||
"Camera model %s does not support ONVIF push, using ONVIF long polling instead",
|
||||
self._api.model,
|
||||
)
|
||||
try:
|
||||
await self._async_start_long_polling(initial=True)
|
||||
except NotSupportedError:
|
||||
_LOGGER.debug(
|
||||
"Camera model %s does not support ONVIF long polling, using fast polling instead",
|
||||
self._api.model,
|
||||
)
|
||||
self._onvif_long_poll_supported = False
|
||||
await self._api.unsubscribe()
|
||||
await self._async_poll_all_motion()
|
||||
else:
|
||||
self._cancel_long_poll_check = async_call_later(
|
||||
self._hass,
|
||||
FIRST_ONVIF_LONG_POLL_TIMEOUT,
|
||||
self._async_check_onvif_long_poll,
|
||||
)
|
||||
|
||||
if self._api.sw_version_update_required:
|
||||
ir.async_create_issue(
|
||||
@ -317,11 +335,22 @@ class ReolinkHost:
|
||||
str(err),
|
||||
)
|
||||
|
||||
async def _async_start_long_polling(self):
|
||||
async def _async_start_long_polling(self, initial=False):
|
||||
"""Start ONVIF long polling task."""
|
||||
if self._long_poll_task is None:
|
||||
try:
|
||||
await self._api.subscribe(sub_type=SubType.long_poll)
|
||||
except NotSupportedError as err:
|
||||
if initial:
|
||||
raise err
|
||||
# make sure the long_poll_task is always created to try again later
|
||||
if not self._lost_subscription:
|
||||
self._lost_subscription = True
|
||||
_LOGGER.error(
|
||||
"Reolink %s event long polling subscription lost: %s",
|
||||
self._api.nvr_name,
|
||||
str(err),
|
||||
)
|
||||
except ReolinkError as err:
|
||||
# make sure the long_poll_task is always created to try again later
|
||||
if not self._lost_subscription:
|
||||
@ -381,12 +410,11 @@ class ReolinkHost:
|
||||
|
||||
async def renew(self) -> None:
|
||||
"""Renew the subscription of motion events (lease time is 15 minutes)."""
|
||||
if not self._onvif_supported:
|
||||
return
|
||||
|
||||
try:
|
||||
if self._onvif_push_supported:
|
||||
await self._renew(SubType.push)
|
||||
if self._long_poll_task is not None:
|
||||
|
||||
if self._onvif_long_poll_supported and self._long_poll_task is not None:
|
||||
if not self._api.subscribed(SubType.long_poll):
|
||||
_LOGGER.debug("restarting long polling task")
|
||||
# To prevent 5 minute request timeout
|
||||
|
@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.7.9"]
|
||||
"requirements": ["reolink-aio==0.7.10"]
|
||||
}
|
||||
|
@ -223,6 +223,7 @@
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"auto": "Auto",
|
||||
"onatnight": "On at night",
|
||||
"schedule": "Schedule",
|
||||
"adaptive": "Adaptive",
|
||||
"autoadaptive": "Auto adaptive"
|
||||
|
@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"requirements": ["ring-doorbell==0.7.2"]
|
||||
"requirements": ["ring-doorbell==0.7.3"]
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class RoborockEntity(Entity):
|
||||
|
||||
async def send(
|
||||
self,
|
||||
command: RoborockCommand,
|
||||
command: RoborockCommand | str,
|
||||
params: dict[str, Any] | list[Any] | int | None = None,
|
||||
) -> dict:
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
@ -48,7 +48,7 @@ class RoborockEntity(Entity):
|
||||
response = await self._api.send_command(command, params)
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
f"Error while calling {command.name} with {params}"
|
||||
f"Error while calling {command.name if isinstance(command, RoborockCommand) else command} with {params}"
|
||||
) from err
|
||||
|
||||
return response
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roborock",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": ["python-roborock==0.33.2"]
|
||||
"requirements": ["python-roborock==0.34.1"]
|
||||
}
|
||||
|
@ -164,10 +164,10 @@
|
||||
"dnd_end_time": {
|
||||
"name": "Do not disturb end"
|
||||
},
|
||||
"off_peak_start_time": {
|
||||
"off_peak_start": {
|
||||
"name": "Off-peak start"
|
||||
},
|
||||
"off_peak_end_time": {
|
||||
"off_peak_end": {
|
||||
"name": "Off-peak end"
|
||||
}
|
||||
},
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyschlage==2023.8.1"]
|
||||
"requirements": ["pyschlage==2023.9.1"]
|
||||
}
|
||||
|
@ -54,7 +54,15 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode"
|
||||
ATTR_LIGHT = "light"
|
||||
BOOST_INCLUSIVE = "boost_inclusive"
|
||||
|
||||
AVAILABLE_FAN_MODES = {"quiet", "low", "medium", "medium_high", "high", "auto"}
|
||||
AVAILABLE_FAN_MODES = {
|
||||
"quiet",
|
||||
"low",
|
||||
"medium",
|
||||
"medium_high",
|
||||
"high",
|
||||
"strong",
|
||||
"auto",
|
||||
}
|
||||
AVAILABLE_SWING_MODES = {
|
||||
"stopped",
|
||||
"fixedtop",
|
||||
|
@ -127,6 +127,7 @@
|
||||
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]",
|
||||
"medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]",
|
||||
"medium_high": "Medium high",
|
||||
"strong": "Strong",
|
||||
"quiet": "Quiet"
|
||||
}
|
||||
},
|
||||
@ -211,6 +212,7 @@
|
||||
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]",
|
||||
"medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]",
|
||||
"medium_high": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_high%]",
|
||||
"strong": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::strong%]",
|
||||
"quiet": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::quiet%]"
|
||||
}
|
||||
},
|
||||
@ -347,6 +349,7 @@
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"quiet": "Quiet",
|
||||
"strong": "Strong",
|
||||
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]",
|
||||
"medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]",
|
||||
"medium_high": "Medium high",
|
||||
|
@ -16,5 +16,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sensirion_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["sensirion-ble==0.1.0"]
|
||||
"requirements": ["sensirion-ble==0.1.1"]
|
||||
}
|
||||
|
@ -345,7 +345,7 @@ class SensorEntity(Entity):
|
||||
"""Return initial entity options.
|
||||
|
||||
These will be stored in the entity registry the first time the entity is seen,
|
||||
and then never updated.
|
||||
and then only updated if the unit system is changed.
|
||||
"""
|
||||
suggested_unit_of_measurement = self._get_initial_suggested_unit()
|
||||
|
||||
@ -783,7 +783,7 @@ class SensorEntity(Entity):
|
||||
registry = er.async_get(self.hass)
|
||||
initial_options = self.get_initial_entity_options() or {}
|
||||
registry.async_update_entity_options(
|
||||
self.entity_id,
|
||||
self.registry_entry.entity_id,
|
||||
f"{DOMAIN}.private",
|
||||
initial_options.get(f"{DOMAIN}.private"),
|
||||
)
|
||||
|
@ -44,7 +44,12 @@ from homeassistant.util.unit_conversion import (
|
||||
|
||||
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
|
||||
|
||||
CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys())
|
||||
CHECK_FORECAST_KEYS = (
|
||||
set().union(Forecast.__annotations__.keys())
|
||||
# Manually add the forecast resulting attributes that only exists
|
||||
# as native_* in the Forecast definition
|
||||
.union(("apparent_temperature", "wind_gust_speed", "dew_point"))
|
||||
)
|
||||
|
||||
CONDITION_CLASSES = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
@ -434,7 +439,8 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
|
||||
diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS)
|
||||
if diff_result:
|
||||
raise vol.Invalid(
|
||||
"Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/"
|
||||
f"Only valid keys in Forecast are allowed, unallowed keys: ({diff_result}), "
|
||||
"see Weather documentation https://www.home-assistant.io/integrations/weather/"
|
||||
)
|
||||
if forecast_type == "twice_daily" and "is_daytime" not in forecast:
|
||||
raise vol.Invalid(
|
||||
|
@ -36,3 +36,5 @@ change:
|
||||
example: "00:01:00, 60 or -60"
|
||||
selector:
|
||||
text:
|
||||
|
||||
reload:
|
||||
|
@ -62,6 +62,10 @@
|
||||
"description": "Duration to add or subtract to the running timer."
|
||||
}
|
||||
}
|
||||
},
|
||||
"reload": {
|
||||
"name": "[%key:common::action::reload%]",
|
||||
"description": "Reloads timers from the YAML-configuration."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +221,6 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
await self.async_refresh()
|
||||
|
||||
self.update_interval = async_set_update_interval(self.hass, self._api)
|
||||
self._next_refresh = None
|
||||
self._async_unsub_refresh()
|
||||
if self._listeners:
|
||||
self._schedule_refresh()
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
|
||||
SensorExtraStoredData,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.recorder import _suggest_report_issue
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
@ -484,6 +485,12 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
DATA_TARIFF_SENSORS
|
||||
]:
|
||||
sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
|
||||
if self._unit_of_measurement is None:
|
||||
_LOGGER.warning(
|
||||
"Source sensor %s has no unit of measurement. Please %s",
|
||||
self._sensor_source_id,
|
||||
_suggest_report_issue(self.hass, self._sensor_source_id),
|
||||
)
|
||||
|
||||
if (
|
||||
adjustment := self.calculate_adjustment(old_state, new_state)
|
||||
@ -491,6 +498,7 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
# If net_consumption is off, the adjustment must be non-negative
|
||||
self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
|
||||
|
||||
self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
self._last_valid_state = new_state_val
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pywaze", "homeassistant.helpers.location"],
|
||||
"requirements": ["pywaze==0.4.0"]
|
||||
"requirements": ["pywaze==0.5.0"]
|
||||
}
|
||||
|
@ -169,8 +169,12 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity):
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
method = getattr(self._device, self.entity_description.method_press)
|
||||
params = self.entity_description.method_press_params
|
||||
if params is not None:
|
||||
await self._try_command(
|
||||
self.entity_description.method_press_error_message,
|
||||
method,
|
||||
self.entity_description.method_press_params,
|
||||
self.entity_description.method_press_error_message, method, params
|
||||
)
|
||||
else:
|
||||
await self._try_command(
|
||||
self.entity_description.method_press_error_message, method
|
||||
)
|
||||
|
@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["yalexs-ble==2.2.3"]
|
||||
"requirements": ["yalexs-ble==2.3.0"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["auth", "application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/yolink",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["yolink-api==0.3.0"]
|
||||
"requirements": ["yolink-api==0.3.1"]
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.36.3",
|
||||
"bellows==0.36.4",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.103",
|
||||
@ -30,7 +30,7 @@
|
||||
"zigpy-xbee==0.18.2",
|
||||
"zigpy-zigate==0.11.0",
|
||||
"zigpy-znp==0.11.4",
|
||||
"universal-silabs-flasher==0.0.13"
|
||||
"universal-silabs-flasher==0.0.14"
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
|
@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zwave_js_server"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"],
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.3"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "0658",
|
||||
|
@ -7,7 +7,7 @@ from typing import Final
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__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)
|
||||
|
@ -81,7 +81,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
self._shutdown_requested = False
|
||||
self.config_entry = config_entries.current_entry.get()
|
||||
self.always_update = always_update
|
||||
self._next_refresh: float | None = None
|
||||
|
||||
# It's None before the first successful update.
|
||||
# Components should call async_config_entry_first_refresh
|
||||
@ -184,7 +183,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
"""Unschedule any pending refresh since there is no longer any listeners."""
|
||||
self._async_unsub_refresh()
|
||||
self._debounced_refresh.async_cancel()
|
||||
self._next_refresh = None
|
||||
|
||||
def async_contexts(self) -> Generator[Any, None, None]:
|
||||
"""Return all registered contexts."""
|
||||
@ -220,13 +218,13 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
# We use event.async_call_at because DataUpdateCoordinator does
|
||||
# not need an exact update interval.
|
||||
now = self.hass.loop.time()
|
||||
if self._next_refresh is None or self._next_refresh <= now:
|
||||
self._next_refresh = int(now) + self._microsecond
|
||||
self._next_refresh += self.update_interval.total_seconds()
|
||||
|
||||
next_refresh = int(now) + self._microsecond
|
||||
next_refresh += self.update_interval.total_seconds()
|
||||
self._unsub_refresh = event.async_call_at(
|
||||
self.hass,
|
||||
self._job,
|
||||
self._next_refresh,
|
||||
next_refresh,
|
||||
)
|
||||
|
||||
async def _handle_refresh_interval(self, _now: datetime) -> None:
|
||||
@ -265,7 +263,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
|
||||
async def async_refresh(self) -> None:
|
||||
"""Refresh data and log errors."""
|
||||
self._next_refresh = None
|
||||
await self._async_refresh(log_failures=True)
|
||||
|
||||
async def _async_refresh( # noqa: C901
|
||||
@ -405,7 +402,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
"""Manually update data, notify listeners and reset refresh interval."""
|
||||
self._async_unsub_refresh()
|
||||
self._debounced_refresh.async_cancel()
|
||||
self._next_refresh = None
|
||||
|
||||
self.data = data
|
||||
self.last_update_success = True
|
||||
|
@ -23,7 +23,7 @@ hass-nabucasa==0.71.0
|
||||
hassil==1.2.5
|
||||
home-assistant-bluetooth==1.10.3
|
||||
home-assistant-frontend==20230911.0
|
||||
home-assistant-intents==2023.8.2
|
||||
home-assistant-intents==2023.9.22
|
||||
httpx==0.24.1
|
||||
ifaddr==0.2.0
|
||||
janus==1.0.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.9.2"
|
||||
version = "2023.9.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -209,7 +209,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.6.0
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.0.5
|
||||
aiocomelit==0.0.8
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.4.16
|
||||
@ -509,7 +509,7 @@ beautifulsoup4==4.12.2
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.36.3
|
||||
bellows==0.36.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected==0.14.0
|
||||
@ -670,7 +670,7 @@ denonavr==0.11.3
|
||||
devolo-home-control-api==0.18.2
|
||||
|
||||
# homeassistant.components.devolo_home_network
|
||||
devolo-plc-api==1.4.0
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@ -829,7 +829,7 @@ freesms==0.2.0
|
||||
|
||||
# homeassistant.components.fritz
|
||||
# homeassistant.components.fritzbox_callmonitor
|
||||
fritzconnection[qr]==1.12.2
|
||||
fritzconnection[qr]==1.13.2
|
||||
|
||||
# homeassistant.components.google_translate
|
||||
gTTS==2.2.4
|
||||
@ -997,7 +997,7 @@ holidays==0.28
|
||||
home-assistant-frontend==20230911.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.8.2
|
||||
home-assistant-intents==2023.9.22
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -1213,7 +1213,7 @@ micloud==0.5
|
||||
mill-local==0.2.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.11.2
|
||||
millheater==0.11.5
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==7.1.12
|
||||
@ -1671,7 +1671,7 @@ pyedimax==0.2.1
|
||||
pyefergy==22.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.11.0
|
||||
pyenphase==1.11.4
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.6
|
||||
@ -1866,7 +1866,7 @@ pymyq==3.1.4
|
||||
pymysensors==0.24.0
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.9
|
||||
pynetgear==0.10.10
|
||||
|
||||
# homeassistant.components.netio
|
||||
pynetio==0.1.9.1
|
||||
@ -1988,7 +1988,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2023.8.1
|
||||
pyschlage==2023.9.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.33
|
||||
@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.33.2
|
||||
python-roborock==0.34.1
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@ -2231,7 +2231,7 @@ pyvlx==0.2.20
|
||||
pyvolumio==0.1.5
|
||||
|
||||
# homeassistant.components.waze_travel_time
|
||||
pywaze==0.4.0
|
||||
pywaze==0.5.0
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
@ -2294,7 +2294,7 @@ renault-api==0.2.0
|
||||
renson-endura-delta==1.5.0
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.7.9
|
||||
reolink-aio==0.7.10
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@ -2303,7 +2303,7 @@ rfk101py==0.0.1
|
||||
rflink==0.0.65
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell==0.7.2
|
||||
ring-doorbell==0.7.3
|
||||
|
||||
# homeassistant.components.fleetgo
|
||||
ritassist==0.9.2
|
||||
@ -2375,7 +2375,7 @@ sense-energy==0.12.1
|
||||
sense_energy==0.12.1
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.1.0
|
||||
sensirion-ble==0.1.1
|
||||
|
||||
# homeassistant.components.sensorpro
|
||||
sensorpro-ble==0.5.3
|
||||
@ -2612,7 +2612,7 @@ unifi-discovery==1.1.7
|
||||
unifiled==0.11
|
||||
|
||||
# homeassistant.components.zha
|
||||
universal-silabs-flasher==0.0.13
|
||||
universal-silabs-flasher==0.0.14
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.5.4
|
||||
@ -2736,10 +2736,10 @@ yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==2.2.3
|
||||
yalexs-ble==2.3.0
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.8.0
|
||||
yalexs==1.9.0
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.13
|
||||
@ -2748,7 +2748,7 @@ yeelight==0.7.13
|
||||
yeelightsunflower==0.0.10
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.3.0
|
||||
yolink-api==0.3.1
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==1.0.1
|
||||
@ -2799,7 +2799,7 @@ zigpy==0.57.1
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.51.2
|
||||
zwave-js-server-python==0.51.3
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave-me-ws==0.4.3
|
||||
|
@ -190,7 +190,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.6.0
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.0.5
|
||||
aiocomelit==0.0.8
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.4.16
|
||||
@ -430,7 +430,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.12.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.36.3
|
||||
bellows==0.36.4
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected==0.14.0
|
||||
@ -544,7 +544,7 @@ denonavr==0.11.3
|
||||
devolo-home-control-api==0.18.2
|
||||
|
||||
# homeassistant.components.devolo_home_network
|
||||
devolo-plc-api==1.4.0
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@ -648,7 +648,7 @@ freebox-api==1.1.0
|
||||
|
||||
# homeassistant.components.fritz
|
||||
# homeassistant.components.fritzbox_callmonitor
|
||||
fritzconnection[qr]==1.12.2
|
||||
fritzconnection[qr]==1.13.2
|
||||
|
||||
# homeassistant.components.google_translate
|
||||
gTTS==2.2.4
|
||||
@ -780,7 +780,7 @@ holidays==0.28
|
||||
home-assistant-frontend==20230911.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.8.2
|
||||
home-assistant-intents==2023.9.22
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -927,7 +927,7 @@ micloud==0.5
|
||||
mill-local==0.2.0
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.11.2
|
||||
millheater==0.11.5
|
||||
|
||||
# homeassistant.components.minio
|
||||
minio==7.1.12
|
||||
@ -1235,7 +1235,7 @@ pyeconet==0.1.20
|
||||
pyefergy==22.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.11.0
|
||||
pyenphase==1.11.4
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@ -1382,7 +1382,7 @@ pymyq==3.1.4
|
||||
pymysensors==0.24.0
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.9
|
||||
pynetgear==0.10.10
|
||||
|
||||
# homeassistant.components.nobo_hub
|
||||
pynobo==1.6.0
|
||||
@ -1477,7 +1477,7 @@ pyrympro==0.0.7
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2023.8.1
|
||||
pyschlage==2023.9.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.33
|
||||
@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0
|
||||
python-qbittorrent==0.4.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.33.2
|
||||
python-roborock==0.34.1
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.33
|
||||
@ -1639,7 +1639,7 @@ pyvizio==0.1.61
|
||||
pyvolumio==0.1.5
|
||||
|
||||
# homeassistant.components.waze_travel_time
|
||||
pywaze==0.4.0
|
||||
pywaze==0.5.0
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
@ -1684,13 +1684,13 @@ renault-api==0.2.0
|
||||
renson-endura-delta==1.5.0
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.7.9
|
||||
reolink-aio==0.7.10
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.65
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell==0.7.2
|
||||
ring-doorbell==0.7.3
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.18.1
|
||||
@ -1735,7 +1735,7 @@ sense-energy==0.12.1
|
||||
sense_energy==0.12.1
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.1.0
|
||||
sensirion-ble==0.1.1
|
||||
|
||||
# homeassistant.components.sensorpro
|
||||
sensorpro-ble==0.5.3
|
||||
@ -1909,7 +1909,7 @@ ultraheat-api==0.5.7
|
||||
unifi-discovery==1.1.7
|
||||
|
||||
# homeassistant.components.zha
|
||||
universal-silabs-flasher==0.0.13
|
||||
universal-silabs-flasher==0.0.14
|
||||
|
||||
# homeassistant.components.upb
|
||||
upb-lib==0.5.4
|
||||
@ -2015,16 +2015,16 @@ yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==2.2.3
|
||||
yalexs-ble==2.3.0
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.8.0
|
||||
yalexs==1.9.0
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.13
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.3.0
|
||||
yolink-api==0.3.1
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==1.0.1
|
||||
@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4
|
||||
zigpy==0.57.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.51.2
|
||||
zwave-js-server-python==0.51.3
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave-me-ws==0.4.3
|
||||
|
@ -366,15 +366,19 @@ def _sort_manifest_keys(key: str) -> str:
|
||||
return _SORT_KEYS.get(key, key)
|
||||
|
||||
|
||||
def sort_manifest(integration: Integration) -> bool:
|
||||
def sort_manifest(integration: Integration, config: Config) -> bool:
|
||||
"""Sort manifest."""
|
||||
keys = list(integration.manifest.keys())
|
||||
if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys:
|
||||
manifest = {key: integration.manifest[key] for key in keys_sorted}
|
||||
if config.action == "generate":
|
||||
integration.manifest_path.write_text(json.dumps(manifest, indent=2))
|
||||
text = "have been sorted"
|
||||
else:
|
||||
text = "are not sorted correctly"
|
||||
integration.add_error(
|
||||
"manifest",
|
||||
"Manifest keys have been sorted: domain, name, then alphabetical order",
|
||||
f"Manifest keys {text}: domain, name, then alphabetical order",
|
||||
)
|
||||
return True
|
||||
return False
|
||||
@ -387,9 +391,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
for integration in integrations.values():
|
||||
validate_manifest(integration, core_components_dir)
|
||||
if not integration.errors:
|
||||
if sort_manifest(integration):
|
||||
if sort_manifest(integration, config):
|
||||
manifests_resorted.append(integration.manifest_path)
|
||||
if manifests_resorted:
|
||||
if config.action == "generate" and manifests_resorted:
|
||||
subprocess.run(
|
||||
["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"]
|
||||
+ manifests_resorted,
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
import pathlib
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -26,7 +26,7 @@ class Config:
|
||||
|
||||
specific_integrations: list[pathlib.Path] | None
|
||||
root: pathlib.Path
|
||||
action: str
|
||||
action: Literal["validate", "generate"]
|
||||
requirements: bool
|
||||
errors: list[Error] = field(default_factory=list)
|
||||
cache: dict[str, Any] = field(default_factory=dict)
|
||||
|
@ -2,6 +2,7 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.airthings_ble.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.airthings_ble import (
|
||||
@ -31,11 +32,13 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant):
|
||||
assert entry is not None
|
||||
assert device is not None
|
||||
|
||||
new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature"
|
||||
|
||||
entity_registry = hass.helpers.entity_registry.async_get(hass)
|
||||
|
||||
sensor = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=TEMPERATURE_V1.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -57,10 +60,7 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
assert len(hass.states.async_all()) > 0
|
||||
|
||||
assert (
|
||||
entity_registry.async_get(sensor.entity_id).unique_id
|
||||
== WAVE_DEVICE_INFO.address + "_temperature"
|
||||
)
|
||||
assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
@ -77,7 +77,7 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
sensor = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=HUMIDITY_V2.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -99,10 +99,9 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
assert len(hass.states.async_all()) > 0
|
||||
|
||||
assert (
|
||||
entity_registry.async_get(sensor.entity_id).unique_id
|
||||
== WAVE_DEVICE_INFO.address + "_humidity"
|
||||
)
|
||||
# Migration should happen, v2 unique id should be updated to the new format
|
||||
new_unique_id = f"{WAVE_DEVICE_INFO.address}_humidity"
|
||||
assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id
|
||||
|
||||
|
||||
async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
@ -119,7 +118,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
v2 = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=CO2_V2.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -127,7 +126,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
v1 = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=CO2_V1.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -149,11 +148,10 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
|
||||
|
||||
assert len(hass.states.async_all()) > 0
|
||||
|
||||
assert (
|
||||
entity_registry.async_get(v1.entity_id).unique_id
|
||||
== WAVE_DEVICE_INFO.address + "_co2"
|
||||
)
|
||||
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
|
||||
# Migration should happen, v1 unique id should be updated to the new format
|
||||
new_unique_id = f"{WAVE_DEVICE_INFO.address}_co2"
|
||||
assert entity_registry.async_get(v1.entity_id).unique_id == new_unique_id
|
||||
assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id
|
||||
|
||||
|
||||
async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||
@ -170,7 +168,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||
|
||||
v1 = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=VOC_V1.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -178,7 +176,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||
|
||||
v2 = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=VOC_V2.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -186,7 +184,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||
|
||||
v3 = entity_registry.async_get_or_create(
|
||||
domain=DOMAIN,
|
||||
platform="sensor",
|
||||
platform=Platform.SENSOR,
|
||||
unique_id=VOC_V3.unique_id,
|
||||
config_entry=entry,
|
||||
device_id=device.id,
|
||||
@ -208,6 +206,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
|
||||
|
||||
assert len(hass.states.async_all()) > 0
|
||||
|
||||
assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id
|
||||
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
|
||||
assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id
|
||||
# No migration should happen, unique id should be the same as before
|
||||
assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id
|
||||
assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id
|
||||
assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id
|
||||
|
@ -18,9 +18,9 @@ from tests.common import MockConfigEntry
|
||||
async def test_user(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user."""
|
||||
with patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.login",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.login",
|
||||
), patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.logout",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.logout",
|
||||
), patch(
|
||||
"homeassistant.components.comelit.async_setup_entry"
|
||||
) as mock_setup_entry, patch(
|
||||
@ -64,7 +64,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) ->
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.login",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.login",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@ -83,9 +83,9 @@ async def test_reauth_successful(hass: HomeAssistant) -> None:
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.login",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.login",
|
||||
), patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.logout",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.logout",
|
||||
), patch("homeassistant.components.comelit.async_setup_entry"), patch(
|
||||
"requests.get"
|
||||
) as mock_request_get:
|
||||
@ -127,9 +127,9 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) ->
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect
|
||||
), patch(
|
||||
"aiocomelit.api.ComeliteSerialBridgeAPi.logout",
|
||||
"aiocomelit.api.ComeliteSerialBridgeApi.logout",
|
||||
), patch(
|
||||
"homeassistant.components.comelit.async_setup_entry"
|
||||
):
|
||||
|
@ -198,6 +198,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
|
||||
from .test_common import (
|
||||
help_custom_config,
|
||||
help_test_availability_when_connection_lost,
|
||||
help_test_availability_without_topic,
|
||||
help_test_custom_availability_payload,
|
||||
@ -441,6 +442,176 @@ async def test_controlling_state_via_topic(
|
||||
assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
light.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"state_topic": "test-topic",
|
||||
"optimistic": True,
|
||||
"brightness_command_topic": "test_light_rgb/brightness/set",
|
||||
"color_mode_state_topic": "color-mode-state-topic",
|
||||
"rgb_command_topic": "test_light_rgb/rgb/set",
|
||||
"rgb_state_topic": "rgb-state-topic",
|
||||
"rgbw_command_topic": "test_light_rgb/rgbw/set",
|
||||
"rgbw_state_topic": "rgbw-state-topic",
|
||||
"rgbww_command_topic": "test_light_rgb/rgbww/set",
|
||||
"rgbww_state_topic": "rgbww-state-topic",
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_received_rgbx_values_set_state_optimistic(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
) -> None:
|
||||
"""Test the state is set correctly when an rgbx update is received."""
|
||||
await mqtt_mock_entry()
|
||||
state = hass.states.get("light.test")
|
||||
assert state and state.state is not None
|
||||
async_fire_mqtt_message(hass, "test-topic", "ON")
|
||||
## Test rgb processing
|
||||
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
assert state.attributes["rgb_color"] == (255, 255, 255)
|
||||
|
||||
# Only update color mode
|
||||
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgbww")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
|
||||
# Resending same rgb value should restore color mode
|
||||
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
assert state.attributes["rgb_color"] == (255, 255, 255)
|
||||
|
||||
# Only update brightness
|
||||
await common.async_turn_on(hass, "light.test", brightness=128)
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 128
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
assert state.attributes["rgb_color"] == (255, 255, 255)
|
||||
|
||||
# Resending same rgb value should restore brightness
|
||||
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
assert state.attributes["rgb_color"] == (255, 255, 255)
|
||||
|
||||
# Only change rgb value
|
||||
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,0")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
assert state.attributes["rgb_color"] == (255, 255, 0)
|
||||
|
||||
## Test rgbw processing
|
||||
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbw"
|
||||
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
|
||||
|
||||
# Only update color mode
|
||||
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
|
||||
# Resending same rgbw value should restore color mode
|
||||
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbw"
|
||||
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
|
||||
|
||||
# Only update brightness
|
||||
await common.async_turn_on(hass, "light.test", brightness=128)
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 128
|
||||
assert state.attributes["color_mode"] == "rgbw"
|
||||
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
|
||||
|
||||
# Resending same rgbw value should restore brightness
|
||||
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbw"
|
||||
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
|
||||
|
||||
# Only change rgbw value
|
||||
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,128,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbw"
|
||||
assert state.attributes["rgbw_color"] == (255, 255, 128, 255)
|
||||
|
||||
## Test rgbww processing
|
||||
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
|
||||
|
||||
# Only update color mode
|
||||
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgb"
|
||||
|
||||
# Resending same rgbw value should restore color mode
|
||||
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
|
||||
|
||||
# Only update brightness
|
||||
await common.async_turn_on(hass, "light.test", brightness=128)
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 128
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
|
||||
|
||||
# Resending same rgbww value should restore brightness
|
||||
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
|
||||
|
||||
# Only change rgbww value
|
||||
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,128,32,255")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.test")
|
||||
assert state.attributes["brightness"] == 255
|
||||
assert state.attributes["color_mode"] == "rgbww"
|
||||
assert state.attributes["rgbww_color"] == (255, 255, 128, 32, 255)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
|
@ -76,41 +76,6 @@ def mock_controller_service():
|
||||
yield service_mock
|
||||
|
||||
|
||||
@pytest.fixture(name="service_5555")
|
||||
def mock_controller_service_5555():
|
||||
"""Mock a successful service."""
|
||||
with patch(
|
||||
"homeassistant.components.netgear.async_setup_entry", return_value=True
|
||||
), patch("homeassistant.components.netgear.router.Netgear") as service_mock:
|
||||
service_mock.return_value.get_info = Mock(return_value=ROUTER_INFOS)
|
||||
service_mock.return_value.port = 5555
|
||||
service_mock.return_value.ssl = True
|
||||
yield service_mock
|
||||
|
||||
|
||||
@pytest.fixture(name="service_incomplete")
|
||||
def mock_controller_service_incomplete():
|
||||
"""Mock a successful service."""
|
||||
router_infos = ROUTER_INFOS.copy()
|
||||
router_infos.pop("DeviceName")
|
||||
with patch(
|
||||
"homeassistant.components.netgear.async_setup_entry", return_value=True
|
||||
), patch("homeassistant.components.netgear.router.Netgear") as service_mock:
|
||||
service_mock.return_value.get_info = Mock(return_value=router_infos)
|
||||
service_mock.return_value.port = 80
|
||||
service_mock.return_value.ssl = False
|
||||
yield service_mock
|
||||
|
||||
|
||||
@pytest.fixture(name="service_failed")
|
||||
def mock_controller_service_failed():
|
||||
"""Mock a failed service."""
|
||||
with patch("homeassistant.components.netgear.router.Netgear") as service_mock:
|
||||
service_mock.return_value.login_try_port = Mock(return_value=None)
|
||||
service_mock.return_value.get_info = Mock(return_value=None)
|
||||
yield service_mock
|
||||
|
||||
|
||||
async def test_user(hass: HomeAssistant, service) -> None:
|
||||
"""Test user step."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -138,7 +103,7 @@ async def test_user(hass: HomeAssistant, service) -> None:
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
|
||||
|
||||
async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
|
||||
async def test_user_connect_error(hass: HomeAssistant, service) -> None:
|
||||
"""Test user step with connection failure."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@ -146,7 +111,23 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
service.return_value.get_info = Mock(return_value=None)
|
||||
|
||||
# Have to provide all config
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "info"}
|
||||
|
||||
service.return_value.login_try_port = Mock(return_value=None)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@ -160,7 +141,7 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
|
||||
assert result["errors"] == {"base": "config"}
|
||||
|
||||
|
||||
async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> None:
|
||||
async def test_user_incomplete_info(hass: HomeAssistant, service) -> None:
|
||||
"""Test user step with incomplete device info."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@ -168,6 +149,10 @@ async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) ->
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
router_infos = ROUTER_INFOS.copy()
|
||||
router_infos.pop("DeviceName")
|
||||
service.return_value.get_info = Mock(return_value=router_infos)
|
||||
|
||||
# Have to provide all config
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@ -313,7 +298,7 @@ async def test_ssdp(hass: HomeAssistant, service) -> None:
|
||||
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||
|
||||
|
||||
async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None:
|
||||
async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None:
|
||||
"""Test ssdp step with port 5555."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@ -332,6 +317,9 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None:
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
service.return_value.port = 5555
|
||||
service.return_value.ssl = True
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: PASSWORD}
|
||||
)
|
||||
|
@ -35,6 +35,7 @@ SERIAL_NUMBER = 0x12635436566
|
||||
|
||||
# Get serial number Command 0x85. Serial is 0x12635436566
|
||||
SERIAL_RESPONSE = "850000012635436566"
|
||||
ZERO_SERIAL_RESPONSE = "850000000000000000"
|
||||
# Model and version command 0x82
|
||||
MODEL_AND_VERSION_RESPONSE = "820006090C"
|
||||
# Get available stations command 0x83
|
||||
@ -84,6 +85,12 @@ def yaml_config() -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def unique_id() -> str:
|
||||
"""Fixture for serial number used in the config entry."""
|
||||
return SERIAL_NUMBER
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry_data() -> dict[str, Any]:
|
||||
"""Fixture for MockConfigEntry data."""
|
||||
@ -92,13 +99,14 @@ async def config_entry_data() -> dict[str, Any]:
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry(
|
||||
config_entry_data: dict[str, Any] | None
|
||||
config_entry_data: dict[str, Any] | None,
|
||||
unique_id: str,
|
||||
) -> MockConfigEntry | None:
|
||||
"""Fixture for MockConfigEntry."""
|
||||
if config_entry_data is None:
|
||||
return None
|
||||
return MockConfigEntry(
|
||||
unique_id=SERIAL_NUMBER,
|
||||
unique_id=unique_id,
|
||||
domain=DOMAIN,
|
||||
data=config_entry_data,
|
||||
options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES},
|
||||
|
@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@ -19,8 +20,11 @@ from .conftest import (
|
||||
CONFIG_ENTRY_DATA,
|
||||
HOST,
|
||||
PASSWORD,
|
||||
SERIAL_NUMBER,
|
||||
SERIAL_RESPONSE,
|
||||
URL,
|
||||
ZERO_SERIAL_RESPONSE,
|
||||
ComponentSetup,
|
||||
mock_response,
|
||||
)
|
||||
|
||||
@ -66,19 +70,132 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult:
|
||||
)
|
||||
|
||||
|
||||
async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("responses", "expected_config_entry", "expected_unique_id"),
|
||||
[
|
||||
(
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
CONFIG_ENTRY_DATA,
|
||||
SERIAL_NUMBER,
|
||||
),
|
||||
(
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_controller_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup: Mock,
|
||||
expected_config_entry: dict[str, str],
|
||||
expected_unique_id: int | None,
|
||||
) -> None:
|
||||
"""Test the controller is setup correctly."""
|
||||
|
||||
result = await complete_flow(hass)
|
||||
assert result.get("type") == "create_entry"
|
||||
assert result.get("title") == HOST
|
||||
assert "result" in result
|
||||
assert result["result"].data == CONFIG_ENTRY_DATA
|
||||
assert dict(result["result"].data) == expected_config_entry
|
||||
assert result["result"].options == {ATTR_DURATION: 6}
|
||||
assert result["result"].unique_id == expected_unique_id
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"unique_id",
|
||||
"config_entry_data",
|
||||
"config_flow_responses",
|
||||
"expected_config_entry",
|
||||
),
|
||||
[
|
||||
(
|
||||
"other-serial-number",
|
||||
{**CONFIG_ENTRY_DATA, "host": "other-host"},
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
),
|
||||
],
|
||||
ids=["with-serial", "zero-serial"],
|
||||
)
|
||||
async def test_multiple_config_entries(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
config_flow_responses: list[AiohttpClientMockResponse],
|
||||
expected_config_entry: dict[str, Any] | None,
|
||||
) -> None:
|
||||
"""Test setting up multiple config entries that refer to different devices."""
|
||||
assert await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ConfigEntryState.LOADED
|
||||
|
||||
responses.clear()
|
||||
responses.extend(config_flow_responses)
|
||||
|
||||
result = await complete_flow(hass)
|
||||
assert result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
assert dict(result.get("result").data) == expected_config_entry
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"unique_id",
|
||||
"config_entry_data",
|
||||
"config_flow_responses",
|
||||
),
|
||||
[
|
||||
(
|
||||
SERIAL_NUMBER,
|
||||
CONFIG_ENTRY_DATA,
|
||||
[mock_response(SERIAL_RESPONSE)],
|
||||
),
|
||||
(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
[mock_response(ZERO_SERIAL_RESPONSE)],
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"duplicate-serial-number",
|
||||
"duplicate-host-port-no-serial",
|
||||
],
|
||||
)
|
||||
async def test_duplicate_config_entries(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
config_flow_responses: list[AiohttpClientMockResponse],
|
||||
) -> None:
|
||||
"""Test that a device can not be registered twice."""
|
||||
assert await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state == ConfigEntryState.LOADED
|
||||
|
||||
responses.clear()
|
||||
responses.extend(config_flow_responses)
|
||||
|
||||
result = await complete_flow(hass)
|
||||
assert result.get("type") == FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_controller_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_setup: Mock,
|
||||
|
@ -225,11 +225,13 @@
|
||||
'area': 20965000,
|
||||
'avoidCount': 19,
|
||||
'begin': 1672543330,
|
||||
'beginDatetime': '2023-01-01T03:22:10+00:00',
|
||||
'cleanType': 3,
|
||||
'complete': 1,
|
||||
'duration': 1176,
|
||||
'dustCollectionStatus': 1,
|
||||
'end': 1672544638,
|
||||
'endDatetime': '2023-01-01T03:43:58+00:00',
|
||||
'error': 0,
|
||||
'finishReason': 56,
|
||||
'mapFlag': 0,
|
||||
|
@ -44,6 +44,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockEntityPlatform,
|
||||
MockModule,
|
||||
MockPlatform,
|
||||
async_mock_restore_state_shutdown_restart,
|
||||
@ -2177,27 +2178,24 @@ async def test_unit_conversion_update(
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
platform = getattr(hass.components, "test.sensor")
|
||||
platform.init(empty=True)
|
||||
|
||||
platform.ENTITIES["0"] = platform.MockSensor(
|
||||
entity0 = platform.MockSensor(
|
||||
name="Test 0",
|
||||
device_class=device_class,
|
||||
native_unit_of_measurement=native_unit,
|
||||
native_value=str(native_value),
|
||||
unique_id="very_unique",
|
||||
)
|
||||
entity0 = platform.ENTITIES["0"]
|
||||
|
||||
platform.ENTITIES["1"] = platform.MockSensor(
|
||||
entity1 = platform.MockSensor(
|
||||
name="Test 1",
|
||||
device_class=device_class,
|
||||
native_unit_of_measurement=native_unit,
|
||||
native_value=str(native_value),
|
||||
unique_id="very_unique_1",
|
||||
)
|
||||
entity1 = platform.ENTITIES["1"]
|
||||
|
||||
platform.ENTITIES["2"] = platform.MockSensor(
|
||||
entity2 = platform.MockSensor(
|
||||
name="Test 2",
|
||||
device_class=device_class,
|
||||
native_unit_of_measurement=native_unit,
|
||||
@ -2205,9 +2203,8 @@ async def test_unit_conversion_update(
|
||||
suggested_unit_of_measurement=suggested_unit,
|
||||
unique_id="very_unique_2",
|
||||
)
|
||||
entity2 = platform.ENTITIES["2"]
|
||||
|
||||
platform.ENTITIES["3"] = platform.MockSensor(
|
||||
entity3 = platform.MockSensor(
|
||||
name="Test 3",
|
||||
device_class=device_class,
|
||||
native_unit_of_measurement=native_unit,
|
||||
@ -2215,9 +2212,33 @@ async def test_unit_conversion_update(
|
||||
suggested_unit_of_measurement=suggested_unit,
|
||||
unique_id="very_unique_3",
|
||||
)
|
||||
entity3 = platform.ENTITIES["3"]
|
||||
|
||||
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||
entity4 = platform.MockSensor(
|
||||
name="Test 4",
|
||||
device_class=device_class,
|
||||
native_unit_of_measurement=native_unit,
|
||||
native_value=str(native_value),
|
||||
unique_id="very_unique_4",
|
||||
)
|
||||
|
||||
entity_platform = MockEntityPlatform(
|
||||
hass, domain="sensor", platform_name="test", platform=None
|
||||
)
|
||||
await entity_platform.async_add_entities((entity0, entity1, entity2, entity3))
|
||||
|
||||
# Pre-register entity4
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"sensor", "test", entity4.unique_id, unit_of_measurement=automatic_unit_1
|
||||
)
|
||||
entity4_entity_id = entry.entity_id
|
||||
entity_registry.async_update_entity_options(
|
||||
entity4_entity_id,
|
||||
"sensor.private",
|
||||
{
|
||||
"suggested_unit_of_measurement": automatic_unit_1,
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Registered entity -> Follow automatic unit conversion
|
||||
@ -2320,6 +2341,25 @@ async def test_unit_conversion_update(
|
||||
assert state.state == suggested_state
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
|
||||
|
||||
# Entity 4 still has a pending request to refresh entity options
|
||||
entry = entity_registry.async_get(entity4_entity_id)
|
||||
assert entry.options == {
|
||||
"sensor.private": {
|
||||
"refresh_initial_entity_options": True,
|
||||
"suggested_unit_of_measurement": automatic_unit_1,
|
||||
}
|
||||
}
|
||||
|
||||
# Add entity 4, the pending request to refresh entity options should be handled
|
||||
await entity_platform.async_add_entities((entity4,))
|
||||
|
||||
state = hass.states.get(entity4_entity_id)
|
||||
assert state.state == automatic_state_2
|
||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2
|
||||
|
||||
entry = entity_registry.async_get(entity4_entity_id)
|
||||
assert entry.options == {}
|
||||
|
||||
|
||||
class MockFlow(ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
@ -1460,6 +1460,39 @@ def test_calculate_adjustment_invalid_new_state(
|
||||
assert "Invalid state unknown" in caplog.text
|
||||
|
||||
|
||||
async def test_unit_of_measurement_missing_invalid_new_state(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that a suggestion is created when new_state is missing unit_of_measurement."""
|
||||
yaml_config = {
|
||||
"utility_meter": {
|
||||
"energy_bill": {
|
||||
"source": "sensor.energy",
|
||||
}
|
||||
}
|
||||
}
|
||||
source_entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, yaml_config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(source_entity_id, 4, {ATTR_UNIT_OF_MEASUREMENT: None})
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.energy_bill")
|
||||
assert state is not None
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
|
||||
assert (
|
||||
f"Source sensor {source_entity_id} has no unit of measurement." in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_device_id(hass: HomeAssistant) -> None:
|
||||
"""Test for source entity device for Utility Meter."""
|
||||
device_registry = dr.async_get(hass)
|
||||
|
Loading…
x
Reference in New Issue
Block a user