mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
2023.9.3 (#100755)
This commit is contained in:
commit
0cbd46592a
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@ -197,7 +197,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
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
|
||||||
|
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@ -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
|
||||||
|
@ -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],
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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%]"
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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.",
|
||||||
|
@ -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:
|
||||||
|
@ -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."
|
||||||
|
@ -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"
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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.",
|
||||||
|
@ -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%]",
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -36,3 +36,5 @@ change:
|
|||||||
example: "00:01:00, 60 or -60"
|
example: "00:01:00, 60 or -60"
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
reload:
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
):
|
):
|
||||||
|
@ -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",
|
||||||
[
|
[
|
||||||
|
@ -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}
|
||||||
)
|
)
|
||||||
|
@ -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},
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user