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

View File

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

View File

@ -58,6 +58,16 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi
"""Describes Airnow sensor entity."""
def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
"""Process extra attributes for station location (if available)."""
if ATTR_API_STATION in data:
return {
"lat": data.get(ATTR_API_STATION_LATITUDE),
"long": data.get(ATTR_API_STATION_LONGITUDE),
}
return {}
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_AQI,
@ -93,10 +103,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
translation_key="station",
icon="mdi:blur",
value_fn=lambda data: data.get(ATTR_API_STATION),
extra_state_attributes_fn=lambda data: {
"lat": data[ATTR_API_STATION_LATITUDE],
"long": data[ATTR_API_STATION_LONGITUDE],
},
extra_state_attributes_fn=station_extra_attrs,
),
)

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
):
matching_reg_entry = entry
if not matching_reg_entry:
if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id:
# Already has the newest unique id format
return
entity_id = matching_reg_entry.entity_id
ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)

View File

@ -298,6 +298,26 @@ class Pipeline:
id: str = field(default_factory=ulid_util.ulid)
@classmethod
def from_json(cls, data: dict[str, Any]) -> Pipeline:
"""Create an instance from a JSON serialization.
This function was added in HA Core 2023.10, previous versions will raise
if there are unexpected items in the serialized data.
"""
return cls(
conversation_engine=data["conversation_engine"],
conversation_language=data["conversation_language"],
id=data["id"],
language=data["language"],
name=data["name"],
stt_engine=data["stt_engine"],
stt_language=data["stt_language"],
tts_engine=data["tts_engine"],
tts_language=data["tts_language"],
tts_voice=data["tts_voice"],
)
def to_json(self) -> dict[str, Any]:
"""Return a JSON serializable representation for storage."""
return {
@ -1205,7 +1225,7 @@ class PipelineStorageCollection(
def _deserialize_item(self, data: dict) -> Pipeline:
"""Create an item from its serialized representation."""
return Pipeline(**data)
return Pipeline.from_json(data)
def _serialize_item(self, item_id: str, item: Pipeline) -> dict:
"""Return the serialized representation of an item for storing."""

View File

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

View File

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

View File

@ -3,11 +3,14 @@ import asyncio
from datetime import timedelta
from typing import Any
from aiocomelit import ComeliteSerialBridgeAPi
from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject
from aiocomelit.const import BRIDGE
import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, DOMAIN
@ -16,13 +19,15 @@ from .const import _LOGGER, DOMAIN
class ComelitSerialBridge(DataUpdateCoordinator):
"""Queries Comelit Serial Bridge."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None:
"""Initialize the scanner."""
self._host = host
self._pin = pin
self.api = ComeliteSerialBridgeAPi(host, pin)
self.api = ComeliteSerialBridgeApi(host, pin)
super().__init__(
hass=hass,
@ -30,6 +35,38 @@ class ComelitSerialBridge(DataUpdateCoordinator):
name=f"{DOMAIN}-{host}-coordinator",
update_interval=timedelta(seconds=5),
)
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, self.config_entry.entry_id)},
model=BRIDGE,
name=f"{BRIDGE} ({self.api.host})",
**self.basic_device_info,
)
@property
def basic_device_info(self) -> dict:
"""Set basic device info."""
return {
"manufacturer": "Comelit",
"hw_version": "20003101",
}
def platform_device_info(
self, device: ComelitSerialBridgeObject, platform: str
) -> dr.DeviceInfo:
"""Set platform device info."""
return dr.DeviceInfo(
identifiers={
(DOMAIN, f"{self.config_entry.entry_id}-{platform}-{device.index}")
},
via_device=(DOMAIN, self.config_entry.entry_id),
name=device.name,
model=f"{BRIDGE} {platform}",
**self.basic_device_info,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update router data."""

View File

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

View File

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

View File

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

View File

@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.8.2"]
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -189,7 +189,11 @@ class FritzBoxCallMonitor:
_LOGGER.debug("Setting up socket connection")
try:
self.connection = FritzMonitor(address=self.host, port=self.port)
kwargs: dict[str, Any] = {"event_queue": self.connection.start()}
kwargs: dict[str, Any] = {
"event_queue": self.connection.start(
reconnect_tries=50, reconnect_delay=120
)
}
Thread(target=self._process_events, kwargs=kwargs).start()
except OSError as err:
self.connection = None

View File

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

View File

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

View File

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

View File

@ -462,6 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received)
@callback
def _rgbx_received(
msg: ReceiveMessage,
template: str,
@ -532,11 +533,26 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
@log_messages(self.hass, self.entity_id)
def rgbww_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages for RGBWW."""
@callback
def _converter(
r: int, g: int, b: int, cw: int, ww: int
) -> tuple[int, int, int]:
min_kelvin = color_util.color_temperature_mired_to_kelvin(
self.max_mireds
)
max_kelvin = color_util.color_temperature_mired_to_kelvin(
self.min_mireds
)
return color_util.color_rgbww_to_rgb(
r, g, b, cw, ww, min_kelvin, max_kelvin
)
rgbww = _rgbx_received(
msg,
CONF_RGBWW_VALUE_TEMPLATE,
ColorMode.RGBWW,
color_util.color_rgbww_to_rgb,
_converter,
)
if rgbww is None:
return

View File

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

View File

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

View File

@ -11,7 +11,8 @@
}
},
"error": {
"config": "Connection or login error: please check your configuration"
"config": "Connection or login error: please check your configuration",
"info": "Failed to get info from router"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
options: dict[str, Any],
) -> FlowResult:
"""Create the config entry."""
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
# Prevent devices with the same serial number. If the device does not have a serial number
# then we can at least prevent configuring the same host twice.
if serial_number:
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
else:
self._async_abort_entries_match(data)
return self.async_create_entry(
title=data[CONF_HOST],
data=data,

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ class RoborockEntity(Entity):
async def send(
self,
command: RoborockCommand,
command: RoborockCommand | str,
params: dict[str, Any] | list[Any] | int | None = None,
) -> dict:
"""Send a command to a vacuum cleaner."""
@ -48,7 +48,7 @@ class RoborockEntity(Entity):
response = await self._api.send_command(command, params)
except RoborockException as err:
raise HomeAssistantError(
f"Error while calling {command.name} with {params}"
f"Error while calling {command.name if isinstance(command, RoborockCommand) else command} with {params}"
) from err
return response

View File

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

View File

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

View File

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

View File

@ -54,7 +54,15 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode"
ATTR_LIGHT = "light"
BOOST_INCLUSIVE = "boost_inclusive"
AVAILABLE_FAN_MODES = {"quiet", "low", "medium", "medium_high", "high", "auto"}
AVAILABLE_FAN_MODES = {
"quiet",
"low",
"medium",
"medium_high",
"high",
"strong",
"auto",
}
AVAILABLE_SWING_MODES = {
"stopped",
"fixedtop",

View File

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

View File

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

View File

@ -345,7 +345,7 @@ class SensorEntity(Entity):
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
and then only updated if the unit system is changed.
"""
suggested_unit_of_measurement = self._get_initial_suggested_unit()
@ -783,7 +783,7 @@ class SensorEntity(Entity):
registry = er.async_get(self.hass)
initial_options = self.get_initial_entity_options() or {}
registry.async_update_entity_options(
self.entity_id,
self.registry_entry.entity_id,
f"{DOMAIN}.private",
initial_options.get(f"{DOMAIN}.private"),
)

View File

@ -44,7 +44,12 @@ from homeassistant.util.unit_conversion import (
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys())
CHECK_FORECAST_KEYS = (
set().union(Forecast.__annotations__.keys())
# Manually add the forecast resulting attributes that only exists
# as native_* in the Forecast definition
.union(("apparent_temperature", "wind_gust_speed", "dew_point"))
)
CONDITION_CLASSES = {
ATTR_CONDITION_CLEAR_NIGHT,
@ -434,7 +439,8 @@ class WeatherTemplate(TemplateEntity, WeatherEntity):
diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS)
if diff_result:
raise vol.Invalid(
"Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/"
f"Only valid keys in Forecast are allowed, unallowed keys: ({diff_result}), "
"see Weather documentation https://www.home-assistant.io/integrations/weather/"
)
if forecast_type == "twice_daily" and "is_daytime" not in forecast:
raise vol.Invalid(

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import _suggest_report_issue
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
@ -484,6 +485,12 @@ class UtilityMeterSensor(RestoreSensor):
DATA_TARIFF_SENSORS
]:
sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
if self._unit_of_measurement is None:
_LOGGER.warning(
"Source sensor %s has no unit of measurement. Please %s",
self._sensor_source_id,
_suggest_report_issue(self.hass, self._sensor_source_id),
)
if (
adjustment := self.calculate_adjustment(old_state, new_state)
@ -491,6 +498,7 @@ class UtilityMeterSensor(RestoreSensor):
# If net_consumption is off, the adjustment must be non-negative
self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._last_valid_state = new_state_val
self.async_write_ha_state()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["zwave_js_server"],
"quality_scale": "platinum",
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"],
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.3"],
"usb": [
{
"vid": "0658",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
import logging
from homeassistant.components.airthings_ble.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.components.airthings_ble import (
@ -31,11 +32,13 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant):
assert entry is not None
assert device is not None
new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature"
entity_registry = hass.helpers.entity_registry.async_get(hass)
sensor = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=TEMPERATURE_V1.unique_id,
config_entry=entry,
device_id=device.id,
@ -57,10 +60,7 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant):
assert len(hass.states.async_all()) > 0
assert (
entity_registry.async_get(sensor.entity_id).unique_id
== WAVE_DEVICE_INFO.address + "_temperature"
)
assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id
async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
@ -77,7 +77,7 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
sensor = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=HUMIDITY_V2.unique_id,
config_entry=entry,
device_id=device.id,
@ -99,10 +99,9 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant):
assert len(hass.states.async_all()) > 0
assert (
entity_registry.async_get(sensor.entity_id).unique_id
== WAVE_DEVICE_INFO.address + "_humidity"
)
# Migration should happen, v2 unique id should be updated to the new format
new_unique_id = f"{WAVE_DEVICE_INFO.address}_humidity"
assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id
async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
@ -119,7 +118,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
v2 = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=CO2_V2.unique_id,
config_entry=entry,
device_id=device.id,
@ -127,7 +126,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
v1 = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=CO2_V1.unique_id,
config_entry=entry,
device_id=device.id,
@ -149,11 +148,10 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant):
assert len(hass.states.async_all()) > 0
assert (
entity_registry.async_get(v1.entity_id).unique_id
== WAVE_DEVICE_INFO.address + "_co2"
)
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
# Migration should happen, v1 unique id should be updated to the new format
new_unique_id = f"{WAVE_DEVICE_INFO.address}_co2"
assert entity_registry.async_get(v1.entity_id).unique_id == new_unique_id
assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id
async def test_migration_with_all_unique_ids(hass: HomeAssistant):
@ -170,7 +168,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
v1 = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=VOC_V1.unique_id,
config_entry=entry,
device_id=device.id,
@ -178,7 +176,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
v2 = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=VOC_V2.unique_id,
config_entry=entry,
device_id=device.id,
@ -186,7 +184,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
v3 = entity_registry.async_get_or_create(
domain=DOMAIN,
platform="sensor",
platform=Platform.SENSOR,
unique_id=VOC_V3.unique_id,
config_entry=entry,
device_id=device.id,
@ -208,6 +206,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant):
assert len(hass.states.async_all()) > 0
assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id
assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id
assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id
# No migration should happen, unique id should be the same as before
assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id
assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id
assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id

View File

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

View File

@ -198,6 +198,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, State
from .test_common import (
help_custom_config,
help_test_availability_when_connection_lost,
help_test_availability_without_topic,
help_test_custom_availability_payload,
@ -441,6 +442,176 @@ async def test_controlling_state_via_topic(
assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
@pytest.mark.parametrize(
"hass_config",
[
help_custom_config(
light.DOMAIN,
DEFAULT_CONFIG,
(
{
"state_topic": "test-topic",
"optimistic": True,
"brightness_command_topic": "test_light_rgb/brightness/set",
"color_mode_state_topic": "color-mode-state-topic",
"rgb_command_topic": "test_light_rgb/rgb/set",
"rgb_state_topic": "rgb-state-topic",
"rgbw_command_topic": "test_light_rgb/rgbw/set",
"rgbw_state_topic": "rgbw-state-topic",
"rgbww_command_topic": "test_light_rgb/rgbww/set",
"rgbww_state_topic": "rgbww-state-topic",
},
),
)
],
)
async def test_received_rgbx_values_set_state_optimistic(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test the state is set correctly when an rgbx update is received."""
await mqtt_mock_entry()
state = hass.states.get("light.test")
assert state and state.state is not None
async_fire_mqtt_message(hass, "test-topic", "ON")
## Test rgb processing
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
assert state.attributes["rgb_color"] == (255, 255, 255)
# Only update color mode
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgbww")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbww"
# Resending same rgb value should restore color mode
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
assert state.attributes["rgb_color"] == (255, 255, 255)
# Only update brightness
await common.async_turn_on(hass, "light.test", brightness=128)
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 128
assert state.attributes["color_mode"] == "rgb"
assert state.attributes["rgb_color"] == (255, 255, 255)
# Resending same rgb value should restore brightness
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
assert state.attributes["rgb_color"] == (255, 255, 255)
# Only change rgb value
async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,0")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
assert state.attributes["rgb_color"] == (255, 255, 0)
## Test rgbw processing
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbw"
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
# Only update color mode
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
# Resending same rgbw value should restore color mode
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbw"
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
# Only update brightness
await common.async_turn_on(hass, "light.test", brightness=128)
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 128
assert state.attributes["color_mode"] == "rgbw"
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
# Resending same rgbw value should restore brightness
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbw"
assert state.attributes["rgbw_color"] == (255, 255, 255, 255)
# Only change rgbw value
async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,128,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbw"
assert state.attributes["rgbw_color"] == (255, 255, 128, 255)
## Test rgbww processing
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbww"
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
# Only update color mode
async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgb"
# Resending same rgbw value should restore color mode
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbww"
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
# Only update brightness
await common.async_turn_on(hass, "light.test", brightness=128)
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 128
assert state.attributes["color_mode"] == "rgbww"
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
# Resending same rgbww value should restore brightness
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbww"
assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255)
# Only change rgbww value
async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,128,32,255")
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.attributes["brightness"] == 255
assert state.attributes["color_mode"] == "rgbww"
assert state.attributes["rgbww_color"] == (255, 255, 128, 32, 255)
@pytest.mark.parametrize(
"hass_config",
[

View File

@ -76,41 +76,6 @@ def mock_controller_service():
yield service_mock
@pytest.fixture(name="service_5555")
def mock_controller_service_5555():
"""Mock a successful service."""
with patch(
"homeassistant.components.netgear.async_setup_entry", return_value=True
), patch("homeassistant.components.netgear.router.Netgear") as service_mock:
service_mock.return_value.get_info = Mock(return_value=ROUTER_INFOS)
service_mock.return_value.port = 5555
service_mock.return_value.ssl = True
yield service_mock
@pytest.fixture(name="service_incomplete")
def mock_controller_service_incomplete():
"""Mock a successful service."""
router_infos = ROUTER_INFOS.copy()
router_infos.pop("DeviceName")
with patch(
"homeassistant.components.netgear.async_setup_entry", return_value=True
), patch("homeassistant.components.netgear.router.Netgear") as service_mock:
service_mock.return_value.get_info = Mock(return_value=router_infos)
service_mock.return_value.port = 80
service_mock.return_value.ssl = False
yield service_mock
@pytest.fixture(name="service_failed")
def mock_controller_service_failed():
"""Mock a failed service."""
with patch("homeassistant.components.netgear.router.Netgear") as service_mock:
service_mock.return_value.login_try_port = Mock(return_value=None)
service_mock.return_value.get_info = Mock(return_value=None)
yield service_mock
async def test_user(hass: HomeAssistant, service) -> None:
"""Test user step."""
result = await hass.config_entries.flow.async_init(
@ -138,7 +103,7 @@ async def test_user(hass: HomeAssistant, service) -> None:
assert result["data"][CONF_PASSWORD] == PASSWORD
async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
async def test_user_connect_error(hass: HomeAssistant, service) -> None:
"""Test user step with connection failure."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@ -146,7 +111,23 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
service.return_value.get_info = Mock(return_value=None)
# Have to provide all config
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: HOST,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "info"}
service.return_value.login_try_port = Mock(return_value=None)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@ -160,7 +141,7 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None:
assert result["errors"] == {"base": "config"}
async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> None:
async def test_user_incomplete_info(hass: HomeAssistant, service) -> None:
"""Test user step with incomplete device info."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@ -168,6 +149,10 @@ async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) ->
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
router_infos = ROUTER_INFOS.copy()
router_infos.pop("DeviceName")
service.return_value.get_info = Mock(return_value=router_infos)
# Have to provide all config
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -313,7 +298,7 @@ async def test_ssdp(hass: HomeAssistant, service) -> None:
assert result["data"][CONF_PASSWORD] == PASSWORD
async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None:
async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None:
"""Test ssdp step with port 5555."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
@ -332,6 +317,9 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None:
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
service.return_value.port = 5555
service.return_value.ssl = True
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PASSWORD: PASSWORD}
)

View File

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

View File

@ -3,6 +3,7 @@
import asyncio
from collections.abc import Generator
from http import HTTPStatus
from typing import Any
from unittest.mock import Mock, patch
import pytest
@ -19,8 +20,11 @@ from .conftest import (
CONFIG_ENTRY_DATA,
HOST,
PASSWORD,
SERIAL_NUMBER,
SERIAL_RESPONSE,
URL,
ZERO_SERIAL_RESPONSE,
ComponentSetup,
mock_response,
)
@ -66,19 +70,132 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult:
)
async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None:
@pytest.mark.parametrize(
("responses", "expected_config_entry", "expected_unique_id"),
[
(
[mock_response(SERIAL_RESPONSE)],
CONFIG_ENTRY_DATA,
SERIAL_NUMBER,
),
(
[mock_response(ZERO_SERIAL_RESPONSE)],
{**CONFIG_ENTRY_DATA, "serial_number": 0},
None,
),
],
)
async def test_controller_flow(
hass: HomeAssistant,
mock_setup: Mock,
expected_config_entry: dict[str, str],
expected_unique_id: int | None,
) -> None:
"""Test the controller is setup correctly."""
result = await complete_flow(hass)
assert result.get("type") == "create_entry"
assert result.get("title") == HOST
assert "result" in result
assert result["result"].data == CONFIG_ENTRY_DATA
assert dict(result["result"].data) == expected_config_entry
assert result["result"].options == {ATTR_DURATION: 6}
assert result["result"].unique_id == expected_unique_id
assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_data",
"config_flow_responses",
"expected_config_entry",
),
[
(
"other-serial-number",
{**CONFIG_ENTRY_DATA, "host": "other-host"},
[mock_response(SERIAL_RESPONSE)],
CONFIG_ENTRY_DATA,
),
(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
[mock_response(ZERO_SERIAL_RESPONSE)],
{**CONFIG_ENTRY_DATA, "serial_number": 0},
),
],
ids=["with-serial", "zero-serial"],
)
async def test_multiple_config_entries(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
config_flow_responses: list[AiohttpClientMockResponse],
expected_config_entry: dict[str, Any] | None,
) -> None:
"""Test setting up multiple config entries that refer to different devices."""
assert await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state == ConfigEntryState.LOADED
responses.clear()
responses.extend(config_flow_responses)
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert dict(result.get("result").data) == expected_config_entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 2
@pytest.mark.parametrize(
(
"unique_id",
"config_entry_data",
"config_flow_responses",
),
[
(
SERIAL_NUMBER,
CONFIG_ENTRY_DATA,
[mock_response(SERIAL_RESPONSE)],
),
(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0},
[mock_response(ZERO_SERIAL_RESPONSE)],
),
],
ids=[
"duplicate-serial-number",
"duplicate-host-port-no-serial",
],
)
async def test_duplicate_config_entries(
hass: HomeAssistant,
setup_integration: ComponentSetup,
responses: list[AiohttpClientMockResponse],
config_flow_responses: list[AiohttpClientMockResponse],
) -> None:
"""Test that a device can not be registered twice."""
assert await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state == ConfigEntryState.LOADED
responses.clear()
responses.extend(config_flow_responses)
result = await complete_flow(hass)
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured"
async def test_controller_cannot_connect(
hass: HomeAssistant,
mock_setup: Mock,

View File

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

View File

@ -44,6 +44,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
from tests.common import (
MockConfigEntry,
MockEntityPlatform,
MockModule,
MockPlatform,
async_mock_restore_state_shutdown_restart,
@ -2177,27 +2178,24 @@ async def test_unit_conversion_update(
entity_registry = er.async_get(hass)
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
entity0 = platform.MockSensor(
name="Test 0",
device_class=device_class,
native_unit_of_measurement=native_unit,
native_value=str(native_value),
unique_id="very_unique",
)
entity0 = platform.ENTITIES["0"]
platform.ENTITIES["1"] = platform.MockSensor(
entity1 = platform.MockSensor(
name="Test 1",
device_class=device_class,
native_unit_of_measurement=native_unit,
native_value=str(native_value),
unique_id="very_unique_1",
)
entity1 = platform.ENTITIES["1"]
platform.ENTITIES["2"] = platform.MockSensor(
entity2 = platform.MockSensor(
name="Test 2",
device_class=device_class,
native_unit_of_measurement=native_unit,
@ -2205,9 +2203,8 @@ async def test_unit_conversion_update(
suggested_unit_of_measurement=suggested_unit,
unique_id="very_unique_2",
)
entity2 = platform.ENTITIES["2"]
platform.ENTITIES["3"] = platform.MockSensor(
entity3 = platform.MockSensor(
name="Test 3",
device_class=device_class,
native_unit_of_measurement=native_unit,
@ -2215,9 +2212,33 @@ async def test_unit_conversion_update(
suggested_unit_of_measurement=suggested_unit,
unique_id="very_unique_3",
)
entity3 = platform.ENTITIES["3"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
entity4 = platform.MockSensor(
name="Test 4",
device_class=device_class,
native_unit_of_measurement=native_unit,
native_value=str(native_value),
unique_id="very_unique_4",
)
entity_platform = MockEntityPlatform(
hass, domain="sensor", platform_name="test", platform=None
)
await entity_platform.async_add_entities((entity0, entity1, entity2, entity3))
# Pre-register entity4
entry = entity_registry.async_get_or_create(
"sensor", "test", entity4.unique_id, unit_of_measurement=automatic_unit_1
)
entity4_entity_id = entry.entity_id
entity_registry.async_update_entity_options(
entity4_entity_id,
"sensor.private",
{
"suggested_unit_of_measurement": automatic_unit_1,
},
)
await hass.async_block_till_done()
# Registered entity -> Follow automatic unit conversion
@ -2320,6 +2341,25 @@ async def test_unit_conversion_update(
assert state.state == suggested_state
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
# Entity 4 still has a pending request to refresh entity options
entry = entity_registry.async_get(entity4_entity_id)
assert entry.options == {
"sensor.private": {
"refresh_initial_entity_options": True,
"suggested_unit_of_measurement": automatic_unit_1,
}
}
# Add entity 4, the pending request to refresh entity options should be handled
await entity_platform.async_add_entities((entity4,))
state = hass.states.get(entity4_entity_id)
assert state.state == automatic_state_2
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2
entry = entity_registry.async_get(entity4_entity_id)
assert entry.options == {}
class MockFlow(ConfigFlow):
"""Test flow."""

View File

@ -1460,6 +1460,39 @@ def test_calculate_adjustment_invalid_new_state(
assert "Invalid state unknown" in caplog.text
async def test_unit_of_measurement_missing_invalid_new_state(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a suggestion is created when new_state is missing unit_of_measurement."""
yaml_config = {
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
}
}
}
source_entity_id = yaml_config[DOMAIN]["energy_bill"]["source"]
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
hass.states.async_set(source_entity_id, 4, {ATTR_UNIT_OF_MEASUREMENT: None})
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill")
assert state is not None
assert state.state == "0"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
assert (
f"Source sensor {source_entity_id} has no unit of measurement." in caplog.text
)
async def test_device_id(hass: HomeAssistant) -> None:
"""Test for source entity device for Utility Meter."""
device_registry = dr.async_get(hass)