This commit is contained in:
Franck Nijhof 2023-09-24 18:58:43 +02:00 committed by GitHub
commit 0cbd46592a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 737 additions and 236 deletions

View File

@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.08.0 uses: home-assistant/builder@2023.09.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -205,8 +205,6 @@ jobs:
--cosign \ --cosign \
--target /data \ --target /data \
--generic ${{ needs.init.outputs.version }} --generic ${{ needs.init.outputs.version }}
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
- name: Archive translations - name: Archive translations
shell: bash shell: bash
@ -275,15 +273,13 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.08.0 uses: home-assistant/builder@2023.09.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
--target /data/machine \ --target /data/machine \
--cosign \ --cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
publish_ha: publish_ha:
name: Publish version files name: Publish version files

View File

@ -97,7 +97,7 @@ jobs:
name: requirements_diff name: requirements_diff
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2023.04.0 uses: home-assistant/wheels@2023.09.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -178,7 +178,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (part 1) - name: Build wheels (part 1)
uses: home-assistant/wheels@2023.04.0 uses: home-assistant/wheels@2023.09.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -192,7 +192,7 @@ jobs:
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
- name: Build wheels (part 2) - name: Build wheels (part 2)
uses: home-assistant/wheels@2023.04.0 uses: home-assistant/wheels@2023.09.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -206,7 +206,7 @@ jobs:
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
- name: Build wheels (part 3) - name: Build wheels (part 3)
uses: home-assistant/wheels@2023.04.0 uses: home-assistant/wheels@2023.09.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -58,6 +58,16 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi
"""Describes Airnow sensor entity.""" """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, ...] = ( SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription( AirNowEntityDescription(
key=ATTR_API_AQI, key=ATTR_API_AQI,
@ -93,10 +103,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
translation_key="station", translation_key="station",
icon="mdi:blur", icon="mdi:blur",
value_fn=lambda data: data.get(ATTR_API_STATION), value_fn=lambda data: data.get(ATTR_API_STATION),
extra_state_attributes_fn=lambda data: { extra_state_attributes_fn=station_extra_attrs,
"lat": data[ATTR_API_STATION_LATITUDE],
"long": data[ATTR_API_STATION_LONGITUDE],
},
), ),
) )

View File

@ -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 not matching_reg_entry or "(" not in entry.unique_id
): ):
matching_reg_entry = entry 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 return
entity_id = matching_reg_entry.entity_id entity_id = matching_reg_entry.entity_id
ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id) ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)

View File

@ -298,6 +298,26 @@ class Pipeline:
id: str = field(default_factory=ulid_util.ulid) 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]: def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage.""" """Return a JSON serializable representation for storage."""
return { return {
@ -1205,7 +1225,7 @@ class PipelineStorageCollection(
def _deserialize_item(self, data: dict) -> Pipeline: def _deserialize_item(self, data: dict) -> Pipeline:
"""Create an item from its serialized representation.""" """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: def _serialize_item(self, item_id: str, item: Pipeline) -> dict:
"""Return the serialized representation of an item for storing.""" """Return the serialized representation of an item for storing."""

View File

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

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any from typing import Any
from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions from aiocomelit import ComeliteSerialBridgeApi, exceptions as aiocomelit_exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant import core, exceptions from homeassistant import core, exceptions
@ -37,7 +37,7 @@ async def validate_input(
) -> dict[str, str]: ) -> dict[str, str]:
"""Validate the user input allows us to connect.""" """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: try:
await api.login() await api.login()

View File

@ -3,11 +3,14 @@ import asyncio
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from aiocomelit import ComeliteSerialBridgeAPi from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject
from aiocomelit.const import BRIDGE
import aiohttp import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, DOMAIN from .const import _LOGGER, DOMAIN
@ -16,13 +19,15 @@ from .const import _LOGGER, DOMAIN
class ComelitSerialBridge(DataUpdateCoordinator): class ComelitSerialBridge(DataUpdateCoordinator):
"""Queries Comelit Serial Bridge.""" """Queries Comelit Serial Bridge."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None: def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""
self._host = host self._host = host
self._pin = pin self._pin = pin
self.api = ComeliteSerialBridgeAPi(host, pin) self.api = ComeliteSerialBridgeApi(host, pin)
super().__init__( super().__init__(
hass=hass, hass=hass,
@ -30,6 +35,38 @@ class ComelitSerialBridge(DataUpdateCoordinator):
name=f"{DOMAIN}-{host}-coordinator", name=f"{DOMAIN}-{host}-coordinator",
update_interval=timedelta(seconds=5), 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]: async def _async_update_data(self) -> dict[str, Any]:
"""Update router data.""" """Update router data."""

View File

@ -9,7 +9,6 @@ from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON
from homeassistant.components.light import LightEntity from homeassistant.components.light import LightEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -37,27 +36,20 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
"""Light device.""" """Light device."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None
def __init__( def __init__(
self, self,
coordinator: ComelitSerialBridge, coordinator: ComelitSerialBridge,
device: ComelitSerialBridgeObject, device: ComelitSerialBridgeObject,
config_entry_unique_id: str | None, config_entry_unique_id: str,
) -> None: ) -> None:
"""Init light entity.""" """Init light entity."""
self._api = coordinator.api self._api = coordinator.api
self._device = device self._device = device
super().__init__(coordinator) super().__init__(coordinator)
self._attr_name = device.name
self._attr_unique_id = f"{config_entry_unique_id}-{device.index}" self._attr_unique_id = f"{config_entry_unique_id}-{device.index}"
self._attr_device_info = DeviceInfo( self._attr_device_info = self.coordinator.platform_device_info(device, LIGHT)
identifiers={
(DOMAIN, self._attr_unique_id),
},
manufacturer="Comelit",
model="Serial Bridge",
name=device.name,
)
async def _light_set_state(self, state: int) -> None: async def _light_set_state(self, state: int) -> None:
"""Set desired light state.""" """Set desired light state."""

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/comelit", "documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiocomelit"], "loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.0.5"] "requirements": ["aiocomelit==0.0.8"]
} }

View File

@ -3,7 +3,7 @@
"flow_title": "{host}", "flow_title": "{host}",
"step": { "step": {
"reauth_confirm": { "reauth_confirm": {
"description": "Please enter the correct PIN for VEDO system: {host}", "description": "Please enter the correct PIN for {host}",
"data": { "data": {
"pin": "[%key:common::config_flow::data::pin%]" "pin": "[%key:common::config_flow::data::pin%]"
} }

View File

@ -7,5 +7,5 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "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"]
} }

View File

@ -74,7 +74,7 @@ async def async_setup_entry( # noqa: C901
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
assert device.device assert device.device
try: try:
async with asyncio.timeout(10): async with asyncio.timeout(30):
return await device.device.async_check_firmware_available() return await device.device.async_check_firmware_available()
except DeviceUnavailable as err: except DeviceUnavailable as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err

View File

@ -8,7 +8,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["devolo_plc_api"], "loggers": ["devolo_plc_api"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["devolo-plc-api==1.4.0"], "requirements": ["devolo-plc-api==1.4.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_dvl-deviceapi._tcp.local.", "type": "_dvl-deviceapi._tcp.local.",

View File

@ -326,6 +326,7 @@ class Thermostat(ClimateEntity):
self._attr_unique_id = self.thermostat["identifier"] self._attr_unique_id = self.thermostat["identifier"]
self.vacation = None self.vacation = None
self._last_active_hvac_mode = HVACMode.HEAT_COOL self._last_active_hvac_mode = HVACMode.HEAT_COOL
self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL
self._attr_hvac_modes = [] self._attr_hvac_modes = []
if self.settings["heatStages"] or self.settings["hasHeatPump"]: if self.settings["heatStages"] or self.settings["hasHeatPump"]:
@ -541,13 +542,14 @@ class Thermostat(ClimateEntity):
def turn_aux_heat_on(self) -> None: def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
_LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat") _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.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY)
self.update_without_throttle = True self.update_without_throttle = True
def turn_aux_heat_off(self) -> None: def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
_LOGGER.debug("Setting HVAC mode to last mode to disable aux heat") _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 self.update_without_throttle = True
def set_preset_mode(self, preset_mode: str) -> None: def set_preset_mode(self, preset_mode: str) -> None:

View File

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

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/fritz", "documentation": "https://www.home-assistant.io/integrations/fritz",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["fritzconnection"], "loggers": ["fritzconnection"],
"requirements": ["fritzconnection[qr]==1.12.2", "xmltodict==0.13.0"], "requirements": ["fritzconnection[qr]==1.13.2", "xmltodict==0.13.0"],
"ssdp": [ "ssdp": [
{ {
"st": "urn:schemas-upnp-org:device:fritzbox:1" "st": "urn:schemas-upnp-org:device:fritzbox:1"

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["fritzconnection"], "loggers": ["fritzconnection"],
"requirements": ["fritzconnection[qr]==1.12.2"] "requirements": ["fritzconnection[qr]==1.13.2"]
} }

View File

@ -189,7 +189,11 @@ class FritzBoxCallMonitor:
_LOGGER.debug("Setting up socket connection") _LOGGER.debug("Setting up socket connection")
try: try:
self.connection = FritzMonitor(address=self.host, port=self.port) 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() Thread(target=self._process_events, kwargs=kwargs).start()
except OSError as err: except OSError as err:
self.connection = None self.connection = None

View File

@ -103,10 +103,7 @@ class IPMAWeather(WeatherEntity, IPMADevice):
else: else:
self._daily_forecast = None self._daily_forecast = None
if self._period == 1 or self._forecast_listeners["hourly"]:
await self._update_forecast("hourly", 1, True) await self._update_forecast("hourly", 1, True)
else:
self._hourly_forecast = None
_LOGGER.debug( _LOGGER.debug(
"Updated location %s based on %s, current observation %s", "Updated location %s based on %s, current observation %s",
@ -139,8 +136,8 @@ class IPMAWeather(WeatherEntity, IPMADevice):
@property @property
def condition(self): def condition(self):
"""Return the current condition.""" """Return the current condition which is only available on the hourly forecast data."""
forecast = self._hourly_forecast or self._daily_forecast forecast = self._hourly_forecast
if not forecast: if not forecast:
return return

View File

@ -196,9 +196,9 @@ async def async_setup_entry(
data = hass.data[DOMAIN][entry.entry_id] data = hass.data[DOMAIN][entry.entry_id]
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST] coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN] coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data[ coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
COORDINATOR_ALERT COORDINATOR_ALERT
] )
entities: list[MeteoFranceSensor[Any]] = [ entities: list[MeteoFranceSensor[Any]] = [
MeteoFranceSensor(coordinator_forecast, description) MeteoFranceSensor(coordinator_forecast, description)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill", "documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["mill", "mill_local"], "loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.11.2", "mill-local==0.2.0"] "requirements": ["millheater==0.11.5", "mill-local==0.2.0"]
} }

View File

@ -462,6 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received)
@callback
def _rgbx_received( def _rgbx_received(
msg: ReceiveMessage, msg: ReceiveMessage,
template: str, template: str,
@ -532,11 +533,26 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
@log_messages(self.hass, self.entity_id) @log_messages(self.hass, self.entity_id)
def rgbww_received(msg: ReceiveMessage) -> None: def rgbww_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages for RGBWW.""" """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( rgbww = _rgbx_received(
msg, msg,
CONF_RGBWW_VALUE_TEMPLATE, CONF_RGBWW_VALUE_TEMPLATE,
ColorMode.RGBWW, ColorMode.RGBWW,
color_util.color_rgbww_to_rgb, _converter,
) )
if rgbww is None: if rgbww is None:
return return

View File

@ -190,8 +190,6 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
except CannotLoginException: except CannotLoginException:
errors["base"] = "config" errors["base"] = "config"
if errors:
return await self._show_setup_form(user_input, errors) return await self._show_setup_form(user_input, errors)
config_data = { config_data = {
@ -204,6 +202,10 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# Check if already configured # Check if already configured
info = await self.hass.async_add_executor_job(api.get_info) 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) await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False)
self._abort_if_unique_id_configured(updates=config_data) self._abort_if_unique_id_configured(updates=config_data)

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/netgear", "documentation": "https://www.home-assistant.io/integrations/netgear",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pynetgear"], "loggers": ["pynetgear"],
"requirements": ["pynetgear==0.10.9"], "requirements": ["pynetgear==0.10.10"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "NETGEAR, Inc.", "manufacturer": "NETGEAR, Inc.",

View File

@ -11,7 +11,8 @@
} }
}, },
"error": { "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": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
options: dict[str, Any], options: dict[str, Any],
) -> FlowResult: ) -> FlowResult:
"""Create the config entry.""" """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) await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
else:
self._async_abort_entries_match(data)
return self.async_create_entry( return self.async_create_entry(
title=data[CONF_HOST], title=data[CONF_HOST],
data=data, data=data,

View File

@ -61,7 +61,8 @@ class ReolinkHost:
) )
self.webhook_id: str | None = None 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._base_url: str = ""
self._webhook_url: str = "" self._webhook_url: str = ""
self._webhook_reachable: bool = False self._webhook_reachable: bool = False
@ -97,7 +98,9 @@ class ReolinkHost:
f"'{self._api.user_level}', only admin users can change camera settings" 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_rtsp = None
enable_onvif = None enable_onvif = None
@ -109,7 +112,7 @@ class ReolinkHost:
) )
enable_rtsp = True enable_rtsp = True
if not self._api.onvif_enabled and self._onvif_supported: if not self._api.onvif_enabled and onvif_supported:
_LOGGER.debug( _LOGGER.debug(
"ONVIF is disabled on %s, trying to enable it", self._api.nvr_name "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) self._unique_id = format_mac(self._api.mac_address)
if self._onvif_supported: if self._onvif_push_supported:
try: try:
await self.subscribe() await self.subscribe()
except NotSupportedError: except NotSupportedError:
self._onvif_supported = False self._onvif_push_supported = False
self.unregister_webhook() self.unregister_webhook()
await self._api.unsubscribe() await self._api.unsubscribe()
else: else:
@ -179,12 +182,27 @@ class ReolinkHost:
self._cancel_onvif_check = async_call_later( self._cancel_onvif_check = async_call_later(
self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif
) )
if not self._onvif_supported: if not self._onvif_push_supported:
_LOGGER.debug( _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, 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() 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: if self._api.sw_version_update_required:
ir.async_create_issue( ir.async_create_issue(
@ -317,11 +335,22 @@ class ReolinkHost:
str(err), str(err),
) )
async def _async_start_long_polling(self): async def _async_start_long_polling(self, initial=False):
"""Start ONVIF long polling task.""" """Start ONVIF long polling task."""
if self._long_poll_task is None: if self._long_poll_task is None:
try: try:
await self._api.subscribe(sub_type=SubType.long_poll) 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: except ReolinkError as err:
# make sure the long_poll_task is always created to try again later # make sure the long_poll_task is always created to try again later
if not self._lost_subscription: if not self._lost_subscription:
@ -381,12 +410,11 @@ class ReolinkHost:
async def renew(self) -> None: async def renew(self) -> None:
"""Renew the subscription of motion events (lease time is 15 minutes).""" """Renew the subscription of motion events (lease time is 15 minutes)."""
if not self._onvif_supported:
return
try: try:
if self._onvif_push_supported:
await self._renew(SubType.push) 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): if not self._api.subscribed(SubType.long_poll):
_LOGGER.debug("restarting long polling task") _LOGGER.debug("restarting long polling task")
# To prevent 5 minute request timeout # To prevent 5 minute request timeout

View File

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

View File

@ -223,6 +223,7 @@
"state": { "state": {
"off": "[%key:common::state::off%]", "off": "[%key:common::state::off%]",
"auto": "Auto", "auto": "Auto",
"onatnight": "On at night",
"schedule": "Schedule", "schedule": "Schedule",
"adaptive": "Adaptive", "adaptive": "Adaptive",
"autoadaptive": "Auto adaptive" "autoadaptive": "Auto adaptive"

View File

@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/ring", "documentation": "https://www.home-assistant.io/integrations/ring",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["ring_doorbell"], "loggers": ["ring_doorbell"],
"requirements": ["ring-doorbell==0.7.2"] "requirements": ["ring-doorbell==0.7.3"]
} }

View File

@ -40,7 +40,7 @@ class RoborockEntity(Entity):
async def send( async def send(
self, self,
command: RoborockCommand, command: RoborockCommand | str,
params: dict[str, Any] | list[Any] | int | None = None, params: dict[str, Any] | list[Any] | int | None = None,
) -> dict: ) -> dict:
"""Send a command to a vacuum cleaner.""" """Send a command to a vacuum cleaner."""
@ -48,7 +48,7 @@ class RoborockEntity(Entity):
response = await self._api.send_command(command, params) response = await self._api.send_command(command, params)
except RoborockException as err: except RoborockException as err:
raise HomeAssistantError( 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 ) from err
return response return response

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock", "documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": ["python-roborock==0.33.2"] "requirements": ["python-roborock==0.34.1"]
} }

View File

@ -164,10 +164,10 @@
"dnd_end_time": { "dnd_end_time": {
"name": "Do not disturb end" "name": "Do not disturb end"
}, },
"off_peak_start_time": { "off_peak_start": {
"name": "Off-peak start" "name": "Off-peak start"
}, },
"off_peak_end_time": { "off_peak_end": {
"name": "Off-peak end" "name": "Off-peak end"
} }
}, },

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/schlage", "documentation": "https://www.home-assistant.io/integrations/schlage",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["pyschlage==2023.8.1"] "requirements": ["pyschlage==2023.9.1"]
} }

View File

@ -54,7 +54,15 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode"
ATTR_LIGHT = "light" ATTR_LIGHT = "light"
BOOST_INCLUSIVE = "boost_inclusive" 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 = { AVAILABLE_SWING_MODES = {
"stopped", "stopped",
"fixedtop", "fixedtop",

View File

@ -127,6 +127,7 @@
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "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": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]",
"medium_high": "Medium high", "medium_high": "Medium high",
"strong": "Strong",
"quiet": "Quiet" "quiet": "Quiet"
} }
}, },
@ -211,6 +212,7 @@
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "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": "[%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%]", "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%]" "quiet": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::quiet%]"
} }
}, },
@ -347,6 +349,7 @@
"fan_mode": { "fan_mode": {
"state": { "state": {
"quiet": "Quiet", "quiet": "Quiet",
"strong": "Strong",
"low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "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": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]",
"medium_high": "Medium high", "medium_high": "Medium high",

View File

@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/sensirion_ble", "documentation": "https://www.home-assistant.io/integrations/sensirion_ble",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["sensirion-ble==0.1.0"] "requirements": ["sensirion-ble==0.1.1"]
} }

View File

@ -345,7 +345,7 @@ class SensorEntity(Entity):
"""Return initial entity options. """Return initial entity options.
These will be stored in the entity registry the first time the entity is seen, 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() suggested_unit_of_measurement = self._get_initial_suggested_unit()
@ -783,7 +783,7 @@ class SensorEntity(Entity):
registry = er.async_get(self.hass) registry = er.async_get(self.hass)
initial_options = self.get_initial_entity_options() or {} initial_options = self.get_initial_entity_options() or {}
registry.async_update_entity_options( registry.async_update_entity_options(
self.entity_id, self.registry_entry.entity_id,
f"{DOMAIN}.private", f"{DOMAIN}.private",
initial_options.get(f"{DOMAIN}.private"), initial_options.get(f"{DOMAIN}.private"),
) )

View File

@ -44,7 +44,12 @@ from homeassistant.util.unit_conversion import (
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf 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 = { CONDITION_CLASSES = {
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
@ -434,7 +439,8 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS) diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS)
if diff_result: if diff_result:
raise vol.Invalid( 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: if forecast_type == "twice_daily" and "is_daytime" not in forecast:
raise vol.Invalid( raise vol.Invalid(

View File

@ -36,3 +36,5 @@ change:
example: "00:01:00, 60 or -60" example: "00:01:00, 60 or -60"
selector: selector:
text: text:
reload:

View File

@ -62,6 +62,10 @@
"description": "Duration to add or subtract to the running timer." "description": "Duration to add or subtract to the running timer."
} }
} }
},
"reload": {
"name": "[%key:common::action::reload%]",
"description": "Reloads timers from the YAML-configuration."
} }
} }
} }

View File

@ -221,7 +221,6 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await self.async_refresh() await self.async_refresh()
self.update_interval = async_set_update_interval(self.hass, self._api) self.update_interval = async_set_update_interval(self.hass, self._api)
self._next_refresh = None
self._async_unsub_refresh() self._async_unsub_refresh()
if self._listeners: if self._listeners:
self._schedule_refresh() self._schedule_refresh()

View File

@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
SensorExtraStoredData, SensorExtraStoredData,
SensorStateClass, SensorStateClass,
) )
from homeassistant.components.sensor.recorder import _suggest_report_issue
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
@ -484,6 +485,12 @@ class UtilityMeterSensor(RestoreSensor):
DATA_TARIFF_SENSORS DATA_TARIFF_SENSORS
]: ]:
sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) 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 ( if (
adjustment := self.calculate_adjustment(old_state, new_state) 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 # 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._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._last_valid_state = new_state_val
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pywaze", "homeassistant.helpers.location"], "loggers": ["pywaze", "homeassistant.helpers.location"],
"requirements": ["pywaze==0.4.0"] "requirements": ["pywaze==0.5.0"]
} }

View File

@ -169,8 +169,12 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """Press the button."""
method = getattr(self._device, self.entity_description.method_press) 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( await self._try_command(
self.entity_description.method_press_error_message, self.entity_description.method_press_error_message, method, params
method, )
self.entity_description.method_press_params, else:
await self._try_command(
self.entity_description.method_press_error_message, method
) )

View File

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

View File

@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"], "dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink", "documentation": "https://www.home-assistant.io/integrations/yolink",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"requirements": ["yolink-api==0.3.0"] "requirements": ["yolink-api==0.3.1"]
} }

View File

@ -21,7 +21,7 @@
"universal_silabs_flasher" "universal_silabs_flasher"
], ],
"requirements": [ "requirements": [
"bellows==0.36.3", "bellows==0.36.4",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.103", "zha-quirks==0.0.103",
@ -30,7 +30,7 @@
"zigpy-xbee==0.18.2", "zigpy-xbee==0.18.2",
"zigpy-zigate==0.11.0", "zigpy-zigate==0.11.0",
"zigpy-znp==0.11.4", "zigpy-znp==0.11.4",
"universal-silabs-flasher==0.0.13" "universal-silabs-flasher==0.0.14"
], ],
"usb": [ "usb": [
{ {

View File

@ -9,7 +9,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["zwave_js_server"], "loggers": ["zwave_js_server"],
"quality_scale": "platinum", "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": [ "usb": [
{ {
"vid": "0658", "vid": "0658",

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 9 MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -81,7 +81,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
self._shutdown_requested = False self._shutdown_requested = False
self.config_entry = config_entries.current_entry.get() self.config_entry = config_entries.current_entry.get()
self.always_update = always_update self.always_update = always_update
self._next_refresh: float | None = None
# It's None before the first successful update. # It's None before the first successful update.
# Components should call async_config_entry_first_refresh # 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.""" """Unschedule any pending refresh since there is no longer any listeners."""
self._async_unsub_refresh() self._async_unsub_refresh()
self._debounced_refresh.async_cancel() self._debounced_refresh.async_cancel()
self._next_refresh = None
def async_contexts(self) -> Generator[Any, None, None]: def async_contexts(self) -> Generator[Any, None, None]:
"""Return all registered contexts.""" """Return all registered contexts."""
@ -220,13 +218,13 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
# We use event.async_call_at because DataUpdateCoordinator does # We use event.async_call_at because DataUpdateCoordinator does
# not need an exact update interval. # not need an exact update interval.
now = self.hass.loop.time() now = self.hass.loop.time()
if self._next_refresh is None or self._next_refresh <= now:
self._next_refresh = int(now) + self._microsecond next_refresh = int(now) + self._microsecond
self._next_refresh += self.update_interval.total_seconds() next_refresh += self.update_interval.total_seconds()
self._unsub_refresh = event.async_call_at( self._unsub_refresh = event.async_call_at(
self.hass, self.hass,
self._job, self._job,
self._next_refresh, next_refresh,
) )
async def _handle_refresh_interval(self, _now: datetime) -> None: async def _handle_refresh_interval(self, _now: datetime) -> None:
@ -265,7 +263,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
async def async_refresh(self) -> None: async def async_refresh(self) -> None:
"""Refresh data and log errors.""" """Refresh data and log errors."""
self._next_refresh = None
await self._async_refresh(log_failures=True) await self._async_refresh(log_failures=True)
async def _async_refresh( # noqa: C901 async def _async_refresh( # noqa: C901
@ -405,7 +402,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
"""Manually update data, notify listeners and reset refresh interval.""" """Manually update data, notify listeners and reset refresh interval."""
self._async_unsub_refresh() self._async_unsub_refresh()
self._debounced_refresh.async_cancel() self._debounced_refresh.async_cancel()
self._next_refresh = None
self.data = data self.data = data
self.last_update_success = True self.last_update_success = True

View File

@ -23,7 +23,7 @@ hass-nabucasa==0.71.0
hassil==1.2.5 hassil==1.2.5
home-assistant-bluetooth==1.10.3 home-assistant-bluetooth==1.10.3
home-assistant-frontend==20230911.0 home-assistant-frontend==20230911.0
home-assistant-intents==2023.8.2 home-assistant-intents==2023.9.22
httpx==0.24.1 httpx==0.24.1
ifaddr==0.2.0 ifaddr==0.2.0
janus==1.0.0 janus==1.0.0

View File

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

View File

@ -209,7 +209,7 @@ aiobafi6==0.9.0
aiobotocore==2.6.0 aiobotocore==2.6.0
# homeassistant.components.comelit # homeassistant.components.comelit
aiocomelit==0.0.5 aiocomelit==0.0.8
# homeassistant.components.dhcp # homeassistant.components.dhcp
aiodiscover==1.4.16 aiodiscover==1.4.16
@ -509,7 +509,7 @@ beautifulsoup4==4.12.2
# beewi-smartclim==0.0.10 # beewi-smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.36.3 bellows==0.36.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0 bimmer-connected==0.14.0
@ -670,7 +670,7 @@ denonavr==0.11.3
devolo-home-control-api==0.18.2 devolo-home-control-api==0.18.2
# homeassistant.components.devolo_home_network # homeassistant.components.devolo_home_network
devolo-plc-api==1.4.0 devolo-plc-api==1.4.1
# homeassistant.components.directv # homeassistant.components.directv
directv==0.4.0 directv==0.4.0
@ -829,7 +829,7 @@ freesms==0.2.0
# homeassistant.components.fritz # homeassistant.components.fritz
# homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_callmonitor
fritzconnection[qr]==1.12.2 fritzconnection[qr]==1.13.2
# homeassistant.components.google_translate # homeassistant.components.google_translate
gTTS==2.2.4 gTTS==2.2.4
@ -997,7 +997,7 @@ holidays==0.28
home-assistant-frontend==20230911.0 home-assistant-frontend==20230911.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.8.2 home-assistant-intents==2023.9.22
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2
@ -1213,7 +1213,7 @@ micloud==0.5
mill-local==0.2.0 mill-local==0.2.0
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.11.2 millheater==0.11.5
# homeassistant.components.minio # homeassistant.components.minio
minio==7.1.12 minio==7.1.12
@ -1671,7 +1671,7 @@ pyedimax==0.2.1
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.11.0 pyenphase==1.11.4
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.6 pyenvisalink==4.6
@ -1866,7 +1866,7 @@ pymyq==3.1.4
pymysensors==0.24.0 pymysensors==0.24.0
# homeassistant.components.netgear # homeassistant.components.netgear
pynetgear==0.10.9 pynetgear==0.10.10
# homeassistant.components.netio # homeassistant.components.netio
pynetio==0.1.9.1 pynetio==0.1.9.1
@ -1988,7 +1988,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16 pysaj==0.0.16
# homeassistant.components.schlage # homeassistant.components.schlage
pyschlage==2023.8.1 pyschlage==2023.9.1
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.33 pysensibo==1.0.33
@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.33.2 python-roborock==0.34.1
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@ -2231,7 +2231,7 @@ pyvlx==0.2.20
pyvolumio==0.1.5 pyvolumio==0.1.5
# homeassistant.components.waze_travel_time # homeassistant.components.waze_travel_time
pywaze==0.4.0 pywaze==0.5.0
# homeassistant.components.html5 # homeassistant.components.html5
pywebpush==1.9.2 pywebpush==1.9.2
@ -2294,7 +2294,7 @@ renault-api==0.2.0
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.9 reolink-aio==0.7.10
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2303,7 +2303,7 @@ rfk101py==0.0.1
rflink==0.0.65 rflink==0.0.65
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell==0.7.2 ring-doorbell==0.7.3
# homeassistant.components.fleetgo # homeassistant.components.fleetgo
ritassist==0.9.2 ritassist==0.9.2
@ -2375,7 +2375,7 @@ sense-energy==0.12.1
sense_energy==0.12.1 sense_energy==0.12.1
# homeassistant.components.sensirion_ble # homeassistant.components.sensirion_ble
sensirion-ble==0.1.0 sensirion-ble==0.1.1
# homeassistant.components.sensorpro # homeassistant.components.sensorpro
sensorpro-ble==0.5.3 sensorpro-ble==0.5.3
@ -2612,7 +2612,7 @@ unifi-discovery==1.1.7
unifiled==0.11 unifiled==0.11
# homeassistant.components.zha # homeassistant.components.zha
universal-silabs-flasher==0.0.13 universal-silabs-flasher==0.0.14
# homeassistant.components.upb # homeassistant.components.upb
upb-lib==0.5.4 upb-lib==0.5.4
@ -2736,10 +2736,10 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==2.2.3 yalexs-ble==2.3.0
# homeassistant.components.august # homeassistant.components.august
yalexs==1.8.0 yalexs==1.9.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.13 yeelight==0.7.13
@ -2748,7 +2748,7 @@ yeelight==0.7.13
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.0 yolink-api==0.3.1
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1
@ -2799,7 +2799,7 @@ zigpy==0.57.1
zm-py==0.5.2 zm-py==0.5.2
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.51.2 zwave-js-server-python==0.51.3
# homeassistant.components.zwave_me # homeassistant.components.zwave_me
zwave-me-ws==0.4.3 zwave-me-ws==0.4.3

View File

@ -190,7 +190,7 @@ aiobafi6==0.9.0
aiobotocore==2.6.0 aiobotocore==2.6.0
# homeassistant.components.comelit # homeassistant.components.comelit
aiocomelit==0.0.5 aiocomelit==0.0.8
# homeassistant.components.dhcp # homeassistant.components.dhcp
aiodiscover==1.4.16 aiodiscover==1.4.16
@ -430,7 +430,7 @@ base36==0.1.1
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.36.3 bellows==0.36.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected==0.14.0 bimmer-connected==0.14.0
@ -544,7 +544,7 @@ denonavr==0.11.3
devolo-home-control-api==0.18.2 devolo-home-control-api==0.18.2
# homeassistant.components.devolo_home_network # homeassistant.components.devolo_home_network
devolo-plc-api==1.4.0 devolo-plc-api==1.4.1
# homeassistant.components.directv # homeassistant.components.directv
directv==0.4.0 directv==0.4.0
@ -648,7 +648,7 @@ freebox-api==1.1.0
# homeassistant.components.fritz # homeassistant.components.fritz
# homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_callmonitor
fritzconnection[qr]==1.12.2 fritzconnection[qr]==1.13.2
# homeassistant.components.google_translate # homeassistant.components.google_translate
gTTS==2.2.4 gTTS==2.2.4
@ -780,7 +780,7 @@ holidays==0.28
home-assistant-frontend==20230911.0 home-assistant-frontend==20230911.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.8.2 home-assistant-intents==2023.9.22
# homeassistant.components.home_connect # homeassistant.components.home_connect
homeconnect==0.7.2 homeconnect==0.7.2
@ -927,7 +927,7 @@ micloud==0.5
mill-local==0.2.0 mill-local==0.2.0
# homeassistant.components.mill # homeassistant.components.mill
millheater==0.11.2 millheater==0.11.5
# homeassistant.components.minio # homeassistant.components.minio
minio==7.1.12 minio==7.1.12
@ -1235,7 +1235,7 @@ pyeconet==0.1.20
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.11.0 pyenphase==1.11.4
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0
@ -1382,7 +1382,7 @@ pymyq==3.1.4
pymysensors==0.24.0 pymysensors==0.24.0
# homeassistant.components.netgear # homeassistant.components.netgear
pynetgear==0.10.9 pynetgear==0.10.10
# homeassistant.components.nobo_hub # homeassistant.components.nobo_hub
pynobo==1.6.0 pynobo==1.6.0
@ -1477,7 +1477,7 @@ pyrympro==0.0.7
pysabnzbd==1.1.1 pysabnzbd==1.1.1
# homeassistant.components.schlage # homeassistant.components.schlage
pyschlage==2023.8.1 pyschlage==2023.9.1
# homeassistant.components.sensibo # homeassistant.components.sensibo
pysensibo==1.0.33 pysensibo==1.0.33
@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.3 python-qbittorrent==0.4.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==0.33.2 python-roborock==0.34.1
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.33 python-smarttub==0.0.33
@ -1639,7 +1639,7 @@ pyvizio==0.1.61
pyvolumio==0.1.5 pyvolumio==0.1.5
# homeassistant.components.waze_travel_time # homeassistant.components.waze_travel_time
pywaze==0.4.0 pywaze==0.5.0
# homeassistant.components.html5 # homeassistant.components.html5
pywebpush==1.9.2 pywebpush==1.9.2
@ -1684,13 +1684,13 @@ renault-api==0.2.0
renson-endura-delta==1.5.0 renson-endura-delta==1.5.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.7.9 reolink-aio==0.7.10
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell==0.7.2 ring-doorbell==0.7.3
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.18.1 rokuecp==0.18.1
@ -1735,7 +1735,7 @@ sense-energy==0.12.1
sense_energy==0.12.1 sense_energy==0.12.1
# homeassistant.components.sensirion_ble # homeassistant.components.sensirion_ble
sensirion-ble==0.1.0 sensirion-ble==0.1.1
# homeassistant.components.sensorpro # homeassistant.components.sensorpro
sensorpro-ble==0.5.3 sensorpro-ble==0.5.3
@ -1909,7 +1909,7 @@ ultraheat-api==0.5.7
unifi-discovery==1.1.7 unifi-discovery==1.1.7
# homeassistant.components.zha # homeassistant.components.zha
universal-silabs-flasher==0.0.13 universal-silabs-flasher==0.0.14
# homeassistant.components.upb # homeassistant.components.upb
upb-lib==0.5.4 upb-lib==0.5.4
@ -2015,16 +2015,16 @@ yalesmartalarmclient==0.3.9
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==2.2.3 yalexs-ble==2.3.0
# homeassistant.components.august # homeassistant.components.august
yalexs==1.8.0 yalexs==1.9.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.13 yeelight==0.7.13
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.0 yolink-api==0.3.1
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1
@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4
zigpy==0.57.1 zigpy==0.57.1
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.51.2 zwave-js-server-python==0.51.3
# homeassistant.components.zwave_me # homeassistant.components.zwave_me
zwave-me-ws==0.4.3 zwave-me-ws==0.4.3

View File

@ -366,15 +366,19 @@ def _sort_manifest_keys(key: str) -> str:
return _SORT_KEYS.get(key, key) return _SORT_KEYS.get(key, key)
def sort_manifest(integration: Integration) -> bool: def sort_manifest(integration: Integration, config: Config) -> bool:
"""Sort manifest.""" """Sort manifest."""
keys = list(integration.manifest.keys()) keys = list(integration.manifest.keys())
if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys: if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys:
manifest = {key: integration.manifest[key] for key in keys_sorted} manifest = {key: integration.manifest[key] for key in keys_sorted}
if config.action == "generate":
integration.manifest_path.write_text(json.dumps(manifest, indent=2)) integration.manifest_path.write_text(json.dumps(manifest, indent=2))
text = "have been sorted"
else:
text = "are not sorted correctly"
integration.add_error( integration.add_error(
"manifest", "manifest",
"Manifest keys have been sorted: domain, name, then alphabetical order", f"Manifest keys {text}: domain, name, then alphabetical order",
) )
return True return True
return False return False
@ -387,9 +391,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
for integration in integrations.values(): for integration in integrations.values():
validate_manifest(integration, core_components_dir) validate_manifest(integration, core_components_dir)
if not integration.errors: if not integration.errors:
if sort_manifest(integration): if sort_manifest(integration, config):
manifests_resorted.append(integration.manifest_path) manifests_resorted.append(integration.manifest_path)
if manifests_resorted: if config.action == "generate" and manifests_resorted:
subprocess.run( subprocess.run(
["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"]
+ manifests_resorted, + manifests_resorted,

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
import json import json
import pathlib import pathlib
from typing import Any from typing import Any, Literal
@dataclass @dataclass
@ -26,7 +26,7 @@ class Config:
specific_integrations: list[pathlib.Path] | None specific_integrations: list[pathlib.Path] | None
root: pathlib.Path root: pathlib.Path
action: str action: Literal["validate", "generate"]
requirements: bool requirements: bool
errors: list[Error] = field(default_factory=list) errors: list[Error] = field(default_factory=list)
cache: dict[str, Any] = field(default_factory=dict) cache: dict[str, Any] = field(default_factory=dict)

View File

@ -2,6 +2,7 @@
import logging import logging
from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.airthings_ble.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.components.airthings_ble import ( 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 entry is not None
assert device 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) entity_registry = hass.helpers.entity_registry.async_get(hass)
sensor = entity_registry.async_get_or_create( sensor = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=TEMPERATURE_V1.unique_id, unique_id=TEMPERATURE_V1.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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 len(hass.states.async_all()) > 0
assert ( assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id
entity_registry.async_get(sensor.entity_id).unique_id
== WAVE_DEVICE_INFO.address + "_temperature"
)
async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): 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( sensor = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=HUMIDITY_V2.unique_id, unique_id=HUMIDITY_V2.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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 len(hass.states.async_all()) > 0
assert ( # Migration should happen, v2 unique id should be updated to the new format
entity_registry.async_get(sensor.entity_id).unique_id new_unique_id = f"{WAVE_DEVICE_INFO.address}_humidity"
== 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): 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( v2 = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=CO2_V2.unique_id, unique_id=CO2_V2.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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( v1 = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=CO2_V1.unique_id, unique_id=CO2_V1.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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 len(hass.states.async_all()) > 0
assert ( # Migration should happen, v1 unique id should be updated to the new format
entity_registry.async_get(v1.entity_id).unique_id new_unique_id = f"{WAVE_DEVICE_INFO.address}_co2"
== 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
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
async def test_migration_with_all_unique_ids(hass: HomeAssistant): 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( v1 = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=VOC_V1.unique_id, unique_id=VOC_V1.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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( v2 = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=VOC_V2.unique_id, unique_id=VOC_V2.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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( v3 = entity_registry.async_get_or_create(
domain=DOMAIN, domain=DOMAIN,
platform="sensor", platform=Platform.SENSOR,
unique_id=VOC_V3.unique_id, unique_id=VOC_V3.unique_id,
config_entry=entry, config_entry=entry,
device_id=device.id, 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 len(hass.states.async_all()) > 0
assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id # No migration should happen, unique id should be the same as before
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id
assert entity_registry.async_get(v3.entity_id).unique_id == v3.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

View File

@ -18,9 +18,9 @@ from tests.common import MockConfigEntry
async def test_user(hass: HomeAssistant) -> None: async def test_user(hass: HomeAssistant) -> None:
"""Test starting a flow by user.""" """Test starting a flow by user."""
with patch( with patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.login", "aiocomelit.api.ComeliteSerialBridgeApi.login",
), patch( ), patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.logout", "aiocomelit.api.ComeliteSerialBridgeApi.logout",
), patch( ), patch(
"homeassistant.components.comelit.async_setup_entry" "homeassistant.components.comelit.async_setup_entry"
) as mock_setup_entry, patch( ) as mock_setup_entry, patch(
@ -64,7 +64,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) ->
assert result["step_id"] == "user" assert result["step_id"] == "user"
with patch( with patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.login", "aiocomelit.api.ComeliteSerialBridgeApi.login",
side_effect=side_effect, side_effect=side_effect,
): ):
result = await hass.config_entries.flow.async_configure( 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) mock_config.add_to_hass(hass)
with patch( with patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.login", "aiocomelit.api.ComeliteSerialBridgeApi.login",
), patch( ), patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.logout", "aiocomelit.api.ComeliteSerialBridgeApi.logout",
), patch("homeassistant.components.comelit.async_setup_entry"), patch( ), patch("homeassistant.components.comelit.async_setup_entry"), patch(
"requests.get" "requests.get"
) as mock_request_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) mock_config.add_to_hass(hass)
with patch( with patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect
), patch( ), patch(
"aiocomelit.api.ComeliteSerialBridgeAPi.logout", "aiocomelit.api.ComeliteSerialBridgeApi.logout",
), patch( ), patch(
"homeassistant.components.comelit.async_setup_entry" "homeassistant.components.comelit.async_setup_entry"
): ):

View File

@ -198,6 +198,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from .test_common import ( from .test_common import (
help_custom_config,
help_test_availability_when_connection_lost, help_test_availability_when_connection_lost,
help_test_availability_without_topic, help_test_availability_without_topic,
help_test_custom_availability_payload, 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 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( @pytest.mark.parametrize(
"hass_config", "hass_config",
[ [

View File

@ -76,41 +76,6 @@ def mock_controller_service():
yield service_mock 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: async def test_user(hass: HomeAssistant, service) -> None:
"""Test user step.""" """Test user step."""
result = await hass.config_entries.flow.async_init( 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 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.""" """Test user step with connection failure."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} 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["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
service.return_value.get_info = Mock(return_value=None)
# Have to provide all config # 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@ -160,7 +141,7 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
assert result["errors"] == {"base": "config"} 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.""" """Test user step with incomplete device info."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} 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["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user" 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 # Have to provide all config
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -313,7 +298,7 @@ async def test_ssdp(hass: HomeAssistant, service) -> None:
assert result["data"][CONF_PASSWORD] == PASSWORD 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.""" """Test ssdp step with port 5555."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, 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["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
service.return_value.port = 5555
service.return_value.ssl = True
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PASSWORD: PASSWORD} result["flow_id"], {CONF_PASSWORD: PASSWORD}
) )

View File

@ -35,6 +35,7 @@ SERIAL_NUMBER = 0x12635436566
# Get serial number Command 0x85. Serial is 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566
SERIAL_RESPONSE = "850000012635436566" SERIAL_RESPONSE = "850000012635436566"
ZERO_SERIAL_RESPONSE = "850000000000000000"
# Model and version command 0x82 # Model and version command 0x82
MODEL_AND_VERSION_RESPONSE = "820006090C" MODEL_AND_VERSION_RESPONSE = "820006090C"
# Get available stations command 0x83 # Get available stations command 0x83
@ -84,6 +85,12 @@ def yaml_config() -> dict[str, Any]:
return {} return {}
@pytest.fixture
async def unique_id() -> str:
"""Fixture for serial number used in the config entry."""
return SERIAL_NUMBER
@pytest.fixture @pytest.fixture
async def config_entry_data() -> dict[str, Any]: async def config_entry_data() -> dict[str, Any]:
"""Fixture for MockConfigEntry data.""" """Fixture for MockConfigEntry data."""
@ -92,13 +99,14 @@ async def config_entry_data() -> dict[str, Any]:
@pytest.fixture @pytest.fixture
async def config_entry( async def config_entry(
config_entry_data: dict[str, Any] | None config_entry_data: dict[str, Any] | None,
unique_id: str,
) -> MockConfigEntry | None: ) -> MockConfigEntry | None:
"""Fixture for MockConfigEntry.""" """Fixture for MockConfigEntry."""
if config_entry_data is None: if config_entry_data is None:
return None return None
return MockConfigEntry( return MockConfigEntry(
unique_id=SERIAL_NUMBER, unique_id=unique_id,
domain=DOMAIN, domain=DOMAIN,
data=config_entry_data, data=config_entry_data,
options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES},

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Generator
from http import HTTPStatus from http import HTTPStatus
from typing import Any
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@ -19,8 +20,11 @@ from .conftest import (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
HOST, HOST,
PASSWORD, PASSWORD,
SERIAL_NUMBER,
SERIAL_RESPONSE, SERIAL_RESPONSE,
URL, URL,
ZERO_SERIAL_RESPONSE,
ComponentSetup,
mock_response, 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.""" """Test the controller is setup correctly."""
result = await complete_flow(hass) result = await complete_flow(hass)
assert result.get("type") == "create_entry" assert result.get("type") == "create_entry"
assert result.get("title") == HOST assert result.get("title") == HOST
assert "result" in result 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"].options == {ATTR_DURATION: 6}
assert result["result"].unique_id == expected_unique_id
assert len(mock_setup.mock_calls) == 1 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( async def test_controller_cannot_connect(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup: Mock, mock_setup: Mock,

View File

@ -225,11 +225,13 @@
'area': 20965000, 'area': 20965000,
'avoidCount': 19, 'avoidCount': 19,
'begin': 1672543330, 'begin': 1672543330,
'beginDatetime': '2023-01-01T03:22:10+00:00',
'cleanType': 3, 'cleanType': 3,
'complete': 1, 'complete': 1,
'duration': 1176, 'duration': 1176,
'dustCollectionStatus': 1, 'dustCollectionStatus': 1,
'end': 1672544638, 'end': 1672544638,
'endDatetime': '2023-01-01T03:43:58+00:00',
'error': 0, 'error': 0,
'finishReason': 56, 'finishReason': 56,
'mapFlag': 0, 'mapFlag': 0,

View File

@ -44,6 +44,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
MockEntityPlatform,
MockModule, MockModule,
MockPlatform, MockPlatform,
async_mock_restore_state_shutdown_restart, async_mock_restore_state_shutdown_restart,
@ -2177,27 +2178,24 @@ async def test_unit_conversion_update(
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
platform = getattr(hass.components, "test.sensor") platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor( entity0 = platform.MockSensor(
name="Test 0", name="Test 0",
device_class=device_class, device_class=device_class,
native_unit_of_measurement=native_unit, native_unit_of_measurement=native_unit,
native_value=str(native_value), native_value=str(native_value),
unique_id="very_unique", unique_id="very_unique",
) )
entity0 = platform.ENTITIES["0"]
platform.ENTITIES["1"] = platform.MockSensor( entity1 = platform.MockSensor(
name="Test 1", name="Test 1",
device_class=device_class, device_class=device_class,
native_unit_of_measurement=native_unit, native_unit_of_measurement=native_unit,
native_value=str(native_value), native_value=str(native_value),
unique_id="very_unique_1", unique_id="very_unique_1",
) )
entity1 = platform.ENTITIES["1"]
platform.ENTITIES["2"] = platform.MockSensor( entity2 = platform.MockSensor(
name="Test 2", name="Test 2",
device_class=device_class, device_class=device_class,
native_unit_of_measurement=native_unit, native_unit_of_measurement=native_unit,
@ -2205,9 +2203,8 @@ async def test_unit_conversion_update(
suggested_unit_of_measurement=suggested_unit, suggested_unit_of_measurement=suggested_unit,
unique_id="very_unique_2", unique_id="very_unique_2",
) )
entity2 = platform.ENTITIES["2"]
platform.ENTITIES["3"] = platform.MockSensor( entity3 = platform.MockSensor(
name="Test 3", name="Test 3",
device_class=device_class, device_class=device_class,
native_unit_of_measurement=native_unit, native_unit_of_measurement=native_unit,
@ -2215,9 +2212,33 @@ async def test_unit_conversion_update(
suggested_unit_of_measurement=suggested_unit, suggested_unit_of_measurement=suggested_unit,
unique_id="very_unique_3", 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() await hass.async_block_till_done()
# Registered entity -> Follow automatic unit conversion # Registered entity -> Follow automatic unit conversion
@ -2320,6 +2341,25 @@ async def test_unit_conversion_update(
assert state.state == suggested_state assert state.state == suggested_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit 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): class MockFlow(ConfigFlow):
"""Test flow.""" """Test flow."""

View File

@ -1460,6 +1460,39 @@ def test_calculate_adjustment_invalid_new_state(
assert "Invalid state unknown" in caplog.text 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: async def test_device_id(hass: HomeAssistant) -> None:
"""Test for source entity device for Utility Meter.""" """Test for source entity device for Utility Meter."""
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)