This commit is contained in:
Paulus Schoutsen 2022-09-08 21:58:18 -04:00 committed by GitHub
commit 0a7f3f6ced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 262 additions and 63 deletions

View File

@ -6,8 +6,8 @@
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"bleak==0.16.0", "bleak==0.16.0",
"bluetooth-adapters==0.3.4", "bluetooth-adapters==0.3.5",
"bluetooth-auto-recovery==0.3.1" "bluetooth-auto-recovery==0.3.2"
], ],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"config_flow": true, "config_flow": true,

View File

@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
class EcobeeSensorEntityDescriptionMixin: class EcobeeSensorEntityDescriptionMixin:
"""Represent the required ecobee entity description attributes.""" """Represent the required ecobee entity description attributes."""
runtime_key: str runtime_key: str | None
@dataclass @dataclass
@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=TEMP_FAHRENHEIT, native_unit_of_measurement=TEMP_FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualTemperature", runtime_key=None,
), ),
EcobeeSensorEntityDescription( EcobeeSensorEntityDescription(
key="humidity", key="humidity",
@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualHumidity", runtime_key=None,
), ),
EcobeeSensorEntityDescription( EcobeeSensorEntityDescription(
key="co2PPM", key="co2PPM",
@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity):
for item in sensor["capability"]: for item in sensor["capability"]:
if item["type"] != self.entity_description.key: if item["type"] != self.entity_description.key:
continue continue
thermostat = self.data.ecobee.get_thermostat(self.index) if self.entity_description.runtime_key is None:
self._state = thermostat["runtime"][self.entity_description.runtime_key] self._state = item["value"]
else:
thermostat = self.data.ecobee.get_thermostat(self.index)
self._state = thermostat["runtime"][
self.entity_description.runtime_key
]
break break

View File

@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator
async def async_get_service( async def async_get_service(
@ -31,8 +32,10 @@ async def async_get_service(
"""Get the LaMetric notification service.""" """Get the LaMetric notification service."""
if discovery_info is None: if discovery_info is None:
return None return None
lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]] coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
return LaMetricNotificationService(lametric) discovery_info["entry_id"]
]
return LaMetricNotificationService(coordinator.lametric)
class LaMetricNotificationService(BaseNotificationService): class LaMetricNotificationService(BaseNotificationService):

View File

@ -3,7 +3,7 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.8.2"], "requirements": ["pylitterbot==2022.9.1"],
"codeowners": ["@natekspencer", "@tkdrob"], "codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }], "dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
async def _is_owm_api_online(hass, api_key, lat, lon): async def _is_owm_api_online(hass, api_key, lat, lon):
owm = OWM(api_key).weather_manager() owm = OWM(api_key).weather_manager()
return await hass.async_add_executor_job(owm.one_call, lat, lon) return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon)

View File

@ -373,7 +373,7 @@ class Luminary(LightEntity):
self._max_mireds = color_util.color_temperature_kelvin_to_mired( self._max_mireds = color_util.color_temperature_kelvin_to_mired(
self._luminary.min_temp() or DEFAULT_KELVIN self._luminary.min_temp() or DEFAULT_KELVIN
) )
if len(self._attr_supported_color_modes == 1): if len(self._attr_supported_color_modes) == 1:
# The light supports only a single color mode # The light supports only a single color mode
self._attr_color_mode = list(self._attr_supported_color_modes)[0] self._attr_color_mode = list(self._attr_supported_color_modes)[0]
@ -392,7 +392,7 @@ class Luminary(LightEntity):
if ColorMode.HS in self._attr_supported_color_modes: if ColorMode.HS in self._attr_supported_color_modes:
self._rgb_color = self._luminary.rgb() self._rgb_color = self._luminary.rgb()
if len(self._attr_supported_color_modes > 1): if len(self._attr_supported_color_modes) > 1:
# The light supports hs + color temp, determine which one it is # The light supports hs + color temp, determine which one it is
if self._rgb_color == (0, 0, 0): if self._rgb_color == (0, 0, 0):
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP

