mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
2022.9.2 (#78169)
This commit is contained in:
commit
2bd71f62ea
@ -15,6 +15,11 @@
|
||||
},
|
||||
"description": "Select the NMI of the site you would like to add"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"no_site": "No site provided",
|
||||
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,11 @@
|
||||
},
|
||||
"description": "Go to {api_url} to generate an API key"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_token": "Invalid API key",
|
||||
"no_site": "No site provided",
|
||||
"unknown_error": "Unexpected error"
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.16.0",
|
||||
"bluetooth-adapters==0.3.5",
|
||||
"bluetooth-adapters==0.4.1",
|
||||
"bluetooth-auto-recovery==0.3.2"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
|
@ -68,4 +68,4 @@ class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.ecowitt.value > 0
|
||||
return bool(self.ecowitt.value)
|
||||
|
@ -25,13 +25,13 @@ async def async_get_device_diagnostics(
|
||||
"device": {
|
||||
"name": station.station,
|
||||
"model": station.model,
|
||||
"frequency": station.frequency,
|
||||
"frequency": station.frequence,
|
||||
"version": station.version,
|
||||
},
|
||||
"raw": ecowitt.last_values[station_id],
|
||||
"sensors": {
|
||||
sensor.key: sensor.value
|
||||
for sensor in station.sensors
|
||||
for sensor in ecowitt.sensors.values()
|
||||
if sensor.station.key == station_id
|
||||
},
|
||||
}
|
||||
|
@ -3,7 +3,8 @@
|
||||
"name": "Ecowitt",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"requirements": ["aioecowitt==2022.08.3"],
|
||||
"dependencies": ["webhook"],
|
||||
"requirements": ["aioecowitt==2022.09.1"],
|
||||
"codeowners": ["@pvizeli"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Support for Ecowitt Weather Stations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from datetime import datetime
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
|
||||
@ -242,6 +245,6 @@ class EcowittSensorEntity(EcowittEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
return self.ecowitt.value
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==1.5.2"],
|
||||
"requirements": ["aiohomekit==1.5.6"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.info("Polling on %s", entry.data[CONF_DEVICE])
|
||||
return await hass.async_add_executor_job(api.read)
|
||||
|
||||
# No automatic polling and no initial refresh of data is being done at this point,
|
||||
# to prevent battery drain. The user will have to do it manually.
|
||||
|
||||
# Polling is only daily to prevent battery drain.
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
|
@ -14,7 +14,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, ULTRAHEAT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -43,6 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
dev_path = await self.hass.async_add_executor_job(
|
||||
get_serial_by_id, user_input[CONF_DEVICE]
|
||||
)
|
||||
_LOGGER.debug("Using this path : %s", dev_path)
|
||||
|
||||
try:
|
||||
return await self.validate_and_create_entry(dev_path)
|
||||
@ -76,6 +77,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Try to connect to the device path and return an entry."""
|
||||
model, device_number = await self.validate_ultraheat(dev_path)
|
||||
|
||||
_LOGGER.debug("Got model %s and device_number %s", model, device_number)
|
||||
await self.async_set_unique_id(device_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
data = {
|
||||
@ -94,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
reader = UltraheatReader(port)
|
||||
heat_meter = HeatMeterService(reader)
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with async_timeout.timeout(ULTRAHEAT_TIMEOUT):
|
||||
# validate and retrieve the model and device number for a unique id
|
||||
data = await self.hass.async_add_executor_job(heat_meter.read)
|
||||
_LOGGER.debug("Got data from Ultraheat API: %s", data)
|
||||
|
@ -11,6 +11,7 @@ from homeassistant.helpers.entity import EntityCategory
|
||||
DOMAIN = "landisgyr_heat_meter"
|
||||
|
||||
GJ_TO_MWH = 0.277778 # conversion factor
|
||||
ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time
|
||||
|
||||
HEAT_METER_SENSOR_TYPES = (
|
||||
SensorEntityDescription(
|
||||
|
@ -6,7 +6,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from led_ble import BLEAK_EXCEPTIONS, LEDBLE
|
||||
from led_ble import BLEAK_EXCEPTIONS, LEDBLE, get_device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
|
||||
@ -27,7 +27,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up LED BLE from a config entry."""
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, address.upper(), True
|
||||
) or await get_device(address)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find LED BLE device with address {address}"
|
||||
|
@ -48,12 +48,12 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity):
|
||||
"""Initialize an ledble light."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_unique_id = device._address
|
||||
self._attr_unique_id = device.address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=name,
|
||||
model=hex(device.model_num),
|
||||
sw_version=hex(device.version_num),
|
||||
connections={(dr.CONNECTION_BLUETOOTH, device._address)},
|
||||
connections={(dr.CONNECTION_BLUETOOTH, device.address)},
|
||||
)
|
||||
self._async_update_attrs()
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "LED BLE",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ble_ble",
|
||||
"requirements": ["led-ble==0.7.1"],
|
||||
"requirements": ["led-ble==0.9.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
@ -219,15 +219,10 @@ class LIFXLight(LIFXEntity, LightEntity):
|
||||
elif power_on:
|
||||
await self.set_power(True, duration=fade)
|
||||
else:
|
||||
if power_on:
|
||||
await self.set_power(True)
|
||||
if hsbk:
|
||||
await self.set_color(hsbk, kwargs, duration=fade)
|
||||
# The response from set_color will tell us if the
|
||||
# bulb is actually on or not, so we don't need to
|
||||
# call power_on if its already on
|
||||
if power_on and self.bulb.power_level == 0:
|
||||
await self.set_power(True)
|
||||
elif power_on:
|
||||
await self.set_power(True)
|
||||
if power_off:
|
||||
await self.set_power(False, duration=fade)
|
||||
|
||||
|
@ -20,7 +20,13 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@ -68,9 +74,11 @@ from .const import ( # noqa: F401
|
||||
CONFIG_ENTRY_IS_SETUP,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_RELOAD_ENTRY,
|
||||
DATA_MQTT_RELOAD_NEEDED,
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_QOS,
|
||||
@ -314,7 +322,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Bail out
|
||||
return False
|
||||
|
||||
hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {}
|
||||
hass.data[DATA_MQTT] = MQTT(hass, entry, conf)
|
||||
# Restore saved subscriptions
|
||||
if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data:
|
||||
hass.data[DATA_MQTT].subscriptions = hass.data.pop(
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE
|
||||
)
|
||||
entry.add_update_listener(_async_config_entry_updated)
|
||||
|
||||
await hass.data[DATA_MQTT].async_connect()
|
||||
@ -438,6 +452,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_forward_entry_setup_and_setup_discovery(config_entry):
|
||||
"""Forward the config entry setup to the platforms and set up discovery."""
|
||||
reload_manual_setup: bool = False
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation, tag
|
||||
@ -460,8 +475,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await _async_setup_discovery(hass, conf, entry)
|
||||
# Setup reload service after all platforms have loaded
|
||||
await async_setup_reload_service()
|
||||
# When the entry is reloaded, also reload manual set up items to enable MQTT
|
||||
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||
hass.data.pop(DATA_MQTT_RELOAD_ENTRY)
|
||||
reload_manual_setup = True
|
||||
|
||||
# When the entry was disabled before, reload manual set up items to enable MQTT again
|
||||
if DATA_MQTT_RELOAD_NEEDED in hass.data:
|
||||
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
|
||||
reload_manual_setup = True
|
||||
|
||||
if reload_manual_setup:
|
||||
await async_reload_manual_mqtt_items(hass)
|
||||
|
||||
await async_forward_entry_setup_and_setup_discovery(entry)
|
||||
@ -613,8 +637,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
mqtt_client.cleanup()
|
||||
|
||||
# Trigger reload manual MQTT items at entry setup
|
||||
# Reload the legacy yaml platform
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False:
|
||||
# The entry is disabled reload legacy manual items when the entry is enabled again
|
||||
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
|
||||
@ -622,7 +644,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# The entry is reloaded:
|
||||
# Trigger re-fetching the yaml config at entry setup
|
||||
hass.data[DATA_MQTT_RELOAD_ENTRY] = True
|
||||
# Stop the loop
|
||||
# Reload the legacy yaml platform to make entities unavailable
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
# Cleanup entity registry hooks
|
||||
registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS
|
||||
]
|
||||
while registry_hooks:
|
||||
registry_hooks.popitem()[1]()
|
||||
# Wait for all ACKs and stop the loop
|
||||
await mqtt_client.async_disconnect()
|
||||
# Store remaining subscriptions to be able to restore or reload them
|
||||
# when the entry is set up again
|
||||
if mqtt_client.subscriptions:
|
||||
hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions
|
||||
|
||||
return True
|
||||
|
@ -309,7 +309,7 @@ class MQTT:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
hass,
|
||||
config_entry,
|
||||
conf,
|
||||
) -> None:
|
||||
@ -435,12 +435,13 @@ class MQTT:
|
||||
"""Return False if there are unprocessed ACKs."""
|
||||
return not bool(self._pending_operations)
|
||||
|
||||
# wait for ACK-s to be processesed (unsubscribe only)
|
||||
# wait for ACKs to be processed
|
||||
async with self._pending_operations_condition:
|
||||
await self._pending_operations_condition.wait_for(no_more_acks)
|
||||
|
||||
# stop the MQTT loop
|
||||
await self.hass.async_add_executor_job(stop)
|
||||
async with self._paho_lock:
|
||||
await self.hass.async_add_executor_job(stop)
|
||||
|
||||
async def async_subscribe(
|
||||
self,
|
||||
@ -501,7 +502,8 @@ class MQTT:
|
||||
async with self._paho_lock:
|
||||
mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic)
|
||||
await self._register_mid(mid)
|
||||
self.hass.async_create_task(self._wait_for_mid(mid))
|
||||
|
||||
self.hass.async_create_task(self._wait_for_mid(mid))
|
||||
|
||||
async def _async_perform_subscriptions(
|
||||
self, subscriptions: Iterable[tuple[str, int]]
|
||||
|
@ -32,6 +32,8 @@ CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
||||
DATA_MQTT = "mqtt"
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions"
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks"
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy"
|
||||
DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers"
|
||||
|
@ -249,7 +249,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid RGB color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid RGB color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@ -259,7 +261,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid XY color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid XY color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@ -269,12 +273,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid HS color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid HS color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
else:
|
||||
color_mode = values["color_mode"]
|
||||
if not self._supports_color_mode(color_mode):
|
||||
_LOGGER.warning("Invalid color mode received")
|
||||
_LOGGER.warning(
|
||||
"Invalid color mode received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
try:
|
||||
if color_mode == ColorMode.COLOR_TEMP:
|
||||
@ -314,7 +322,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._color_mode = ColorMode.XY
|
||||
self._xy = (x, y)
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.warning("Invalid or incomplete color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid or incomplete color value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
def _prepare_subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
@ -351,7 +362,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.warning("Invalid brightness value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid brightness value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
if (
|
||||
self._supported_features
|
||||
@ -366,7 +380,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color temp value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid color temp value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
if self._supported_features and LightEntityFeature.EFFECT:
|
||||
with suppress(KeyError):
|
||||
|
@ -28,7 +28,13 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, async_get_hass, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
HomeAssistant,
|
||||
async_get_hass,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@ -48,6 +54,7 @@ from homeassistant.helpers.entity import (
|
||||
async_generate_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.json import json_loads
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@ -64,7 +71,9 @@ from .const import (
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_RELOAD_ENTRY,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_PAYLOAD_AVAILABLE,
|
||||
@ -363,6 +372,12 @@ async def async_setup_platform_helper(
|
||||
async_setup_entities: SetupEntity,
|
||||
) -> None:
|
||||
"""Help to set up the platform for manual configured MQTT entities."""
|
||||
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||
_LOGGER.debug(
|
||||
"MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry",
|
||||
platform_domain,
|
||||
)
|
||||
return
|
||||
if not (entry_status := mqtt_config_entry_enabled(hass)):
|
||||
_LOGGER.warning(
|
||||
"MQTT integration is %s, skipping setup of manually configured MQTT %s",
|
||||
@ -647,6 +662,17 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di
|
||||
await async_publish(hass, discovery_topic, "", retain=True)
|
||||
|
||||
|
||||
async def async_clear_discovery_topic_if_entity_removed(
|
||||
hass: HomeAssistant,
|
||||
discovery_data: dict[str, Any],
|
||||
event: Event,
|
||||
) -> None:
|
||||
"""Clear the discovery topic if the entity is removed."""
|
||||
if event.data["action"] == "remove":
|
||||
# publish empty payload to config topic to avoid re-adding
|
||||
await async_remove_discovery_payload(hass, discovery_data)
|
||||
|
||||
|
||||
class MqttDiscoveryDeviceUpdate:
|
||||
"""Add support for auto discovery for platforms without an entity."""
|
||||
|
||||
@ -780,7 +806,8 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
discovery_data: dict,
|
||||
hass: HomeAssistant,
|
||||
discovery_data: dict | None,
|
||||
discovery_update: Callable | None = None,
|
||||
) -> None:
|
||||
"""Initialize the discovery update mixin."""
|
||||
@ -788,6 +815,14 @@ class MqttDiscoveryUpdate(Entity):
|
||||
self._discovery_update = discovery_update
|
||||
self._remove_discovery_updated: Callable | None = None
|
||||
self._removed_from_hass = False
|
||||
if discovery_data is None:
|
||||
return
|
||||
self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS
|
||||
]
|
||||
discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
|
||||
if discovery_hash in self._registry_hooks:
|
||||
self._registry_hooks.pop(discovery_hash)()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to discovery updates."""
|
||||
@ -850,7 +885,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
async def async_removed_from_registry(self) -> None:
|
||||
"""Clear retained discovery topic in broker."""
|
||||
if not self._removed_from_hass:
|
||||
if not self._removed_from_hass and self._discovery_data is not None:
|
||||
# Stop subscribing to discovery updates to not trigger when we clear the
|
||||
# discovery topic
|
||||
self._cleanup_discovery_on_remove()
|
||||
@ -861,7 +896,20 @@ class MqttDiscoveryUpdate(Entity):
|
||||
@callback
|
||||
def add_to_platform_abort(self) -> None:
|
||||
"""Abort adding an entity to a platform."""
|
||||
if self._discovery_data:
|
||||
if self._discovery_data is not None:
|
||||
discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH]
|
||||
if self.registry_entry is not None:
|
||||
self._registry_hooks[
|
||||
discovery_hash
|
||||
] = async_track_entity_registry_updated_event(
|
||||
self.hass,
|
||||
self.entity_id,
|
||||
partial(
|
||||
async_clear_discovery_topic_if_entity_removed,
|
||||
self.hass,
|
||||
self._discovery_data,
|
||||
),
|
||||
)
|
||||
stop_discovery_updates(self.hass, self._discovery_data)
|
||||
send_discovery_done(self.hass, self._discovery_data)
|
||||
super().add_to_platform_abort()
|
||||
@ -969,7 +1017,7 @@ class MqttEntity(
|
||||
# Initialize mixin classes
|
||||
MqttAttributes.__init__(self, config)
|
||||
MqttAvailability.__init__(self, config)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
|
||||
MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update)
|
||||
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
|
||||
|
||||
def _init_entity_id(self):
|
||||
|
@ -128,7 +128,7 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
self.pushover.send_message(
|
||||
self._user_key,
|
||||
message,
|
||||
kwargs.get(ATTR_TARGET),
|
||||
",".join(kwargs.get(ATTR_TARGET, [])),
|
||||
title,
|
||||
url,
|
||||
url_title,
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==2022.09.0"],
|
||||
"requirements": ["regenmaschine==2022.09.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "local_polling",
|
||||
"homekit": {
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "sensibo",
|
||||
"name": "Sensibo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
||||
"requirements": ["pysensibo==1.0.19"],
|
||||
"requirements": ["pysensibo==1.0.20"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
@ -84,11 +84,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, address.upper(), connectable
|
||||
)
|
||||
) or await switchbot.get_device(address)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Switchbot {sensor_type} with address {address}"
|
||||
)
|
||||
|
||||
await switchbot.close_stale_connections(ble_device)
|
||||
cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice)
|
||||
device = cls(
|
||||
device=ble_device,
|
||||
|
@ -69,13 +69,16 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
self.ble_device = service_info.device
|
||||
if adv := switchbot.parse_advertisement_data(
|
||||
service_info.device, service_info.advertisement
|
||||
):
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
if "modelName" in self.data:
|
||||
self._ready_event.set()
|
||||
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
||||
if not self.device.advertisement_changed(adv):
|
||||
return
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
self.device.update_from_advertisement(adv)
|
||||
super()._async_handle_bluetooth_event(service_info, change)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.18.27"],
|
||||
"requirements": ["PySwitchbot==0.19.5"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": [
|
||||
|
@ -71,14 +71,16 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Switchbot sensor based on a config entry."""
|
||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
entities = [
|
||||
SwitchBotSensor(
|
||||
coordinator,
|
||||
sensor,
|
||||
)
|
||||
for sensor in coordinator.data["data"]
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
]
|
||||
entities.append(SwitchbotRSSISensor(coordinator, "rssi"))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
@ -98,6 +100,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
self.entity_description = SENSOR_TYPES[sensor]
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
def native_value(self) -> str | int:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data["data"][self._sensor]
|
||||
|
||||
|
||||
class SwitchbotRSSISensor(SwitchBotSensor):
|
||||
"""Representation of a Switchbot RSSI sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.ble_device.rssi
|
||||
|
@ -30,13 +30,19 @@ OPTIONS_SCHEMA = vol.Schema(
|
||||
vol.Required(
|
||||
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_LOWER): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_UPPER): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@ -722,6 +722,9 @@ async def handle_supported_brands(
|
||||
for int_or_exc in ints_or_excs.values():
|
||||
if isinstance(int_or_exc, Exception):
|
||||
raise int_or_exc
|
||||
# Happens if a custom component without supported brands overrides a built-in one with supported brands
|
||||
if "supported_brands" not in int_or_exc.manifest:
|
||||
continue
|
||||
data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"]
|
||||
connection.send_result(msg["id"], data)
|
||||
|
||||
|
@ -39,6 +39,9 @@ BINARY_SENSOR_DESCRIPTIONS = {
|
||||
key=XiaomiBinarySensorDeviceClass.SMOKE,
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
),
|
||||
XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription(
|
||||
key=XiaomiBinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Yale Access Bluetooth",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"requirements": ["yalexs-ble==1.6.4"],
|
||||
"requirements": ["yalexs-ble==1.8.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [{ "manufacturer_id": 465 }],
|
||||
|
@ -8,8 +8,8 @@
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.79",
|
||||
"zigpy-deconz==0.18.0",
|
||||
"zigpy==0.50.2",
|
||||
"zigpy-deconz==0.18.1",
|
||||
"zigpy==0.50.3",
|
||||
"zigpy-xbee==0.15.0",
|
||||
"zigpy-zigate==0.9.2",
|
||||
"zigpy-znp==0.8.2"
|
||||
|
@ -12,7 +12,12 @@ from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import NodeStatus
|
||||
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress
|
||||
from zwave_js_server.model.firmware import (
|
||||
FirmwareUpdateFinished,
|
||||
FirmwareUpdateInfo,
|
||||
FirmwareUpdateProgress,
|
||||
FirmwareUpdateStatus,
|
||||
)
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@ -82,7 +87,10 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self._status_unsub: Callable[[], None] | None = None
|
||||
self._poll_unsub: Callable[[], None] | None = None
|
||||
self._progress_unsub: Callable[[], None] | None = None
|
||||
self._finished_unsub: Callable[[], None] | None = None
|
||||
self._num_files_installed: int = 0
|
||||
self._finished_event = asyncio.Event()
|
||||
self._finished_status: FirmwareUpdateStatus | None = None
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = "Firmware"
|
||||
@ -119,18 +127,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _reset_progress(self) -> None:
|
||||
"""Reset update install progress."""
|
||||
def _update_finished(self, event: dict[str, Any]) -> None:
|
||||
"""Update install progress on event."""
|
||||
finished: FirmwareUpdateFinished = event["firmware_update_finished"]
|
||||
self._finished_status = finished.status
|
||||
self._finished_event.set()
|
||||
|
||||
@callback
|
||||
def _unsub_firmware_events_and_reset_progress(
|
||||
self, write_state: bool = False
|
||||
) -> None:
|
||||
"""Unsubscribe from firmware events and reset update install progress."""
|
||||
if self._progress_unsub:
|
||||
self._progress_unsub()
|
||||
self._progress_unsub = None
|
||||
|
||||
if self._finished_unsub:
|
||||
self._finished_unsub()
|
||||
self._finished_unsub = None
|
||||
|
||||
self._finished_status = None
|
||||
self._finished_event.clear()
|
||||
self._num_files_installed = 0
|
||||
self._attr_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
self._attr_in_progress = 0
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
|
||||
"""Update the entity."""
|
||||
self._poll_unsub = None
|
||||
|
||||
# If device is asleep/dead, wait for it to wake up/become alive before
|
||||
# attempting an update
|
||||
for status, event_name in (
|
||||
(NodeStatus.ASLEEP, "wake up"),
|
||||
(NodeStatus.DEAD, "alive"),
|
||||
@ -187,19 +215,40 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
"""Install an update."""
|
||||
firmware = self._latest_version_firmware
|
||||
assert firmware
|
||||
self._attr_in_progress = 0
|
||||
self.async_write_ha_state()
|
||||
self._unsub_firmware_events_and_reset_progress(True)
|
||||
|
||||
self._progress_unsub = self.node.on(
|
||||
"firmware update progress", self._update_progress
|
||||
)
|
||||
self._finished_unsub = self.node.once(
|
||||
"firmware update finished", self._update_finished
|
||||
)
|
||||
|
||||
for file in firmware.files:
|
||||
try:
|
||||
await self.driver.controller.async_begin_ota_firmware_update(
|
||||
self.node, file
|
||||
)
|
||||
except BaseZwaveJSServerError as err:
|
||||
self._reset_progress()
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
raise HomeAssistantError(err) from err
|
||||
|
||||
# We need to block until we receive the `firmware update finished` event
|
||||
await self._finished_event.wait()
|
||||
assert self._finished_status is not None
|
||||
|
||||
# If status is not OK, we should throw an error to let the user know
|
||||
if self._finished_status not in (
|
||||
FirmwareUpdateStatus.OK_NO_RESTART,
|
||||
FirmwareUpdateStatus.OK_RESTART_PENDING,
|
||||
FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION,
|
||||
):
|
||||
status = self._finished_status
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
raise HomeAssistantError(status.name.replace("_", " ").title())
|
||||
|
||||
# If we get here, the firmware installation was successful and we need to
|
||||
# update progress accordingly
|
||||
self._num_files_installed += 1
|
||||
self._attr_in_progress = floor(
|
||||
100 * self._num_files_installed / len(firmware.files)
|
||||
@ -208,7 +257,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
|
||||
self._attr_installed_version = self._attr_latest_version = firmware.version
|
||||
self._latest_version_firmware = None
|
||||
self._reset_progress()
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""Poll a value."""
|
||||
@ -255,6 +304,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self._poll_unsub()
|
||||
self._poll_unsub = None
|
||||
|
||||
if self._progress_unsub:
|
||||
self._progress_unsub()
|
||||
self._progress_unsub = None
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
@ -454,6 +454,22 @@ class EntityPlatform:
|
||||
self.scan_interval,
|
||||
)
|
||||
|
||||
def _entity_id_already_exists(self, entity_id: str) -> tuple[bool, bool]:
|
||||
"""Check if an entity_id already exists.
|
||||
|
||||
Returns a tuple [already_exists, restored]
|
||||
"""
|
||||
already_exists = entity_id in self.entities
|
||||
restored = False
|
||||
|
||||
if not already_exists and not self.hass.states.async_available(entity_id):
|
||||
existing = self.hass.states.get(entity_id)
|
||||
if existing is not None and ATTR_RESTORED in existing.attributes:
|
||||
restored = True
|
||||
else:
|
||||
already_exists = True
|
||||
return (already_exists, restored)
|
||||
|
||||
async def _async_add_entity( # noqa: C901
|
||||
self,
|
||||
entity: Entity,
|
||||
@ -480,12 +496,31 @@ class EntityPlatform:
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
requested_entity_id = None
|
||||
suggested_object_id: str | None = None
|
||||
generate_new_entity_id = False
|
||||
|
||||
# Get entity_id from unique ID registration
|
||||
if entity.unique_id is not None:
|
||||
registered_entity_id = entity_registry.async_get_entity_id(
|
||||
self.domain, self.platform_name, entity.unique_id
|
||||
)
|
||||
if registered_entity_id:
|
||||
already_exists, _ = self._entity_id_already_exists(registered_entity_id)
|
||||
|
||||
if already_exists:
|
||||
# If there's a collision, the entry belongs to another entity
|
||||
entity.registry_entry = None
|
||||
msg = (
|
||||
f"Platform {self.platform_name} does not generate unique IDs. "
|
||||
)
|
||||
if entity.entity_id:
|
||||
msg += f"ID {entity.unique_id} is already used by {registered_entity_id} - ignoring {entity.entity_id}"
|
||||
else:
|
||||
msg += f"ID {entity.unique_id} already exists - ignoring {registered_entity_id}"
|
||||
self.logger.error(msg)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
if self.config_entry is not None:
|
||||
config_entry_id: str | None = self.config_entry.entry_id
|
||||
else:
|
||||
@ -541,7 +576,6 @@ class EntityPlatform:
|
||||
pass
|
||||
|
||||
if entity.entity_id is not None:
|
||||
requested_entity_id = entity.entity_id
|
||||
suggested_object_id = split_entity_id(entity.entity_id)[1]
|
||||
else:
|
||||
if device and entity.has_entity_name: # type: ignore[unreachable]
|
||||
@ -592,16 +626,6 @@ class EntityPlatform:
|
||||
entity.registry_entry = entry
|
||||
entity.entity_id = entry.entity_id
|
||||
|
||||
if entry.disabled:
|
||||
self.logger.debug(
|
||||
"Not adding entity %s because it's disabled",
|
||||
entry.name
|
||||
or entity.name
|
||||
or f'"{self.platform_name} {entity.unique_id}"',
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
# We won't generate an entity ID if the platform has already set one
|
||||
# We will however make sure that platform cannot pick a registered ID
|
||||
elif entity.entity_id is not None and entity_registry.async_is_registered(
|
||||
@ -628,28 +652,22 @@ class EntityPlatform:
|
||||
entity.add_to_platform_abort()
|
||||
raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}")
|
||||
|
||||
already_exists = entity.entity_id in self.entities
|
||||
restored = False
|
||||
|
||||
if not already_exists and not self.hass.states.async_available(
|
||||
entity.entity_id
|
||||
):
|
||||
existing = self.hass.states.get(entity.entity_id)
|
||||
if existing is not None and ATTR_RESTORED in existing.attributes:
|
||||
restored = True
|
||||
else:
|
||||
already_exists = True
|
||||
already_exists, restored = self._entity_id_already_exists(entity.entity_id)
|
||||
|
||||
if already_exists:
|
||||
if entity.unique_id is not None:
|
||||
msg = f"Platform {self.platform_name} does not generate unique IDs. "
|
||||
if requested_entity_id:
|
||||
msg += f"ID {entity.unique_id} is already used by {entity.entity_id} - ignoring {requested_entity_id}"
|
||||
else:
|
||||
msg += f"ID {entity.unique_id} already exists - ignoring {entity.entity_id}"
|
||||
else:
|
||||
msg = f"Entity id already exists - ignoring: {entity.entity_id}"
|
||||
self.logger.error(msg)
|
||||
self.logger.error(
|
||||
f"Entity id already exists - ignoring: {entity.entity_id}"
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
if entity.registry_entry and entity.registry_entry.disabled:
|
||||
self.logger.debug(
|
||||
"Not adding entity %s because it's disabled",
|
||||
entry.name
|
||||
or entity.name
|
||||
or f'"{self.platform_name} {entity.unique_id}"',
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
|
@ -11,7 +11,7 @@ attrs==21.2.0
|
||||
awesomeversion==22.8.0
|
||||
bcrypt==3.1.7
|
||||
bleak==0.16.0
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-adapters==0.4.1
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.9.1"
|
||||
version = "2022.9.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.18.27
|
||||
PySwitchbot==0.19.5
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@ -147,7 +147,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
aioecowitt==2022.09.1
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
@ -171,7 +171,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.5.2
|
||||
aiohomekit==1.5.6
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-adapters==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
@ -968,7 +968,7 @@ lakeside==0.12
|
||||
laundrify_aio==1.1.2
|
||||
|
||||
# homeassistant.components.led_ble
|
||||
led-ble==0.7.1
|
||||
led-ble==0.9.1
|
||||
|
||||
# homeassistant.components.foscam
|
||||
libpyfoscam==1.0
|
||||
@ -1838,7 +1838,7 @@ pysaj==0.0.16
|
||||
pysdcp==1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.19
|
||||
pysensibo==1.0.20
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@ -2118,7 +2118,7 @@ raincloudy==0.0.7
|
||||
raspyrfm-client==1.2.8
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2022.09.0
|
||||
regenmaschine==2022.09.1
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.11
|
||||
@ -2542,7 +2542,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.6.4
|
||||
yalexs-ble==1.8.1
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.1
|
||||
@ -2578,7 +2578,7 @@ zhong_hong_hvac==1.0.9
|
||||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.18.0
|
||||
zigpy-deconz==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.15.0
|
||||
@ -2590,7 +2590,7 @@ zigpy-zigate==0.9.2
|
||||
zigpy-znp==0.8.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.50.2
|
||||
zigpy==0.50.3
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.18.27
|
||||
PySwitchbot==0.19.5
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@ -134,7 +134,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
aioecowitt==2022.09.1
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
@ -155,7 +155,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.5.2
|
||||
aiohomekit==1.5.6
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -341,7 +341,7 @@ blinkpy==0.19.0
|
||||
bluemaestro-ble==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-adapters==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
@ -706,7 +706,7 @@ lacrosse-view==0.0.9
|
||||
laundrify_aio==1.1.2
|
||||
|
||||
# homeassistant.components.led_ble
|
||||
led-ble==0.7.1
|
||||
led-ble==0.9.1
|
||||
|
||||
# homeassistant.components.foscam
|
||||
libpyfoscam==1.0
|
||||
@ -1285,7 +1285,7 @@ pyruckus==0.16
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.19
|
||||
pysensibo==1.0.20
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@ -1451,7 +1451,7 @@ radios==0.1.1
|
||||
radiotherm==2.1.0
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2022.09.0
|
||||
regenmaschine==2022.09.1
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.11
|
||||
@ -1746,7 +1746,7 @@ xmltodict==0.13.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.6.4
|
||||
yalexs-ble==1.8.1
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.1
|
||||
@ -1767,7 +1767,7 @@ zeroconf==0.39.1
|
||||
zha-quirks==0.0.79
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.18.0
|
||||
zigpy-deconz==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.15.0
|
||||
@ -1779,7 +1779,7 @@ zigpy-zigate==0.9.2
|
||||
zigpy-znp==0.8.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.50.2
|
||||
zigpy==0.50.3
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.41.1
|
||||
|
@ -4,6 +4,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
import homeassistant.components.automation as automation
|
||||
from homeassistant.components.device_automation import DeviceAutomationType
|
||||
from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info
|
||||
@ -1425,7 +1426,24 @@ async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> Non
|
||||
|
||||
await help_test_unload_config_entry(hass, tmp_path, {})
|
||||
|
||||
# Fake short press 2
|
||||
# Rediscover message and fake short press 2 (non impact)
|
||||
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
|
||||
mqtt_entry = hass.config_entries.async_entries("mqtt")[0]
|
||||
|
||||
# Load the entry again
|
||||
new_yaml_config_file = tmp_path / "configuration.yaml"
|
||||
new_yaml_config_file.write_text("")
|
||||
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
|
||||
await mqtt_entry.async_setup(hass)
|
||||
|
||||
# Rediscover and fake short press 3
|
||||
async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press")
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""The tests for the MQTT discovery."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
@ -23,6 +24,8 @@ from homeassistant.const import (
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_common import help_test_unload_config_entry
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_capture_events,
|
||||
@ -1356,3 +1359,170 @@ async def test_mqtt_discovery_unsubscribe_once(
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#")
|
||||
|
||||
|
||||
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
|
||||
async def test_clear_config_topic_disabled_entity(
|
||||
hass, mqtt_mock_entry_no_yaml_config, device_reg, caplog
|
||||
):
|
||||
"""Test the discovery topic is removed when a disabled entity is removed."""
|
||||
mqtt_mock = await mqtt_mock_entry_no_yaml_config()
|
||||
# discover an entity that is not enabled by default
|
||||
config = {
|
||||
"name": "sbfspot_12345",
|
||||
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
|
||||
"unique_id": "sbfspot_12345",
|
||||
"enabled_by_default": False,
|
||||
"device": {
|
||||
"identifiers": ["sbfspot_12345"],
|
||||
"name": "sbfspot_12345",
|
||||
"sw_version": "1.0",
|
||||
"connections": [["mac", "12:34:56:AB:CD:EF"]],
|
||||
},
|
||||
}
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
|
||||
json.dumps(config),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
# discover an entity that is not unique (part 1), will be added
|
||||
config_not_unique1 = copy.deepcopy(config)
|
||||
config_not_unique1["name"] = "sbfspot_12345_1"
|
||||
config_not_unique1["unique_id"] = "not_unique"
|
||||
config_not_unique1.pop("enabled_by_default")
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config",
|
||||
json.dumps(config_not_unique1),
|
||||
)
|
||||
# discover an entity that is not unique (part 2), will not be added
|
||||
config_not_unique2 = copy.deepcopy(config_not_unique1)
|
||||
config_not_unique2["name"] = "sbfspot_12345_2"
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config",
|
||||
json.dumps(config_not_unique2),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "Platform mqtt does not generate unique IDs" in caplog.text
|
||||
|
||||
assert hass.states.get("sensor.sbfspot_12345") is None # disabled
|
||||
assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled
|
||||
assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique
|
||||
|
||||
# Verify device is created
|
||||
device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
|
||||
assert device_entry is not None
|
||||
|
||||
# Remove the device from the registry
|
||||
device_reg.async_remove_device(device_entry.id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert all valid discovery topics are cleared
|
||||
assert mqtt_mock.async_publish.call_count == 2
|
||||
assert (
|
||||
call("homeassistant/sensor/sbfspot_0/sbfspot_12345/config", "", 0, True)
|
||||
in mqtt_mock.async_publish.mock_calls
|
||||
)
|
||||
assert (
|
||||
call("homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", "", 0, True)
|
||||
in mqtt_mock.async_publish.mock_calls
|
||||
)
|
||||
|
||||
|
||||
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
|
||||
async def test_clean_up_registry_monitoring(
|
||||
hass, mqtt_mock_entry_no_yaml_config, device_reg, tmp_path
|
||||
):
|
||||
"""Test registry monitoring hook is removed after a reload."""
|
||||
await mqtt_mock_entry_no_yaml_config()
|
||||
hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS]
|
||||
# discover an entity that is not enabled by default
|
||||
config1 = {
|
||||
"name": "sbfspot_12345",
|
||||
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
|
||||
"unique_id": "sbfspot_12345",
|
||||
"enabled_by_default": False,
|
||||
"device": {
|
||||
"identifiers": ["sbfspot_12345"],
|
||||
"name": "sbfspot_12345",
|
||||
"sw_version": "1.0",
|
||||
"connections": [["mac", "12:34:56:AB:CD:EF"]],
|
||||
},
|
||||
}
|
||||
# Publish it config
|
||||
# Since it is not enabled_by_default the sensor will not be loaded
|
||||
# it should register a hook for monitoring the entiry registry
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
|
||||
json.dumps(config1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hooks) == 1
|
||||
|
||||
# Publish it again no new monitor should be started
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345/config",
|
||||
json.dumps(config1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hooks) == 1
|
||||
|
||||
# Verify device is created
|
||||
device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")})
|
||||
assert device_entry is not None
|
||||
|
||||
# Enload the entry
|
||||
# The monitoring should be cleared
|
||||
await help_test_unload_config_entry(hass, tmp_path, {})
|
||||
assert len(hooks) == 0
|
||||
|
||||
|
||||
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR])
|
||||
async def test_unique_id_collission_has_priority(
|
||||
hass, mqtt_mock_entry_no_yaml_config, entity_reg
|
||||
):
|
||||
"""Test tehe unique_id collision detection has priority over registry disabled items."""
|
||||
await mqtt_mock_entry_no_yaml_config()
|
||||
config = {
|
||||
"name": "sbfspot_12345",
|
||||
"state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/",
|
||||
"unique_id": "sbfspot_12345",
|
||||
"enabled_by_default": False,
|
||||
"device": {
|
||||
"identifiers": ["sbfspot_12345"],
|
||||
"name": "sbfspot_12345",
|
||||
"sw_version": "1.0",
|
||||
"connections": [["mac", "12:34:56:AB:CD:EF"]],
|
||||
},
|
||||
}
|
||||
# discover an entity that is not unique and disabled by default (part 1), will be added
|
||||
config_not_unique1 = copy.deepcopy(config)
|
||||
config_not_unique1["name"] = "sbfspot_12345_1"
|
||||
config_not_unique1["unique_id"] = "not_unique"
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config",
|
||||
json.dumps(config_not_unique1),
|
||||
)
|
||||
# discover an entity that is not unique (part 2), will not be added, and the registry entry is cleared
|
||||
config_not_unique2 = copy.deepcopy(config_not_unique1)
|
||||
config_not_unique2["name"] = "sbfspot_12345_2"
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config",
|
||||
json.dumps(config_not_unique2),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled
|
||||
assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique
|
||||
|
||||
# Verify the first entity is created
|
||||
assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None
|
||||
# Verify the second entity is not created because it is not unique
|
||||
assert entity_reg.async_get("sensor.sbfspot_12345_2") is None
|
||||
|
@ -1760,6 +1760,12 @@ async def test_validate_config_invalid(websocket_client, key, config, error):
|
||||
|
||||
async def test_supported_brands(hass, websocket_client):
|
||||
"""Test supported brands."""
|
||||
# Custom components without supported brands that override a built-in component with
|
||||
# supported brand will still be listed in HAS_SUPPORTED_BRANDS and should be ignored.
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule("override_without_brands"),
|
||||
)
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}),
|
||||
@ -1773,7 +1779,7 @@ async def test_supported_brands(hass, websocket_client):
|
||||
|
||||
with patch(
|
||||
"homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS",
|
||||
("abcd", "test"),
|
||||
("abcd", "test", "override_without_brands"),
|
||||
):
|
||||
await websocket_client.send_json({"id": 7, "type": "supported_brands"})
|
||||
msg = await websocket_client.receive_json()
|
||||
|
@ -52,3 +52,48 @@ async def test_smoke_sensor(hass):
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_moisture(hass):
|
||||
"""Make sure that formldehyde sensors are correctly mapped."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="C4:7C:8D:6A:3E:7A",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
saved_callback = None
|
||||
|
||||
def _async_register_callback(_hass, _callback, _matcher, _mode):
|
||||
nonlocal saved_callback
|
||||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
# WARNING: This test data is synthetic, rather than captured from a real device
|
||||
# obj type is 0x1014, payload len is 0x2 and payload is 0xf400
|
||||
saved_callback(
|
||||
make_advertisement(
|
||||
"C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x14\x10\x02\xf4\x00"
|
||||
),
|
||||
BluetoothChange.ADVERTISEMENT,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
sensor = hass.states.get("binary_sensor.smart_flower_pot_6a3e7a_moisture")
|
||||
sensor_attr = sensor.attributes
|
||||
assert sensor.state == "on"
|
||||
assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Moisture"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import serial.tools.list_ports
|
||||
from zigpy.backups import BackupManager
|
||||
import zigpy.config
|
||||
from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH
|
||||
from zigpy.exceptions import NetworkNotFormed
|
||||
@ -49,6 +50,7 @@ def disable_platform_only():
|
||||
def mock_app():
|
||||
"""Mock zigpy app interface."""
|
||||
mock_app = AsyncMock()
|
||||
mock_app.backups = create_autospec(BackupManager, instance=True)
|
||||
mock_app.backups.backups = []
|
||||
|
||||
with patch(
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Test the Z-Wave JS update entities."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.event import Event
|
||||
from zwave_js_server.exceptions import FailedZWaveCommand
|
||||
from zwave_js_server.model.firmware import FirmwareUpdateStatus
|
||||
|
||||
from homeassistant.components.update.const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
@ -51,7 +53,7 @@ FIRMWARE_UPDATES = {
|
||||
}
|
||||
|
||||
|
||||
async def test_update_entity_success(
|
||||
async def test_update_entity_states(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
@ -60,7 +62,7 @@ async def test_update_entity_success(
|
||||
caplog,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test update entity."""
|
||||
"""Test update entity states."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -137,39 +139,14 @@ async def test_update_entity_success(
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test successful install call without a version
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
args = client.async_send_command.call_args_list[0][0][0]
|
||||
assert args["command"] == "controller.begin_ota_firmware_update"
|
||||
assert (
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
assert args["update"] == {
|
||||
"target": 0,
|
||||
"url": "https://example2.com",
|
||||
"integrity": "sha2",
|
||||
}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_update_entity_install_failure(
|
||||
async def test_update_entity_install_raises(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
controller_node,
|
||||
integration,
|
||||
):
|
||||
"""Test update entity failed install."""
|
||||
"""Test update entity install raises exception."""
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
@ -287,11 +264,10 @@ async def test_update_entity_ha_not_running(
|
||||
assert args["nodeId"] == zen_31.node_id
|
||||
|
||||
|
||||
async def test_update_entity_failure(
|
||||
async def test_update_entity_update_failure(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
controller_node,
|
||||
integration,
|
||||
):
|
||||
"""Test update entity update failed."""
|
||||
@ -311,3 +287,169 @@ async def test_update_entity_failure(
|
||||
args["nodeId"]
|
||||
== climate_radio_thermostat_ct100_plus_different_endpoints.node_id
|
||||
)
|
||||
|
||||
|
||||
async def test_update_entity_progress(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
integration,
|
||||
):
|
||||
"""Test update entity progress."""
|
||||
node = climate_radio_thermostat_ct100_plus_different_endpoints
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
|
||||
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command.return_value = None
|
||||
|
||||
# Test successful install call without a version
|
||||
install_task = hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Sleep so that task starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
event = Event(
|
||||
type="firmware update progress",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "firmware update progress",
|
||||
"nodeId": node.node_id,
|
||||
"sentFragments": 1,
|
||||
"totalFragments": 20,
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
# Validate that the progress is updated
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_IN_PROGRESS] == 5
|
||||
|
||||
event = Event(
|
||||
type="firmware update finished",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "firmware update finished",
|
||||
"nodeId": node.node_id,
|
||||
"status": FirmwareUpdateStatus.OK_NO_RESTART,
|
||||
},
|
||||
)
|
||||
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Validate that progress is reset and entity reflects new version
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_IN_PROGRESS] is False
|
||||
assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4"
|
||||
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await install_task
|
||||
|
||||
|
||||
async def test_update_entity_install_failed(
|
||||
hass,
|
||||
client,
|
||||
climate_radio_thermostat_ct100_plus_different_endpoints,
|
||||
integration,
|
||||
caplog,
|
||||
):
|
||||
"""Test update entity install returns error status."""
|
||||
node = climate_radio_thermostat_ct100_plus_different_endpoints
|
||||
client.async_send_command.return_value = FIRMWARE_UPDATES
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
|
||||
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
client.async_send_command.return_value = None
|
||||
|
||||
async def call_install():
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{
|
||||
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test install call - we expect it to raise
|
||||
install_task = hass.async_create_task(call_install())
|
||||
|
||||
# Sleep so that task starts
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
event = Event(
|
||||
type="firmware update progress",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "firmware update progress",
|
||||
"nodeId": node.node_id,
|
||||
"sentFragments": 1,
|
||||
"totalFragments": 20,
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
# Validate that the progress is updated
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_IN_PROGRESS] == 5
|
||||
|
||||
event = Event(
|
||||
type="firmware update finished",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "firmware update finished",
|
||||
"nodeId": node.node_id,
|
||||
"status": FirmwareUpdateStatus.ERROR_TIMEOUT,
|
||||
},
|
||||
)
|
||||
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Validate that progress is reset and entity reflects old version
|
||||
state = hass.states.get(UPDATE_ENTITY)
|
||||
assert state
|
||||
attrs = state.attributes
|
||||
assert attrs[ATTR_IN_PROGRESS] is False
|
||||
assert attrs[ATTR_INSTALLED_VERSION] == "10.7"
|
||||
assert attrs[ATTR_LATEST_VERSION] == "11.2.4"
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# validate that the install task failed
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await install_task
|
||||
|
@ -438,13 +438,15 @@ async def test_async_remove_with_platform_update_finishes(hass):
|
||||
|
||||
|
||||
async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog):
|
||||
"""Test for not adding duplicate entities."""
|
||||
"""Test for not adding duplicate entities.
|
||||
|
||||
Also test that the entity registry is not updated for duplicates.
|
||||
"""
|
||||
caplog.set_level(logging.ERROR)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
await component.async_add_entities(
|
||||
[MockEntity(name="test1", unique_id="not_very_unique")]
|
||||
)
|
||||
ent1 = MockEntity(name="test1", unique_id="not_very_unique")
|
||||
await component.async_add_entities([ent1])
|
||||
|
||||
assert len(hass.states.async_entity_ids()) == 1
|
||||
assert not caplog.text
|
||||
@ -466,6 +468,11 @@ async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog):
|
||||
assert ent2.platform is None
|
||||
assert len(hass.states.async_entity_ids()) == 1
|
||||
|
||||
registry = er.async_get(hass)
|
||||
# test the entity name was not updated
|
||||
entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique")
|
||||
assert entry.original_name == "test1"
|
||||
|
||||
|
||||
async def test_using_prescribed_entity_id(hass):
|
||||
"""Test for using predefined entity ID."""
|
||||
@ -577,6 +584,28 @@ async def test_registry_respect_entity_disabled(hass):
|
||||
assert hass.states.async_entity_ids() == []
|
||||
|
||||
|
||||
async def test_unique_id_conflict_has_priority_over_disabled_entity(hass, caplog):
|
||||
"""Test that an entity that is not unique has priority over a disabled entity."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
entity1 = MockEntity(
|
||||
name="test1", unique_id="not_very_unique", enabled_by_default=False
|
||||
)
|
||||
entity2 = MockEntity(
|
||||
name="test2", unique_id="not_very_unique", enabled_by_default=False
|
||||
)
|
||||
await component.async_add_entities([entity1])
|
||||
await component.async_add_entities([entity2])
|
||||
|
||||
assert len(hass.states.async_entity_ids()) == 1
|
||||
assert "Platform test_domain does not generate unique IDs." in caplog.text
|
||||
assert entity1.registry_entry is not None
|
||||
assert entity2.registry_entry is None
|
||||
registry = er.async_get(hass)
|
||||
# test the entity name was not updated
|
||||
entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique")
|
||||
assert entry.original_name == "test1"
|
||||
|
||||
|
||||
async def test_entity_registry_updates_name(hass):
|
||||
"""Test that updates on the entity registry update platform entities."""
|
||||
registry = mock_registry(
|
||||
|
Loading…
x
Reference in New Issue
Block a user