This commit is contained in:
Paulus Schoutsen 2022-09-11 13:28:28 -04:00 committed by GitHub
commit 2bd71f62ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 784 additions and 164 deletions

View File

@ -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%]"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"],

View File

@ -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)

View File

@ -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
},
}

View File

@ -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"
}

View File

@ -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

View File

@ -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"],

View File

@ -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,

View File

@ -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)

View File

@ -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(

View File

@ -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}"

View File

@ -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()

View File

@ -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": [

View File

@ -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)

View File

@ -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

View File

@ -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]]

View File

@ -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"

View File

@ -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):

View File

@ -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):

View File

@ -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,

View File

@ -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": {

View File

@ -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",

View File

@ -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,

View File

@ -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)

View File

@ -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": [

View File

@ -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

View File

@ -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
),
),
}
)

View File

@ -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)

View File

@ -39,6 +39,9 @@ BINARY_SENSOR_DESCRIPTIONS = {
key=XiaomiBinarySensorDeviceClass.SMOKE,
device_class=BinarySensorDeviceClass.SMOKE,
),
XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription(
key=XiaomiBinarySensorDeviceClass.MOISTURE,
),
}

View File

@ -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 }],

View File

@ -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"

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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(

View File

@ -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

View File

@ -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(