View File

@ -9,7 +9,7 @@ from typing import Any
from regenmaschine import Client from regenmaschine import Client
from regenmaschine.controller import Controller from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError from regenmaschine.errors import RainMachineError, UnknownAPICallError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@ -190,7 +190,9 @@ async def async_update_programs_and_zones(
) )
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Set up RainMachine as config entry.""" """Set up RainMachine as config entry."""
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession) client = Client(session=websession)
@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data = await controller.restrictions.universal() data = await controller.restrictions.universal()
else: else:
data = await controller.zones.all(details=True, include_inactive=True) data = await controller.zones.all(details=True, include_inactive=True)
except UnknownAPICallError:
LOGGER.info(
"Skipping unsupported API call for controller %s: %s",
controller.name,
api_category,
)
except RainMachineError as err: except RainMachineError as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err

View File

@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update the state.""" """Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR: if self.entity_description.key == TYPE_FLOW_SENSOR:
self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor") self._attr_is_on = self.coordinator.data.get("system", {}).get(
"useFlowSensor"
)
class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):

View File

@ -3,7 +3,7 @@
"name": "RainMachine", "name": "RainMachine",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine", "documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.08.0"], "requirements": ["regenmaschine==2022.09.0"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "local_polling", "iot_class": "local_polling",
"homekit": { "homekit": {

View File

@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update the state.""" """Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3: if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
self._attr_native_value = self.coordinator.data["system"].get( self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter" "flowSensorClicksPerCubicMeter"
) )
elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS: elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks") clicks = self.coordinator.data.get("system", {}).get(
clicks_per_m3 = self.coordinator.data["system"].get( "flowSensorWateringClicks"
)
clicks_per_m3 = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter" "flowSensorClicksPerCubicMeter"
) )
@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
else: else:
self._attr_native_value = None self._attr_native_value = None
elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX: elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
self._attr_native_value = self.coordinator.data["system"].get( self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorStartIndex" "flowSensorStartIndex"
) )
elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS: elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
self._attr_native_value = self.coordinator.data["system"].get( self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorWateringClicks" "flowSensorWateringClicks"
) )

View File

@ -2,7 +2,7 @@
"domain": "velbus", "domain": "velbus",
"name": "Velbus", "name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus", "documentation": "https://www.home-assistant.io/integrations/velbus",
"requirements": ["velbus-aio==2022.6.2"], "requirements": ["velbus-aio==2022.9.1"],
"config_flow": true, "config_flow": true,
"codeowners": ["@Cereal2nd", "@brefra"], "codeowners": ["@Cereal2nd", "@brefra"],
"dependencies": ["usb"], "dependencies": ["usb"],

View File

@ -313,19 +313,24 @@ class ControllerEvents:
node, node,
) )
LOGGER.debug("Node added: %s", node.node_id)
# Listen for ready node events, both new and re-interview.
self.config_entry.async_on_unload(
node.on(
"ready",
lambda event: self.hass.async_create_task(
self.node_events.async_on_node_ready(event["node"])
),
)
)
# we only want to run discovery when the node has reached ready state, # we only want to run discovery when the node has reached ready state,
# otherwise we'll have all kinds of missing info issues. # otherwise we'll have all kinds of missing info issues.
if node.ready: if node.ready:
await self.node_events.async_on_node_ready(node) await self.node_events.async_on_node_ready(node)
return return
# if node is not yet ready, register one-time callback for ready state
LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id)
node.once(
"ready",
lambda event: self.hass.async_create_task(
self.node_events.async_on_node_ready(event["node"])
),
)
# we do submit the node to device registry so user has # we do submit the node to device registry so user has
# some visual feedback that something is (in the process of) being added # some visual feedback that something is (in the process of) being added
self.register_node_in_dev_reg(node) self.register_node_in_dev_reg(node)
@ -414,12 +419,25 @@ class NodeEvents:
async def async_on_node_ready(self, node: ZwaveNode) -> None: async def async_on_node_ready(self, node: ZwaveNode) -> None:
"""Handle node ready event.""" """Handle node ready event."""
LOGGER.debug("Processing node %s", node) LOGGER.debug("Processing node %s", node)
driver = self.controller_events.driver_events.driver
# register (or update) node in device registry # register (or update) node in device registry
device = self.controller_events.register_node_in_dev_reg(node) device = self.controller_events.register_node_in_dev_reg(node)
# We only want to create the defaultdict once, even on reinterviews # We only want to create the defaultdict once, even on reinterviews
if device.id not in self.controller_events.registered_unique_ids: if device.id not in self.controller_events.registered_unique_ids:
self.controller_events.registered_unique_ids[device.id] = defaultdict(set) self.controller_events.registered_unique_ids[device.id] = defaultdict(set)
# Remove any old value ids if this is a reinterview.
self.controller_events.discovered_value_ids.pop(device.id, None)
# Remove stale entities that may exist from a previous interview.
async_dispatcher_send(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(driver, node)}_"
"remove_entity_on_ready_node"
),
)
value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {}
# run discovery on all node values and create/update entities # run discovery on all node values and create/update entities

View File

@ -792,7 +792,9 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key,
CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key,
CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL],
CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE], CONF_ADDON_EMULATE_HARDWARE: user_input.get(
CONF_EMULATE_HARDWARE, False
),
} }
if new_addon_config != addon_config: if new_addon_config != addon_config:

View File

@ -123,5 +123,5 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing"
# This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for
# your own (https://github.com/zwave-js/firmware-updates/). # your own (https://github.com/zwave-js/firmware-updates/).
API_KEY_FIRMWARE_UPDATE_SERVICE = ( API_KEY_FIRMWARE_UPDATE_SERVICE = (
"55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" "2e39d98fc56386389fbb35e5a98fa1b44b9fdd8f971460303587cff408430d4cfcde6134"
) )

View File

@ -123,6 +123,7 @@ def get_device_entities(
"entity_category": entry.entity_category, "entity_category": entry.entity_category,
"supported_features": entry.supported_features, "supported_features": entry.supported_features,
"unit_of_measurement": entry.unit_of_measurement, "unit_of_measurement": entry.unit_of_measurement,
"value_id": value_id,
"primary_value": primary_value_data, "primary_value": primary_value_data,
} }
entities.append(entity) entities.append(entity)

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id, get_unique_id from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
EVENT_VALUE_UPDATED = "value updated" EVENT_VALUE_UPDATED = "value updated"
EVENT_VALUE_REMOVED = "value removed" EVENT_VALUE_REMOVED = "value removed"
@ -96,6 +96,17 @@ class ZWaveBaseEntity(Entity):
self.async_on_remove( self.async_on_remove(
self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed) self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed)
) )
self.async_on_remove(
async_dispatcher_connect(
self.hass,
(
f"{DOMAIN}_"
f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
"remove_entity_on_ready_node"
),
self.async_remove,
)
)
for status_event in (EVENT_ALIVE, EVENT_DEAD): for status_event in (EVENT_ALIVE, EVENT_DEAD):
self.async_on_remove( self.async_on_remove(

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
from math import floor
from typing import Any from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -11,7 +12,7 @@ from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import NodeStatus from zwave_js_server.const import NodeStatus
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
from zwave_js_server.model.driver import Driver from zwave_js_server.model.driver import Driver
from zwave_js_server.model.firmware import FirmwareUpdateInfo from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress
from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.node import Node as ZwaveNode
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
@ -63,7 +64,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = ( _attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES UpdateEntityFeature.INSTALL
| UpdateEntityFeature.RELEASE_NOTES
| UpdateEntityFeature.PROGRESS
) )
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False _attr_should_poll = False
@ -78,6 +81,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._latest_version_firmware: FirmwareUpdateInfo | None = None self._latest_version_firmware: FirmwareUpdateInfo | None = None
self._status_unsub: Callable[[], None] | None = None self._status_unsub: Callable[[], None] | None = None
self._poll_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None
self._progress_unsub: Callable[[], None] | None = None
self._num_files_installed: int = 0
# Entity class attributes # Entity class attributes
self._attr_name = "Firmware" self._attr_name = "Firmware"
@ -93,6 +98,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
self._status_unsub = None self._status_unsub = None
self.hass.async_create_task(self._async_update()) self.hass.async_create_task(self._async_update())
@callback
def _update_progress(self, event: dict[str, Any]) -> None:
"""Update install progress on event."""
progress: FirmwareUpdateProgress = event["firmware_update_progress"]
if not self._latest_version_firmware:
return
# We will assume that each file in the firmware update represents an equal
# percentage of the overall progress. This is likely not true because each file
# may be a different size, but it's the best we can do since we don't know the
# total number of fragments across all files.
self._attr_in_progress = floor(
100
* (
self._num_files_installed
+ (progress.sent_fragments / progress.total_fragments)
)
/ len(self._latest_version_firmware.files)
)
self.async_write_ha_state()
@callback
def _reset_progress(self) -> None:
"""Reset update install progress."""
if self._progress_unsub:
self._progress_unsub()
self._progress_unsub = None
self._num_files_installed = 0
self._attr_in_progress = False
self.async_write_ha_state()
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
"""Update the entity.""" """Update the entity."""
self._poll_unsub = None self._poll_unsub = None
@ -152,18 +187,29 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
"""Install an update.""" """Install an update."""
firmware = self._latest_version_firmware firmware = self._latest_version_firmware
assert firmware assert firmware
try: self._attr_in_progress = 0
for file in firmware.files: self.async_write_ha_state()
self._progress_unsub = self.node.on(
"firmware update progress", self._update_progress
)
for file in firmware.files:
try:
await self.driver.controller.async_begin_ota_firmware_update( await self.driver.controller.async_begin_ota_firmware_update(
self.node, file self.node, file
) )
except BaseZwaveJSServerError as err: except BaseZwaveJSServerError as err:
raise HomeAssistantError(err) from err self._reset_progress()
else: raise HomeAssistantError(err) from err
self._attr_installed_version = self._attr_latest_version = firmware.version self._num_files_installed += 1
self._latest_version_firmware = None self._attr_in_progress = floor(
100 * self._num_files_installed / len(firmware.files)
)
self.async_write_ha_state() self.async_write_ha_state()
self._attr_installed_version = self._attr_latest_version = firmware.version
self._latest_version_firmware = None
self._reset_progress()
async def async_poll_value(self, _: bool) -> None: async def async_poll_value(self, _: bool) -> None:
"""Poll a value.""" """Poll a value."""
LOGGER.error( LOGGER.error(
@ -189,6 +235,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
) )
) )
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_ready_node",
self.async_remove,
)
)
self.async_on_remove(async_at_start(self.hass, self._async_update)) self.async_on_remove(async_at_start(self.hass, self._async_update))
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
@ -200,3 +254,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
if self._poll_unsub: if self._poll_unsub:
self._poll_unsub() self._poll_unsub()
self._poll_unsub = None self._poll_unsub = None
if self._progress_unsub:
self._progress_unsub()
self._progress_unsub = None

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 9 MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -11,8 +11,8 @@ attrs==21.2.0
awesomeversion==22.8.0 awesomeversion==22.8.0
bcrypt==3.1.7 bcrypt==3.1.7
bleak==0.16.0 bleak==0.16.0
bluetooth-adapters==0.3.4 bluetooth-adapters==0.3.5
bluetooth-auto-recovery==0.3.1 bluetooth-auto-recovery==0.3.2
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0
cryptography==37.0.4 cryptography==37.0.4

View File

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

View File

@ -430,10 +430,10 @@ bluemaestro-ble==0.2.0
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.3.4 bluetooth-adapters==0.3.5
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.1 bluetooth-auto-recovery==0.3.2
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.1.22 bond-async==0.1.22
@ -1668,7 +1668,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.8.2 pylitterbot==2022.9.1
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1 pylutron-caseta==0.13.1
@ -2118,7 +2118,7 @@ raincloudy==0.0.7
raspyrfm-client==1.2.8 raspyrfm-client==1.2.8
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2022.08.0 regenmaschine==2022.09.0
# homeassistant.components.renault # homeassistant.components.renault
renault-api==0.1.11 renault-api==0.1.11
@ -2449,7 +2449,7 @@ vallox-websocket-api==2.12.0
vehicle==0.4.0 vehicle==0.4.0
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2022.6.2 velbus-aio==2022.9.1
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.18 venstarcolortouch==0.18

View File

@ -341,10 +341,10 @@ blinkpy==0.19.0
bluemaestro-ble==0.2.0 bluemaestro-ble==0.2.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.3.4 bluetooth-adapters==0.3.5
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.1 bluetooth-auto-recovery==0.3.2
# homeassistant.components.bond # homeassistant.components.bond
bond-async==0.1.22 bond-async==0.1.22
@ -1166,7 +1166,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.8.2 pylitterbot==2022.9.1
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1 pylutron-caseta==0.13.1
@ -1451,7 +1451,7 @@ radios==0.1.1
radiotherm==2.1.0 radiotherm==2.1.0
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==2022.08.0 regenmaschine==2022.09.0
# homeassistant.components.renault # homeassistant.components.renault
renault-api==0.1.11 renault-api==0.1.11
@ -1677,7 +1677,7 @@ vallox-websocket-api==2.12.0
vehicle==0.4.0 vehicle==0.4.0
# homeassistant.components.velbus # homeassistant.components.velbus
velbus-aio==2022.6.2 velbus-aio==2022.9.1
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.18 venstarcolortouch==0.18

View File

@ -17,13 +17,13 @@ from tests.common import MockConfigEntry
def create_mock_robot( def create_mock_robot(
robot_data: dict | None = None, side_effect: Any | None = None robot_data: dict | None, account: Account, side_effect: Any | None = None
) -> Robot: ) -> Robot:
"""Create a mock Litter-Robot device.""" """Create a mock Litter-Robot device."""
if not robot_data: if not robot_data:
robot_data = {} robot_data = {}
robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}) robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account)
robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.start_cleaning = AsyncMock(side_effect=side_effect)
robot.set_power_status = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect)
robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect)
@ -44,7 +44,9 @@ def create_mock_account(
account = MagicMock(spec=Account) account = MagicMock(spec=Account)
account.connect = AsyncMock() account.connect = AsyncMock()
account.refresh_robots = AsyncMock() account.refresh_robots = AsyncMock()
account.robots = [] if skip_robots else [create_mock_robot(robot_data, side_effect)] account.robots = (
[] if skip_robots else [create_mock_robot(robot_data, account, side_effect)]
)
return account return account

View File

@ -208,6 +208,8 @@ def _create_mocked_owm(is_api_online: bool):
mocked_owm.one_call.return_value = one_call mocked_owm.one_call.return_value = one_call
mocked_owm.weather_manager.return_value.one_call.return_value = is_api_online mocked_owm.weather_manager.return_value.weather_at_coords.return_value = (
is_api_online
)
return mocked_owm return mocked_owm

View File

@ -1951,6 +1951,30 @@ async def different_device_server_version(*args):
0, 0,
different_device_server_version, different_device_server_version,
), ),
(
{"config": ADDON_DISCOVERY_INFO},
{},
{
"device": "/test",
"network_key": "old123",
"s0_legacy_key": "old123",
"s2_access_control_key": "old456",
"s2_authenticated_key": "old789",
"s2_unauthenticated_key": "old987",
"log_level": "info",
},
{
"usb_path": "/new",
"s0_legacy_key": "new123",
"s2_access_control_key": "new456",
"s2_authenticated_key": "new789",
"s2_unauthenticated_key": "new987",
"log_level": "info",
"emulate_hardware": False,
},
0,
different_device_server_version,
),
], ],
) )
async def test_options_different_device( async def test_options_different_device(
@ -2018,14 +2042,16 @@ async def test_options_different_device(
result = await hass.config_entries.options.async_configure(result["flow_id"]) result = await hass.config_entries.options.async_configure(result["flow_id"])
await hass.async_block_till_done() await hass.async_block_till_done()
# Default emulate_hardware is False.
addon_options = {"emulate_hardware": False} | old_addon_options
# Legacy network key is not reset. # Legacy network key is not reset.
old_addon_options.pop("network_key") addon_options.pop("network_key")
assert set_addon_options.call_count == 2 assert set_addon_options.call_count == 2
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, hass,
"core_zwave_js", "core_zwave_js",
{"options": old_addon_options}, {"options": addon_options},
) )
assert result["type"] == "progress" assert result["type"] == "progress"
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"

View File

@ -152,6 +152,7 @@ async def test_device_diagnostics_missing_primary_value(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
) )
assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] == { assert air_entity["primary_value"] == {
"command_class": value.command_class, "command_class": value.command_class,
"command_class_name": value.command_class_name, "command_class_name": value.command_class_name,
@ -189,4 +190,5 @@ async def test_device_diagnostics_missing_primary_value(
x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id
) )
assert air_entity["value_id"] == value.value_id
assert air_entity["primary_value"] is None assert air_entity["primary_value"] is None

View File

@ -3,6 +3,7 @@ from copy import deepcopy
from unittest.mock import call, patch from unittest.mock import call, patch
import pytest import pytest
from zwave_js_server.client import Client
from zwave_js_server.event import Event from zwave_js_server.event import Event
from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion
from zwave_js_server.model.node import Node from zwave_js_server.model.node import Node
@ -12,6 +13,7 @@ from homeassistant.components.zwave_js.const import DOMAIN
from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import ( from homeassistant.helpers import (
area_registry as ar, area_registry as ar,
device_registry as dr, device_registry as dr,
@ -242,6 +244,61 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration):
) )
async def test_existing_node_reinterview(
hass: HomeAssistant,
client: Client,
multisensor_6_state: dict,
multisensor_6: Node,
integration: MockConfigEntry,
) -> None:
"""Test we handle a node re-interview firing a node ready event."""
dev_reg = dr.async_get(hass)
node = multisensor_6
assert client.driver is not None
air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}"
air_temperature_device_id_ext = (
f"{air_temperature_device_id}-{node.manufacturer_id}:"
f"{node.product_type}:{node.product_id}"
)
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
assert state # entity and device added
assert state.state != STATE_UNAVAILABLE
device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
assert device
assert device == dev_reg.async_get_device(
identifiers={(DOMAIN, air_temperature_device_id_ext)}
)
assert device.sw_version == "1.12"
node_state = deepcopy(multisensor_6_state)
node_state["firmwareVersion"] = "1.13"
event = Event(
type="ready",
data={
"source": "node",
"event": "ready",
"nodeId": node.node_id,
"nodeState": node_state,
},
)
client.driver.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
assert state
assert state.state != STATE_UNAVAILABLE
device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)})
assert device
assert device == dev_reg.async_get_device(
identifiers={(DOMAIN, air_temperature_device_id_ext)}
)
assert device.sw_version == "1.13"
async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration): async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration):
"""Test we handle a non-ready node that exists during integration setup.""" """Test we handle a non-ready node that exists during integration setup."""
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)