Compare commits

..

68 Commits

Author SHA1 Message Date
Paulus Schoutsen a5f86bff45 2023.6.1 (#94288) 2023-06-08 14:57:37 -04:00
Paulus Schoutsen d991970754 Bumped version to 2023.6.1 2023-06-08 13:39:33 -04:00
Tom Harris d745b44180 Fix Insteon startup for users with X10 devices (#94277) 2023-06-08 13:39:10 -04:00
Tom Harris 602fcd6b1b Restructure Insteon start-up (#92818)
* Restructure startup

* Code review

* Further typing

* Fix circular import
2023-06-08 13:39:09 -04:00
Franck Nijhof b39b0a960e Fix repair issue about no yaml for config entries (#94271) 2023-06-08 13:35:08 -04:00
Paulus Schoutsen 40bb796f03 Fix default value when logger used (#94269) 2023-06-08 13:28:54 -04:00
Christopher Bailey 2801ba6cad Bump unifiprotect to 4.10.2 (#94263) 2023-06-08 13:28:52 -04:00
Paul Bottein 5da0ef36ea Update frontend to 20230608.0 (#94256) 2023-06-08 13:28:51 -04:00
Joost Lekkerkerker d861292900 Retrieve friends in an async manner in Lastfm (#94255) 2023-06-08 13:28:50 -04:00
Jc2k a3fda43c64 Bump aiohomekit to 2.6.5 (fixes python 3.11 regression) (#94245) 2023-06-08 13:28:49 -04:00
Joost Lekkerkerker 8705a26a1a Catch exception when user has no lastfm friends (#94235) 2023-06-08 13:28:48 -04:00
jan iversen 2b1c45c28c Solve wrong return code from modbus. (#94234) 2023-06-08 13:28:47 -04:00
Jan Bouwhuis 0cf3825183 Fix imap crash on email without subject (#94230) 2023-06-08 13:28:46 -04:00
Kostas Chatzikokolakis 413e1c97d7 Bump pulsectl to 23.5.2 (#94227) 2023-06-08 13:28:45 -04:00
Joost Lekkerkerker 3b27a3aabf Bump python-opensky to 0.0.9 (#94224) 2023-06-08 13:28:44 -04:00
Joost Lekkerkerker 4509e13ceb Bump python-opensky (#93916) 2023-06-08 13:28:43 -04:00
Álvaro Fernández Rojas 33bf8c600b Update aioairzone-cloud to v0.1.8 (#94223) 2023-06-08 13:27:46 -04:00
Jan-Philipp Benecke b508875f17 Set httpx log level to warning (#94217)
Set log level of httpx to warning
2023-06-08 13:27:45 -04:00
Allen Porter ac963a2b6e Require pydantic 1.10.8 or higher (#94208)
* Requied pydantic 1.10.9 or higher

* Simplify constraint to 2.0

* Drop constraint by one patch release to 1.10.8 or higher

* Add package constraints to gen requirements script
2023-06-08 13:27:43 -04:00
James Connor 13029cf26f Fix ambiclimate for Python 3.11 (#94203)
Fix ambiclimate python 3.11 break
2023-06-08 13:27:42 -04:00
Paulus Schoutsen f39a6b96ff Rename Local Media to My Media (#94201)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-06-08 13:27:41 -04:00
Mick Vleeshouwer c6a17d6832 Bump pyoverkiz to 1.8.0 (#94176) 2023-06-08 13:27:40 -04:00
Joost Lekkerkerker 74c0552a12 Fix Abode unit of measurement (#94168)
Change unit of measurement to HA const
2023-06-08 13:27:38 -04:00
Justin Vanderhooft f24b514c9a Bump melnor-bluetooth to fix a timezone issue (#94159) 2023-06-08 13:27:37 -04:00
Erik Montnemery e1c47fdb61 Fix OTBR reset (#94157) 2023-06-08 13:27:36 -04:00
j4n-e4t 93baf24394 Add error handling to input_select integration (#93940) 2023-06-08 12:27:23 -04:00
Franck Nijhof a4e236d0b9 2023.6.0 (#94158) 2023-06-07 15:39:47 +02:00
Franck Nijhof 421fa5b035 Bumped version to 2023.6.0 2023-06-07 13:49:03 +02:00
Erik Montnemery 3d3fecbd23 Disable google assistant local control of climate entities (#94153) 2023-06-07 13:48:20 +02:00
Erik Montnemery 468be632fd Add debug logs to cloud migration (#94151) 2023-06-07 13:48:17 +02:00
Bram Kragten 74ccdcda68 Update frontend to 20230607.0 (#94150) 2023-06-07 13:48:14 +02:00
Erik Montnemery 5cc61acfb2 Fix migration of Google Assistant cloud settings (#94148) 2023-06-07 13:48:11 +02:00
Christopher Bailey 02d55a8e49 Bump unifiprotect to 4.10.1 (#94141) 2023-06-07 13:48:06 +02:00
Paulus Schoutsen f4e3ef6b51 Bumped version to 2023.6.0b6 2023-06-06 22:00:28 -04:00
Paulus Schoutsen 7740539df0 Bump waqiasync to 1.1.0 (#94136) 2023-06-06 22:00:20 -04:00
Christopher Bailey b077bf9b86 Fix multiple smart detects firing at once for UniFi Protect (#94133)
* Fix multiple smart detects firing at once

* Tweak

* Clean up logging. Linting

* Linting
2023-06-06 22:00:19 -04:00
Joakim Plate 23f2898836 Correct zha device classes for voc and pm25 (#94130)
Correct zha device classes
2023-06-06 22:00:18 -04:00
Shay Levy e6638ca356 Remove goalfeed integration (#94129) 2023-06-06 21:59:32 -04:00
Jean-François Roy 93d52d8835 Bump aiobafi6 to 0.8.2 (#94125) 2023-06-06 21:58:09 -04:00
puddly 26e08abb9a Revert "Increase Zigbee command retries (#93877)" (#94123) 2023-06-06 21:58:08 -04:00
J. Nick Koston 6a573b507e Remove mark_read service from persistent_notification (#94122)
* Remove mark_read from persistent_notification

Nothing on the frontend uses this, and the service is not documented

There is not much point in keeping this as the notifications
are no longer stored in the state machine

* adjust

* adjust
2023-06-06 21:58:07 -04:00
Bram Kragten 2b39550e55 Update frontend to 20230606.0 (#94119) 2023-06-06 21:58:06 -04:00
J. Nick Koston 0e50baf007 Verify persistant notifications can be dismissed by the id they are created with (#94112) 2023-06-06 21:58:05 -04:00
Luke 286de1f051 Bump python-roborock to 23.4 (#94111)
* bump to 23.0

* bump to 23.4
2023-06-06 21:58:04 -04:00
Luke 3e23996247 Bump Roborock to 0.21.0 (#94035)
bump to 21.0
2023-06-06 21:58:03 -04:00
Álvaro Fernández Rojas 7a658117bb Update aioairzone to v0.6.3 and fix issue with latest firmware update (#94100) 2023-06-06 21:56:36 -04:00
Luke 49388eab3a Add diagnostics to Roborock (#94099)
* Add diagnostics

* Update homeassistant/components/roborock/models.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* adds snapshot

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2023-06-06 21:56:35 -04:00
Justin Vanderhooft e6fcc6b73c fix: Bump melnor-bluetooth to fix deadlock (#94098) 2023-06-06 21:56:33 -04:00
Robert Svensson e00012289d Bump aiounifi to v48 - Fix fail to initialise due to board_rev not exist (#94093) 2023-06-06 21:56:32 -04:00
Luke f373f1abd5 Add missing translation keys for Roborock mop intensity (#94088) 2023-06-06 21:56:31 -04:00
puddly 2c43672a8a Include port info in the ZHA websocket settings response (#93934) 2023-06-06 21:56:30 -04:00
Paulus Schoutsen 7a6327d7e2 Bumped version to 2023.6.0b5 2023-06-05 16:13:07 -04:00
G Johansson ee8f63b9c9 Fix reload service in Command Line (#94085)
Fix multi platform reload service in command line
2023-06-05 16:12:59 -04:00
Bram Kragten 28e0f5e104 Update frontend to 20230605.0 (#94083)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-06-05 16:12:58 -04:00
Michael Hansen eb036af410 Bump intents to 2023.6.5 (#94077) 2023-06-05 16:12:57 -04:00
Luke 4bb6fec1d6 Don't add Roborock switches if it is not supported (#94069)
* don't add switches if it is not supported

* don't create entity unless if it is valid

* Raise on other exceptions

* rework valid_enties
2023-06-05 16:12:56 -04:00
J. Nick Koston dbd5511e5e Bump zeroconf to 0.64.0 (#94052) 2023-06-05 16:12:55 -04:00
Raman Gupta 580065e946 Fix zwave_js.update entity restore logic (#94043) 2023-06-05 16:12:54 -04:00
Pascal Reeb 4a31cb0ad8 Update pynuki to 1.6.2 (#94041)
chore(component/nuki): update pynuki to 1.6.2
2023-06-05 16:12:53 -04:00
G Johansson 5a63079c80 Remove update_before_add from binary_sensor in Command Line (#94040)
Remove update_before_add
2023-06-05 16:12:52 -04:00
tronikos 902bd521d2 Android TV Remote: Abort zeroconf if mac address is missing (#94026)
Abort zeroconf if mac address is missing
2023-06-05 16:12:51 -04:00
Ernst Klamer aff4d537a7 Bump xiaomi-ble to 0.17.2 (#94011)
Bump xiaomi-ble

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-06-05 16:12:49 -04:00
Joost Lekkerkerker 4f00cc9faa Show the sensor state using the coordinatordata instead of initial data (#94008)
* Show the sensor state using the coordinatordata instead of initial data

* Add test

* Remove part
2023-06-05 16:12:48 -04:00
Joost Lekkerkerker 2a99fea1de Add video id to youtube sensor state attributes (#93668)
* Add video id to state attributes

* Make extra state attributes not optional

* Revert "Make extra state attributes not optional"

This reverts commit d2f9e936c809dd50a5e4bbdaa181c9c9ddd3d217.
2023-06-05 16:12:48 -04:00
tronikos 9aeba6221b Fix error in tibber while fetching latest statistics (#93998) 2023-06-05 15:55:59 -04:00
Paulus Schoutsen bb2a89f065 Bumped version to 2023.6.0b4 2023-06-02 23:35:41 -04:00
Robert Hillis f92298c6fc Catch Google Sheets api error (#93979) 2023-06-02 23:35:36 -04:00
G Johansson 6ff55a6505 Add scan interval to Command Line (#93752)
* Add scan interval

* Handle previous not complete

* Fix faulty text

* Add tests

* lingering

* Cool down

* Fix tests
2023-06-02 23:35:35 -04:00
115 changed files with 1804 additions and 542 deletions
-1
View File
@@ -420,7 +420,6 @@ omit =
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
homeassistant/components/glances/sensor.py
homeassistant/components/goalfeed/*
homeassistant/components/goodwe/__init__.py
homeassistant/components/goodwe/button.py
homeassistant/components/goodwe/coordinator.py
+1
View File
@@ -391,6 +391,7 @@ def async_enable_logging(
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger(None).exception(
"Uncaught exception", exc_info=args # type: ignore[arg-type]
+2 -1
View File
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -71,7 +72,7 @@ class AbodeSensor(AbodeDevice, SensorEntity):
elif description.key == CONST.HUMI_STATUS_KEY:
self._attr_native_unit_of_measurement = device.humidity_unit
elif description.key == CONST.LUX_STATUS_KEY:
self._attr_native_unit_of_measurement = device.lux_unit
self._attr_native_unit_of_measurement = LIGHT_LUX
@property
def native_value(self) -> float | None:
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.1"]
"requirements": ["aioairzone==0.6.3"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.1.7"]
"requirements": ["aioairzone-cloud==0.1.8"]
}
@@ -98,7 +98,7 @@ async def async_setup_entry(
tasks = []
for heater in data_connection.get_devices():
tasks.append(heater.update_device_info())
tasks.append(asyncio.create_task(heater.update_device_info()))
await asyncio.wait(tasks)
devs = []
@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.host = discovery_info.host
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
self.mac = discovery_info.properties.get("bt")
assert self.mac
if not self.mac:
return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.host, CONF_NAME: self.name}
+1 -1
View File
@@ -5,7 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"iot_class": "local_push",
"requirements": ["aiobafi6==0.8.0"],
"requirements": ["aiobafi6==0.8.2"],
"zeroconf": [
{
"type": "_api._tcp.local.",
@@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
if self._prefs.alexa_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.alexa_settings_version < 3
@@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
):
self._migrate_alexa_entity_settings_v1()
_LOGGER.info(
"Finished migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)
@@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool:
if domain in SUPPORTED_DOMAINS:
return True
device_class = get_device_class(hass, entity_id)
try:
device_class = get_device_class(hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
@@ -208,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
if self._prefs.google_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.google_settings_version < 3
@@ -220,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig):
):
self._migrate_google_entity_settings_v1()
_LOGGER.info(
"Finished migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)
@@ -11,16 +11,24 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.const import (
CONF_COMMAND,
CONF_COMMAND_CLOSE,
@@ -34,6 +42,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@@ -74,6 +83,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
): vol.All(cv.time_period, cv.positive_timedelta),
}
)
COVER_SCHEMA = vol.Schema(
@@ -86,6 +98,9 @@ COVER_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
NOTIFY_SCHEMA = vol.Schema(
@@ -106,6 +121,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
SWITCH_SCHEMA = vol.Schema(
@@ -118,6 +136,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
COMBINED_SCHEMA = vol.Schema(
@@ -152,7 +173,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
platforms: list[Platform] = []
for platform_config in command_line_config:
for platform, _config in platform_config.items():
platforms.append(PLATFORM_MAPPING[platform])
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
platforms.append(mapped_platform)
_LOGGER.debug(
"Loading config %s for platform %s",
platform_config,
@@ -1,6 +1,7 @@
"""Support for custom shell commands to retrieve values."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import voluptuous as vol
@@ -18,17 +19,19 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData
DEFAULT_NAME = "Binary Command Sensor"
@@ -84,6 +87,9 @@ async def async_setup_platform(
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL
)
if value_template is not None:
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
@@ -98,15 +104,17 @@ async def async_setup_platform(
payload_off,
value_template,
unique_id,
scan_interval,
)
],
True,
)
class CommandBinarySensor(BinarySensorEntity):
"""Representation of a command line binary sensor."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +124,7 @@ class CommandBinarySensor(BinarySensorEntity):
payload_off: str,
value_template: Template | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
self.data = data
@@ -126,8 +135,39 @@ class CommandBinarySensor(BinarySensorEntity):
self._payload_off = payload_off
self._value_template = value_template
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Binary Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -141,3 +181,5 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = True
elif value == self._payload_off:
self._attr_is_on = False
self.async_write_ha_state()
@@ -1,7 +1,11 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
import logging
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
CONF_COMMAND_TIMEOUT = "command_timeout"
DEFAULT_TIMEOUT = 15
DOMAIN = "command_line"
+56 -15
View File
@@ -1,7 +1,8 @@
"""Support for command line covers."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -19,21 +20,23 @@ from homeassistant.const import (
CONF_COVERS,
CONF_FRIENDLY_NAME,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
COVER_SCHEMA = vol.Schema(
{
@@ -97,11 +100,12 @@ async def async_setup_platform(
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_UNIQUE_ID),
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not covers:
_LOGGER.error("No covers added")
LOGGER.error("No covers added")
return
async_add_entities(covers)
@@ -110,6 +114,8 @@ async def async_setup_platform(
class CommandCover(CoverEntity):
"""Representation a command line cover."""
_attr_should_poll = False
def __init__(
self,
name: str,
@@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
value_template: Template | None,
timeout: int,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the cover."""
self._attr_name = name
@@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
self._value_template = value_template
self._timeout = timeout
self._attr_unique_id = unique_id
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
def _move_cover(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
returncode = call_shell_with_timeout(command, self._timeout)
success = returncode == 0
if not success:
_LOGGER.error(
LOGGER.error(
"Command failed (with return code %s): %s", returncode, command
)
@@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
def _query_state(self) -> str | None:
"""Query for the state."""
if self._command_state:
_LOGGER.info("Running state value command: %s", self._command_state)
LOGGER.info("Running state value command: %s", self._command_state)
return check_output_or_log(self._command_state, self._timeout)
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -181,15 +218,19 @@ class CommandCover(CoverEntity):
self._state = None
if payload:
self._state = int(payload)
await self.async_update_ha_state(True)
def open_cover(self, **kwargs: Any) -> None:
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._move_cover(self._command_open)
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
await self._update_entity_state(None)
def close_cover(self, **kwargs: Any) -> None:
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._move_cover(self._command_close)
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
await self._update_entity_state(None)
def stop_cover(self, **kwargs: Any) -> None:
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._move_cover(self._command_stop)
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
await self._update_entity_state(None)
+49 -12
View File
@@ -1,10 +1,10 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from datetime import timedelta
import json
import logging
import voluptuous as vol
@@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_COMMAND,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@@ -28,15 +29,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log
_LOGGER = logging.getLogger(__name__)
CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor"
@@ -88,6 +88,7 @@ async def async_setup_platform(
if value_template is not None:
value_template.hass = hass
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
data = CommandSensorData(hass, command, command_timeout)
async_add_entities(
@@ -99,15 +100,17 @@ async def async_setup_platform(
value_template,
json_attributes,
unique_id,
scan_interval,
)
],
True,
]
)
class CommandSensor(SensorEntity):
"""Representation of a sensor that is using shell commands."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +119,7 @@ class CommandSensor(SensorEntity):
value_template: Template | None,
json_attributes: list[str] | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the sensor."""
self._attr_name = name
@@ -126,8 +130,39 @@ class CommandSensor(SensorEntity):
self._value_template = value_template
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -144,11 +179,11 @@ class CommandSensor(SensorEntity):
if k in json_dict
}
else:
_LOGGER.warning("JSON result was not a dictionary")
LOGGER.warning("JSON result was not a dictionary")
except ValueError:
_LOGGER.warning("Unable to parse output as JSON: %s", value)
LOGGER.warning("Unable to parse output as JSON: %s", value)
else:
_LOGGER.warning("Empty reply found when expecting JSON data")
LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None:
self._attr_native_value = None
return
@@ -163,6 +198,8 @@ class CommandSensor(SensorEntity):
else:
self._attr_native_value = value
self.async_write_ha_state()
class CommandSensorData:
"""The class for handling the data retrieval."""
@@ -191,7 +228,7 @@ class CommandSensorData:
args_to_render = {"arguments": args}
rendered_args = args_compiled.render(args_to_render)
except TemplateError as ex:
_LOGGER.exception("Error rendering command template: %s", ex)
LOGGER.exception("Error rendering command template: %s", ex)
return
else:
rendered_args = None
@@ -203,5 +240,5 @@ class CommandSensorData:
# Template used. Construct the string used in the shell
command = f"{prog} {rendered_args}"
_LOGGER.debug("Running command: %s", command)
LOGGER.debug("Running command: %s", command)
self.value = check_output_or_log(command, self.timeout)
+50 -10
View File
@@ -1,7 +1,8 @@
"""Support for custom shell commands to turn a switch on/off."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -20,6 +21,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
@@ -27,16 +29,17 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SWITCH_SCHEMA = vol.Schema(
{
@@ -112,11 +115,12 @@ async def async_setup_platform(
device_config.get(CONF_COMMAND_STATE),
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not switches:
_LOGGER.error("No switches added")
LOGGER.error("No switches added")
return
async_add_entities(switches)
@@ -125,6 +129,8 @@ async def async_setup_platform(
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Representation a switch that can be toggled using shell commands."""
_attr_should_poll = False
def __init__(
self,
config: ConfigType,
@@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_state: str | None,
value_template: Template | None,
timeout: int,
scan_interval: timedelta,
) -> None:
"""Initialize the switch."""
super().__init__(self.hass, config)
@@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._command_state = command_state
self._value_template = value_template
self._timeout = timeout
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
async def _switch(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
success = (
await self.hass.async_add_executor_job(
@@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
)
if not success:
_LOGGER.error("Command failed: %s", command)
LOGGER.error("Command failed: %s", command)
return success
def _query_state_value(self, command: str) -> str | None:
"""Execute state command for return value."""
_LOGGER.info("Running state value command: %s", command)
LOGGER.info("Running state value command: %s", command)
return check_output_or_log(command, self._timeout)
def _query_state_code(self, command: str) -> bool:
"""Execute state command for return code."""
_LOGGER.info("Running state code command: %s", command)
LOGGER.info("Running state code command: %s", command)
return (
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
)
@@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -201,15 +238,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if payload or value:
self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload)
await self.async_update_ha_state(True)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if await self._switch(self._command_on) and not self._command_state:
self._attr_is_on = True
self.async_schedule_update_ha_state()
await self._update_entity_state(None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if await self._switch(self._command_off) and not self._command_state:
self._attr_is_on = False
self.async_schedule_update_ha_state()
await self._update_entity_state(None)
@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"]
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"]
}
+2 -2
View File
@@ -192,7 +192,7 @@ class Flexit(ClimateEntity):
result = float(
await self._async_read_int16_from_register(register_type, register)
)
if result == -1:
if not result:
return -1
return result / 10.0
@@ -200,6 +200,6 @@ class Flexit(ClimateEntity):
result = await self._hub.async_pymodbus_call(
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
)
if result == -1:
if not result:
return False
return True
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230601.1"]
"requirements": ["home-assistant-frontend==20230608.0"]
}
@@ -1,65 +0,0 @@
"""Component for the Goalfeed service."""
import json
import pysher
import requests
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
# Version downgraded due to regression in library
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
DOMAIN = "goalfeed"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
GOALFEED_HOST = "feed.goalfeed.ca"
GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth"
GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Goalfeed component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
def goal_handler(data):
"""Handle goal events."""
goal = json.loads(json.loads(data))
hass.bus.fire("goal", event_data=goal)
def connect_handler(data):
"""Handle connection."""
post_data = {
"username": username,
"password": password,
"connection_info": data,
}
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json()
channel = pusher.subscribe("private-goals", resp["auth"])
channel.bind("goal", goal_handler)
pusher = pysher.Pusher(
GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST
)
pusher.connection.bind("pusher:connection_established", connect_handler)
pusher.connect()
return True
@@ -1,9 +0,0 @@
{
"domain": "goalfeed",
"name": "Goalfeed",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/goalfeed",
"iot_class": "cloud_push",
"loggers": ["pysher"],
"requirements": ["pysher==1.0.7"]
}
@@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
SOURCE_CLOUD = "cloud"
SOURCE_LOCAL = "local"
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT}
FAN_SPEEDS = {
"5/5": ["High", "Max", "Fast", "5"],
@@ -7,13 +7,18 @@ import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
@@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
except RefreshError as ex:
entry.async_start_reauth(hass)
raise ex
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
row_data = {"created": str(datetime.now())} | call.data[DATA]
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==2.6.4"],
"requirements": ["aiohomekit==2.6.5"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
+1 -1
View File
@@ -120,7 +120,7 @@ class ImapMessage:
@property
def subject(self) -> str:
"""Decode the message subject."""
decoded_header = decode_header(self.email_message["Subject"])
decoded_header = decode_header(self.email_message["Subject"] or "")
subject_header = make_header(decoded_header)
return str(subject_header)
@@ -302,12 +302,9 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
async def async_select_option(self, option: str) -> None:
"""Select new option."""
if option not in self.options:
_LOGGER.warning(
"Invalid option: %s (possible options: %s)",
option,
", ".join(self.options),
raise HomeAssistantError(
f"Invalid option: {option} (possible options: {', '.join(self.options)})"
)
return
self._attr_current_option = option
self.async_write_ha_state()
@@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SENSOR_TYPES = {
OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING,
@@ -62,7 +62,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.BINARY_SENSOR}"
async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
async_add_insteon_binary_sensor_entities()
async_add_insteon_devices(
hass,
Platform.BINARY_SENSOR,
InsteonBinarySensorEntity,
async_add_entities,
)
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):
+7 -2
View File
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
FAN_ONLY = "fan_only"
@@ -71,7 +71,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.CLIMATE}"
async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
async_add_insteon_climate_entities()
async_add_insteon_devices(
hass,
Platform.CLIMATE,
InsteonClimateEntity,
async_add_entities,
)
class InsteonClimateEntity(InsteonEntity, ClimateEntity):
+7 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -34,7 +34,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.COVER}"
async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
async_add_insteon_cover_entities()
async_add_insteon_devices(
hass,
Platform.COVER,
InsteonCoverEntity,
async_add_entities,
)
class InsteonCoverEntity(InsteonEntity, CoverEntity):
+7 -2
View File
@@ -17,7 +17,7 @@ from homeassistant.util.percentage import (
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SPEED_RANGE = (1, 255) # off is not included
@@ -38,7 +38,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.FAN}"
async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
async_add_insteon_fan_entities()
async_add_insteon_devices(
hass,
Platform.FAN,
InsteonFanEntity,
async_add_entities,
)
class InsteonFanEntity(InsteonEntity, FanEntity):
+9 -6
View File
@@ -1,4 +1,7 @@
"""Utility methods for the Insteon platform."""
from collections.abc import Iterable
from pyinsteon.device_types.device_base import Device
from pyinsteon.device_types.ipdb import (
AccessControl_Morningstar,
ClimateControl_Thermostat,
@@ -44,7 +47,7 @@ from pyinsteon.device_types.ipdb import (
from homeassistant.const import Platform
DEVICE_PLATFORM = {
DEVICE_PLATFORM: dict[Device, dict[Platform, Iterable[int]]] = {
AccessControl_Morningstar: {Platform.LOCK: [1]},
DimmableLightingControl: {Platform.LIGHT: [1]},
DimmableLightingControl_Dial: {Platform.LIGHT: [1]},
@@ -101,11 +104,11 @@ DEVICE_PLATFORM = {
}
def get_device_platforms(device):
def get_device_platforms(device) -> dict[Platform, Iterable[int]]:
"""Return the HA platforms for a device type."""
return DEVICE_PLATFORM.get(type(device), {}).keys()
return DEVICE_PLATFORM.get(type(device), {})
def get_platform_groups(device, domain) -> dict:
"""Return the platforms that a device belongs in."""
return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined]
def get_device_platform_groups(device: Device, platform: Platform) -> Iterable[int]:
"""Return the list of device groups for a platform."""
return get_device_platforms(device).get(platform, [])
+7 -2
View File
@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
MAX_BRIGHTNESS = 255
@@ -37,7 +37,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LIGHT}"
async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
async_add_insteon_light_entities()
async_add_insteon_devices(
hass,
Platform.LIGHT,
InsteonDimmerEntity,
async_add_entities,
)
class InsteonDimmerEntity(InsteonEntity, LightEntity):
+7 -2
View File
@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -30,7 +30,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LOCK}"
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
async_add_insteon_lock_entities()
async_add_insteon_devices(
hass,
Platform.LOCK,
InsteonLockEntity,
async_add_entities,
)
class InsteonLockEntity(InsteonEntity, LockEntity):
+7 -2
View File
@@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -33,7 +33,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.SWITCH}"
async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
async_add_insteon_switch_entities()
async_add_insteon_devices(
hass,
Platform.SWITCH,
InsteonSwitchEntity,
async_add_entities,
)
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):
+44 -13
View File
@@ -1,7 +1,10 @@
"""Utilities used by insteon component."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import logging
from typing import TYPE_CHECKING, Any
from pyinsteon import devices
from pyinsteon.address import Address
@@ -30,6 +33,7 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
@@ -38,6 +42,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
CONF_CAT,
@@ -78,7 +83,7 @@ from .const import (
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .ipdb import get_device_platforms, get_platform_groups
from .ipdb import get_device_platform_groups, get_device_platforms
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
@@ -89,6 +94,9 @@ from .schemas import (
X10_HOUSECODE_SCHEMA,
)
if TYPE_CHECKING:
from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
@@ -132,6 +140,9 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
_LOGGER.debug("Firing event %s with %s", event, schema)
hass.bus.async_fire(event, schema)
if str(device.address).startswith("X10"):
return
for name_or_group, event in device.events.items():
if isinstance(name_or_group, int):
for _, event in device.events[name_or_group].items():
@@ -158,8 +169,10 @@ def register_new_device_callback(hass):
await device.async_status()
platforms = get_device_platforms(device)
for platform in platforms:
groups = get_device_platform_groups(device, platform)
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
dispatcher_send(hass, signal, {"address": device.address})
dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
add_insteon_events(hass, device)
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
@@ -383,20 +396,38 @@ def print_aldb_to_log(aldb):
@callback
def async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
):
"""Add Insteon devices to a platform."""
new_entities = []
device_list = [discovery_info.get("address")] if discovery_info else devices
for address in device_list:
device = devices[address]
groups = get_platform_groups(device, platform)
for group in groups:
new_entities.append(entity_type(device, group))
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
discovery_info: dict[str, Any],
) -> None:
"""Add an Insteon group to a platform."""
address = discovery_info["address"]
device = devices[address]
new_entities = [
entity_type(device=device, group=group) for group in discovery_info["groups"]
]
async_add_entities(new_entities)
@callback
def async_add_insteon_devices(
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add all entities to a platform."""
for address in devices:
device = devices[address]
groups = get_device_platform_groups(device, platform)
discovery_info = {"address": address, "groups": groups}
async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
)
def get_usb_ports() -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = list_ports.comports()
+11 -5
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from pylast import LastFMNetwork, User, WSError
from pylast import LastFMNetwork, PyLastError, User, WSError
import voluptuous as vol
from homeassistant.config_entries import (
@@ -128,11 +128,14 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
main_user, _ = get_lastfm_user(
self.data[CONF_API_KEY], self.data[CONF_MAIN_USER]
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
return self.async_show_form(
step_id="friends",
@@ -197,11 +200,14 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
self.options[CONF_API_KEY],
self.options[CONF_MAIN_USER],
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
else:
friends = []
+1 -4
View File
@@ -13,7 +13,6 @@ from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import (
ATTR_LEVEL,
DEFAULT_LOGSEVERITY,
DOMAIN,
LOGGER_DEFAULT,
LOGGER_FILTERS,
@@ -39,9 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(
LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY
): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
}
+1 -1
View File
@@ -119,7 +119,7 @@ class LoggerSettings:
self._yaml_config = yaml_config
self._default_level = logging.INFO
if DOMAIN in yaml_config:
if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
@@ -38,7 +38,7 @@ def async_setup(hass: HomeAssistant) -> None:
class LocalSource(MediaSource):
"""Provide local directories as media sources."""
name: str = "Local Media"
name: str = "My media"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize local source."""
@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/melnor",
"iot_class": "local_polling",
"requirements": ["melnor-bluetooth==0.0.22"]
"requirements": ["melnor-bluetooth==0.0.25"]
}
+1 -1
View File
@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nuki",
"iot_class": "local_polling",
"loggers": ["pynuki"],
"requirements": ["pynuki==1.6.1"]
"requirements": ["pynuki==1.6.2"]
}
@@ -4,5 +4,5 @@
"codeowners": ["@joostlek"],
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling",
"requirements": ["python-opensky==0.0.7"]
"requirements": ["python-opensky==0.0.9"]
}
+1 -1
View File
@@ -78,7 +78,7 @@ def setup_platform(
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
radius = config.get(CONF_RADIUS, 0)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
session = async_get_clientsession(hass)
opensky = OpenSky(session=session)
add_entities(
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0"]
"requirements": ["python-otbr-api==2.2.0"]
}
+5
View File
@@ -95,6 +95,11 @@ class OTBRData:
"""Create an active operational dataset."""
return await self.api.create_active_dataset(dataset)
@_handle_otbr_error
async def delete_active_dataset(self) -> None:
"""Delete the active operational dataset."""
return await self.api.delete_active_dataset()
@_handle_otbr_error
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
"""Set current active operational dataset in TLVS format."""
@@ -81,6 +81,12 @@ async def websocket_create_network(
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
try:
await data.delete_active_dataset()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc))
return
try:
await data.create_active_dataset(
python_otbr_api.ActiveDataSet(
@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.7.9"],
"requirements": ["pyoverkiz==1.8.0"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",
@@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id"
ATTR_TITLE: Final = "title"
ATTR_STATUS: Final = "status"
STATUS_UNREAD = "unread"
STATUS_READ = "read"
# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated"
@@ -43,7 +41,6 @@ class Notification(TypedDict):
message: str
notification_id: str
title: str | None
status: str
class UpdateType(StrEnum):
@@ -98,7 +95,6 @@ def async_create(
notifications[notification_id] = {
ATTR_MESSAGE: message,
ATTR_NOTIFICATION_ID: notification_id,
ATTR_STATUS: STATUS_UNREAD,
ATTR_TITLE: title,
ATTR_CREATED_AT: dt_util.utcnow(),
}
@@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the persistent notification component."""
notifications = _async_get_or_create_notifications(hass)
@callback
def create_service(call: ServiceCall) -> None:
@@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Handle the dismiss notification service call."""
async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID])
@callback
def mark_read_service(call: ServiceCall) -> None:
"""Handle the mark_read notification service call."""
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
if notification_id not in notifications:
_LOGGER.error(
(
"Marking persistent_notification read failed: "
"Notification ID %s not found"
),
notification_id,
)
return
notification = notifications[notification_id]
notification[ATTR_STATUS] = STATUS_READ
async_dispatcher_send(
hass,
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
UpdateType.UPDATED,
{notification_id: notification},
)
hass.services.async_register(
DOMAIN,
"create",
@@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION
)
hass.services.async_register(
DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION
)
websocket_api.async_register_command(hass, websocket_get_notifications)
websocket_api.async_register_command(hass, websocket_subscribe_notifications)
@@ -33,15 +33,3 @@ dismiss:
example: 1234
selector:
text:
mark_read:
name: Mark read
description: Mark a notification read.
fields:
notification_id:
name: Notification ID
description: Target ID of the notification, which should be mark read.
required: true
example: 1234
selector:
text:
@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback",
"iot_class": "local_polling",
"requirements": ["pulsectl==20.2.4"]
"requirements": ["pulsectl==23.5.2"]
}
@@ -0,0 +1,38 @@
"""Support for the Airzone diagnostics."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
return {
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
"coordinators": {
f"**REDACTED-{i}**": {
"roborock_device_info": async_redact_data(
coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD
),
"api": coordinator.api.diagnostic_data,
}
for i, coordinator in enumerate(coordinators.values())
},
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.17.0"]
"requirements": ["python-roborock==0.23.4"]
}
@@ -1,5 +1,6 @@
"""Roborock Models."""
from dataclasses import dataclass
from typing import Any
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.roborock_typing import DeviceProp
@@ -13,3 +14,12 @@ class RoborockHassDeviceInfo:
network_info: NetworkInfo
product: HomeDataProduct
props: DeviceProp
def as_dict(self) -> dict[str, dict[str, Any]]:
"""Turn RoborockHassDeviceInfo into a dictionary."""
return {
"device": self.device.as_dict(),
"network_info": self.network_info.as_dict(),
"product": self.product.as_dict(),
"props": self.props.as_dict(),
}
@@ -90,8 +90,11 @@
"name": "Mop intensity",
"state": {
"off": "Off",
"low": "Low",
"mild": "Mild",
"medium": "Medium",
"moderate": "Moderate",
"high": "High",
"intense": "Intense",
"custom": "Custom"
}
+43 -9
View File
@@ -1,9 +1,11 @@
"""Support for Roborock switch."""
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin:
evaluate_value: Callable[[dict], bool]
# Sets the status of the switch
set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]]
# Check support of this feature
check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]]
@dataclass
@@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_CHILD_LOCK_STATUS
),
evaluate_value=lambda data: data["lock_status"] == 1,
key="child_lock",
translation_key="child_lock",
@@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_FLOW_LED_STATUS
),
evaluate_value=lambda data: data["status"] == 1,
key="status_indicator",
translation_key="status_indicator",
@@ -75,16 +85,38 @@ async def async_setup_entry(
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
async_add_entities(
(
RoborockSwitchEntity(
f"{description.key}_{slugify(device_id)}",
coordinator,
description,
)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
possible_entities: list[
tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription]
] = [
(device_id, coordinator, description)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
results = await asyncio.gather(
*(
description.check_support(coordinator)
for _, coordinator, description in possible_entities
),
return_exceptions=True,
)
valid_entities: list[RoborockSwitchEntity] = []
for posible_entity, result in zip(possible_entities, results):
if isinstance(result, Exception):
if not isinstance(result, RoborockException):
raise result
_LOGGER.debug("Not adding entity because of %s", result)
else:
valid_entities.append(
RoborockSwitchEntity(
f"{posible_entity[2].key}_{slugify(posible_entity[0])}",
posible_entity[1],
posible_entity[2],
result,
)
)
async_add_entities(
valid_entities,
True,
)
@@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity):
unique_id: str,
coordinator: RoborockDataUpdateCoordinator,
entity_description: RoborockSwitchDescription,
initial_value: bool,
) -> None:
"""Create a switch entity."""
self.entity_description = entity_description
super().__init__(unique_id, coordinator.device_info, coordinator.api)
self._attr_is_on = initial_value
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"],
"zeroconf": ["_meshcop._udp.local."]
}
+1 -1
View File
@@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
)
last_stats = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, statistic_id, True, {}
get_last_statistics, self.hass, 1, statistic_id, True, set()
)
if not last_stats:
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["aiounifi"],
"quality_scale": "platinum",
"requirements": ["aiounifi==47"],
"requirements": ["aiounifi==48"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -14,8 +14,6 @@ from pyunifiprotect.data import (
ProtectAdoptableDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectAudioType,
SmartDetectObjectType,
)
from pyunifiprotect.data.nvr import UOSDisk
@@ -364,8 +362,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_person",
ufp_enabled="is_person_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PERSON,
ufp_event_obj="last_person_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_vehicle",
@@ -374,8 +371,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_vehicle",
ufp_enabled="is_vehicle_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.VEHICLE,
ufp_event_obj="last_vehicle_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_face",
@@ -384,8 +380,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_face",
ufp_enabled="is_face_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.FACE,
ufp_event_obj="last_face_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_package",
@@ -394,8 +389,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_package",
ufp_enabled="is_package_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PACKAGE,
ufp_event_obj="last_package_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_audio_any",
@@ -412,8 +406,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.SMOKE,
ufp_event_obj="last_smoke_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_audio_cmonx",
@@ -422,8 +415,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.CMONX,
ufp_event_obj="last_cmonx_detect_event",
),
)
@@ -40,6 +40,11 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
_LOGGER = logging.getLogger(__name__)
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
SMART_EVENTS = {
EventType.SMART_DETECT,
EventType.SMART_AUDIO_DETECT,
EventType.SMART_DETECT_LINE,
}
@callback
@@ -223,6 +228,25 @@ class ProtectData:
# trigger updates for camera that the event references
elif isinstance(obj, Event):
if obj.type in SMART_EVENTS:
if obj.camera is not None:
if obj.end is None:
_LOGGER.debug(
"%s (%s): New smart detection started for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
else:
_LOGGER.debug(
"%s (%s): Smart detection ended for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
if obj.type == EventType.DEVICE_ADOPTED:
if obj.metadata is not None and obj.metadata.device_id is not None:
device = self.api.bootstrap.get_device_from_id(
@@ -41,7 +41,7 @@
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.9.1", "unifi-discovery==1.1.7"],
"requirements": ["pyunifiprotect==4.10.2", "unifi-discovery==1.1.7"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
+17 -17
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
import logging
from typing import Any, Generic, TypeVar, cast
@@ -10,6 +11,7 @@ from typing import Any, Generic, TypeVar, cast
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from homeassistant.helpers.entity import EntityDescription
from homeassistant.util import dt as dt_util
from .utils import get_nested_attr
@@ -67,7 +69,6 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
"""Mixin for events."""
ufp_event_obj: str | None = None
ufp_smart_type: str | None = None
def get_event_obj(self, obj: T) -> Event | None:
"""Return value from UniFi Protect device."""
@@ -79,23 +80,22 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
def get_is_on(self, obj: T) -> bool:
"""Return value if event is active."""
value = bool(self.get_ufp_value(obj))
if value:
event = self.get_event_obj(obj)
value = event is not None
if not value:
_LOGGER.debug("%s (%s): missing event", self.name, obj.mac)
event = self.get_event_obj(obj)
if event is None:
return False
if event is not None and self.ufp_smart_type is not None:
value = self.ufp_smart_type in event.smart_detect_types
if not value:
_LOGGER.debug(
"%s (%s): %s not in %s",
self.name,
obj.mac,
self.ufp_smart_type,
event.smart_detect_types,
)
now = dt_util.utcnow()
value = now > event.start
if value and event.end is not None and now > event.end:
value = False
# only log if the recent ended recently
if event.end + timedelta(seconds=10) < now:
_LOGGER.debug(
"%s (%s): end ended at %s",
self.name,
obj.mac,
event.end.isoformat(),
)
if value:
_LOGGER.debug("%s (%s): value is on", self.name, obj.mac)
@@ -15,7 +15,6 @@ from pyunifiprotect.data import (
ProtectDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectObjectType,
)
from homeassistant.components.sensor import (
@@ -528,10 +527,9 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
name="License Plate Detected",
icon="mdi:car",
translation_key="license_plate",
ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE,
ufp_value="is_smart_detected",
ufp_required_field="can_detect_license_plate",
ufp_event_obj="last_smart_detect_event",
ufp_event_obj="last_license_plate_detect_event",
),
)
@@ -767,8 +765,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
EventEntityMixin._async_update_device_from_protect(self, device)
is_on = self.entity_description.get_is_on(device)
is_license_plate = (
self.entity_description.ufp_smart_type
== SmartDetectObjectType.LICENSE_PLATE
self.entity_description.ufp_event_obj == "last_license_plate_detect_event"
)
if (
not is_on
+1 -1
View File
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/waqi",
"iot_class": "cloud_polling",
"loggers": ["waqiasync"],
"requirements": ["waqiasync==1.0.0"]
"requirements": ["waqiasync==1.1.0"]
}
@@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==0.17.0"]
"requirements": ["xiaomi-ble==0.17.2"]
}
+6 -11
View File
@@ -1,9 +1,6 @@
"""Entity representing a YouTube account."""
from __future__ import annotations
from typing import Any
from homeassistant.const import ATTR_ID
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -21,20 +18,18 @@ class YouTubeChannelEntity(CoordinatorEntity):
self,
coordinator: YouTubeDataUpdateCoordinator,
description: EntityDescription,
channel: dict[str, Any],
channel_id: str,
) -> None:
"""Initialize a Google Mail entity."""
"""Initialize a YouTube entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}_{description.key}"
f"{coordinator.config_entry.entry_id}_{channel_id}_{description.key}"
)
self._channel_id = channel_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={
(DOMAIN, f"{coordinator.config_entry.entry_id}_{channel[ATTR_ID]}")
},
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{channel_id}")},
manufacturer=MANUFACTURER,
name=channel[ATTR_TITLE],
name=coordinator.data[channel_id][ATTR_TITLE],
)
self._channel = channel
+21 -4
View File
@@ -18,6 +18,7 @@ from .const import (
ATTR_SUBSCRIBER_COUNT,
ATTR_THUMBNAIL,
ATTR_TITLE,
ATTR_VIDEO_ID,
COORDINATOR,
DOMAIN,
)
@@ -30,6 +31,7 @@ class YouTubeMixin:
value_fn: Callable[[Any], StateType]
entity_picture_fn: Callable[[Any], str]
attributes_fn: Callable[[Any], dict[str, Any]] | None
@dataclass
@@ -44,6 +46,9 @@ SENSOR_TYPES = [
icon="mdi:youtube",
value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE],
entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL],
attributes_fn=lambda channel: {
ATTR_VIDEO_ID: channel[ATTR_LATEST_VIDEO][ATTR_VIDEO_ID]
},
),
YouTubeSensorEntityDescription(
key="subscribers",
@@ -52,6 +57,7 @@ SENSOR_TYPES = [
native_unit_of_measurement="subscribers",
value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT],
entity_picture_fn=lambda channel: channel[ATTR_ICON],
attributes_fn=None,
),
]
@@ -64,8 +70,8 @@ async def async_setup_entry(
COORDINATOR
]
async_add_entities(
YouTubeSensor(coordinator, sensor_type, channel)
for channel in coordinator.data.values()
YouTubeSensor(coordinator, sensor_type, channel_id)
for channel_id in coordinator.data
for sensor_type in SENSOR_TYPES
)
@@ -78,9 +84,20 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.entity_description.value_fn(self._channel)
return self.entity_description.value_fn(self.coordinator.data[self._channel_id])
@property
def entity_picture(self) -> str:
"""Return the value reported by the sensor."""
return self.entity_description.entity_picture_fn(self._channel)
return self.entity_description.entity_picture_fn(
self.coordinator.data[self._channel_id]
)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the extra state attributes."""
if self.entity_description.attributes_fn:
return self.entity_description.attributes_fn(
self.coordinator.data[self._channel_id]
)
return None
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.63.0"]
"requirements": ["zeroconf==0.64.0"]
}
@@ -45,8 +45,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
DEFAULT_REQUEST_RETRIES = 3
class AttrReportConfig(TypedDict, total=True):
"""Configuration to report for the attributes."""
@@ -80,8 +78,6 @@ def decorate_command(cluster_handler, command):
@wraps(command)
async def wrapper(*args, **kwds):
kwds.setdefault("tries", DEFAULT_REQUEST_RETRIES)
try:
result = await command(*args, **kwds)
cluster_handler.debug(
+4 -1
View File
@@ -722,7 +722,9 @@ class PPBVOCLevel(Sensor):
"""VOC Level sensor."""
SENSOR_ATTR = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
_attr_device_class: SensorDeviceClass = (
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_name: str = "VOC level"
_decimals = 0
@@ -736,6 +738,7 @@ class PM25(Sensor):
"""Particulate Matter 2.5 microns or less sensor."""
SENSOR_ATTR = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.PM25
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_name: str = "Particulate matter"
_decimals = 0
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeVar, cast
import voluptuous as vol
import zigpy.backups
from zigpy.config import CONF_DEVICE
from zigpy.config.validators import cv_boolean
from zigpy.types.named import EUI64
from zigpy.zcl.clusters.security import IasAce
@@ -1136,6 +1137,7 @@ async def websocket_get_network_settings(
msg[ID],
{
"radio_type": async_get_radio_type(hass, zha_gateway.config_entry).name,
"device": zha_gateway.application_controller.config[CONF_DEVICE],
"settings": backup.as_dict(),
},
)
+15 -10
View File
@@ -42,6 +42,7 @@ PARALLEL_UPDATES = 1
UPDATE_DELAY_STRING = "delay"
UPDATE_DELAY_INTERVAL = 5 # In minutes
ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware"
@dataclass
@@ -53,7 +54,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the extra data."""
return {
"latest_version_firmware": asdict(self.latest_version_firmware)
ATTR_LATEST_VERSION_FIRMWARE: asdict(self.latest_version_firmware)
if self.latest_version_firmware
else None
}
@@ -61,7 +62,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData):
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ZWaveNodeFirmwareUpdateExtraStoredData:
"""Initialize the extra data from a dict."""
if not (firmware_dict := data["latest_version_firmware"]):
if not (firmware_dict := data[ATTR_LATEST_VERSION_FIRMWARE]):
return cls(None)
return cls(NodeFirmwareUpdateInfo.from_dict(firmware_dict))
@@ -326,20 +327,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
)
# If we have a complete previous state, use that to set the latest version
if (state := await self.async_get_last_state()) and (
extra_data := await self.async_get_last_extra_data()
if (
(state := await self.async_get_last_state())
and (latest_version := state.attributes.get(ATTR_LATEST_VERSION))
is not None
and (extra_data := await self.async_get_last_extra_data())
):
self._attr_latest_version = state.attributes[ATTR_LATEST_VERSION]
self._attr_latest_version = latest_version
self._latest_version_firmware = (
ZWaveNodeFirmwareUpdateExtraStoredData.from_dict(
extra_data.as_dict()
).latest_version_firmware
)
# If we have no state to restore, we can set the latest version to installed
# so that the entity starts as off. If we have partial restore data due to an
# upgrade to an HA version where this feature is released from one that is not
# the entity will start in an unknown state until we can correct on next update
elif not state:
# If we have no state or latest version to restore, we can set the latest
# version to installed so that the entity starts as off. If we have partial
# restore data due to an upgrade to an HA version where this feature is released
# from one that is not the entity will start in an unknown state until we can
# correct on next update
elif not state or not latest_version:
self._attr_latest_version = self._attr_installed_version
# Spread updates out in 5 minute increments to avoid flooding the network
+1 -1
View File
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "0b3"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
@@ -1995,12 +1995,6 @@
}
}
},
"goalfeed": {
"name": "Goalfeed",
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push"
},
"goalzero": {
"name": "Goal Zero Yeti",
"integration_type": "device",
+5 -6
View File
@@ -25,8 +25,8 @@ ha-av==10.1.0
hass-nabucasa==0.67.1
hassil==1.0.6
home-assistant-bluetooth==1.10.0
home-assistant-frontend==20230601.1
home-assistant-intents==2023.5.30
home-assistant-frontend==20230608.0
home-assistant-intents==2023.6.5
httpx==0.24.1
ifaddr==0.2.0
janus==1.0.0
@@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtcvad==2.0.10
yarl==1.9.2
zeroconf==0.63.0
zeroconf==0.64.0
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238
@@ -128,9 +128,8 @@ authlib<1.0
# Version 2.0 added typing, prevent accidental fallbacks
backoff>=2.0
# Breaking change in version
# https://github.com/samuelcolvin/pydantic/issues/4092
pydantic!=1.9.1
# Require to avoid issues with decorators (#93904). v2 has breaking changes.
pydantic>=1.10.8,<2.0
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
+4 -1
View File
@@ -245,7 +245,10 @@ async def _async_setup_component(
severity=IssueSeverity.ERROR,
issue_domain=domain,
translation_key="integration_key_no_support",
translation_placeholders={"domain": domain},
translation_placeholders={
"domain": domain,
"add_integration": f"/config/integrations/dashboard/add?domain={domain}",
},
)
start = timer()
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.6.0b3"
version = "2023.6.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+18 -21
View File
@@ -116,10 +116,10 @@ aio_georss_gdacs==0.8
aioairq==0.2.4
# homeassistant.components.airzone_cloud
aioairzone-cloud==0.1.7
aioairzone-cloud==0.1.8
# homeassistant.components.airzone
aioairzone==0.6.1
aioairzone==0.6.3
# homeassistant.components.ambient_station
aioambient==2023.04.0
@@ -134,7 +134,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5
# homeassistant.components.baf
aiobafi6==0.8.0
aiobafi6==0.8.2
# homeassistant.components.aws
aiobotocore==2.1.0
@@ -177,7 +177,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.homekit_controller
aiohomekit==2.6.4
aiohomekit==2.6.5
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -294,7 +294,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==47
aiounifi==48
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@@ -924,10 +924,10 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230601.1
home-assistant-frontend==20230608.0
# homeassistant.components.conversation
home-assistant-intents==2023.5.30
home-assistant-intents==2023.6.5
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -1122,7 +1122,7 @@ mcstatus==6.0.0
meater-python==0.0.8
# homeassistant.components.melnor
melnor-bluetooth==0.0.22
melnor-bluetooth==0.0.25
# homeassistant.components.message_bird
messagebird==1.2.0
@@ -1418,7 +1418,7 @@ psutil-home-assistant==0.0.1
psutil==5.9.5
# homeassistant.components.pulseaudio_loopback
pulsectl==20.2.4
pulsectl==23.5.2
# homeassistant.components.androidtv
pure-python-adb[async]==0.3.0.dev0
@@ -1831,7 +1831,7 @@ pynina==0.3.0
pynobo==1.6.0
# homeassistant.components.nuki
pynuki==1.6.1
pynuki==1.6.2
# homeassistant.components.nut
pynut2==2.1.2
@@ -1872,7 +1872,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.7.9
pyoverkiz==1.8.0
# homeassistant.components.openweathermap
pyowm==3.2.0
@@ -1966,9 +1966,6 @@ pyserial==3.5
# homeassistant.components.sesame
pysesame2==1.0.1
# homeassistant.components.goalfeed
pysher==1.0.7
# homeassistant.components.sia
pysiaalarm==3.1.1
@@ -2105,11 +2102,11 @@ python-mystrom==2.2.0
python-nest==4.2.0
# homeassistant.components.opensky
python-opensky==0.0.7
python-opensky==0.0.9
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.1.0
python-otbr-api==2.2.0
# homeassistant.components.picnic
python-picnic-api==1.1.0
@@ -2121,7 +2118,7 @@ python-qbittorrent==0.4.2
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==0.17.0
python-roborock==0.23.4
# homeassistant.components.smarttub
python-smarttub==0.0.33
@@ -2171,7 +2168,7 @@ pytrafikverket==0.3.3
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.9.1
pyunifiprotect==4.10.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@@ -2635,7 +2632,7 @@ wakeonlan==2.1.0
wallbox==0.4.12
# homeassistant.components.waqi
waqiasync==1.0.0
waqiasync==1.1.0
# homeassistant.components.folder_watcher
watchdog==2.3.1
@@ -2677,7 +2674,7 @@ wyoming==0.0.1
xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble
xiaomi-ble==0.17.0
xiaomi-ble==0.17.2
# homeassistant.components.knx
xknx==2.10.0
@@ -2728,7 +2725,7 @@ zamg==0.2.2
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.63.0
zeroconf==0.64.0
# homeassistant.components.zeversolar
zeversolar==0.3.1
+15 -15
View File
@@ -106,10 +106,10 @@ aio_georss_gdacs==0.8
aioairq==0.2.4
# homeassistant.components.airzone_cloud
aioairzone-cloud==0.1.7
aioairzone-cloud==0.1.8
# homeassistant.components.airzone
aioairzone==0.6.1
aioairzone==0.6.3
# homeassistant.components.ambient_station
aioambient==2023.04.0
@@ -124,7 +124,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5
# homeassistant.components.baf
aiobafi6==0.8.0
aiobafi6==0.8.2
# homeassistant.components.aws
aiobotocore==2.1.0
@@ -164,7 +164,7 @@ aioguardian==2022.07.0
aioharmony==0.2.10
# homeassistant.components.homekit_controller
aiohomekit==2.6.4
aiohomekit==2.6.5
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -275,7 +275,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==47
aiounifi==48
# homeassistant.components.vlc_telnet
aiovlc==0.1.0
@@ -716,10 +716,10 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230601.1
home-assistant-frontend==20230608.0
# homeassistant.components.conversation
home-assistant-intents==2023.5.30
home-assistant-intents==2023.6.5
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -848,7 +848,7 @@ mcstatus==6.0.0
meater-python==0.0.8
# homeassistant.components.melnor
melnor-bluetooth==0.0.22
melnor-bluetooth==0.0.25
# homeassistant.components.meteo_france
meteofrance-api==1.2.0
@@ -1347,7 +1347,7 @@ pynina==0.3.0
pynobo==1.6.0
# homeassistant.components.nuki
pynuki==1.6.1
pynuki==1.6.2
# homeassistant.components.nut
pynut2==2.1.2
@@ -1382,7 +1382,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.7.9
pyoverkiz==1.8.0
# homeassistant.components.openweathermap
pyowm==3.2.0
@@ -1532,7 +1532,7 @@ python-nest==4.2.0
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.1.0
python-otbr-api==2.2.0
# homeassistant.components.picnic
python-picnic-api==1.1.0
@@ -1541,7 +1541,7 @@ python-picnic-api==1.1.0
python-qbittorrent==0.4.2
# homeassistant.components.roborock
python-roborock==0.17.0
python-roborock==0.23.4
# homeassistant.components.smarttub
python-smarttub==0.0.33
@@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.9.1
pyunifiprotect==4.10.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@@ -1947,7 +1947,7 @@ wyoming==0.0.1
xbox-webapi==2.0.11
# homeassistant.components.xiaomi_ble
xiaomi-ble==0.17.0
xiaomi-ble==0.17.2
# homeassistant.components.knx
xknx==2.10.0
@@ -1986,7 +1986,7 @@ youless-api==1.0.1
zamg==0.2.2
# homeassistant.components.zeroconf
zeroconf==0.63.0
zeroconf==0.64.0
# homeassistant.components.zeversolar
zeversolar==0.3.1
+2 -3
View File
@@ -132,9 +132,8 @@ authlib<1.0
# Version 2.0 added typing, prevent accidental fallbacks
backoff>=2.0
# Breaking change in version
# https://github.com/samuelcolvin/pydantic/issues/4092
pydantic!=1.9.1
# Require to avoid issues with decorators (#93904). v2 has breaking changes.
pydantic>=1.10.8,<2.0
# Breaks asyncio
# https://github.com/pubnub/python/issues/130
+1 -1
View File
@@ -39,7 +39,7 @@ async def test_attributes(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.environment_sensor_lux")
assert state.state == "1.0"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "lux"
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "lx"
state = hass.states.get("sensor.environment_sensor_temperature")
# Abodepy device JSON reports 19.5, but Home Assistant shows 19.4
-3
View File
@@ -1,14 +1,12 @@
"""The climate tests for the Airzone platform."""
from unittest.mock import patch
from aioairzone.common import OperationMode
from aioairzone.const import (
API_COOL_SET_POINT,
API_DATA,
API_HEAT_SET_POINT,
API_MAX_TEMP,
API_MIN_TEMP,
API_MODE,
API_ON,
API_SET_POINT,
API_SPEED,
@@ -336,7 +334,6 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
{
API_SYSTEM_ID: 1,
API_ZONE_ID: 1,
API_MODE: OperationMode.COOLING.value,
API_ON: 1,
}
]
@@ -712,6 +712,30 @@ async def test_zeroconf_flow_already_configured_host_not_changed_no_reload_entry
assert len(mock_setup_entry.mock_calls) == 0
async def test_zeroconf_flow_abort_if_mac_is_missing(
hass: HomeAssistant,
) -> None:
"""Test when mac is missing in the zeroconf discovery we abort."""
host = "1.2.3.4"
name = "My Android TV"
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host=host,
addresses=[host],
port=6466,
hostname=host,
type="mock_type",
name=name + "._androidtvremote2._tcp.local.",
properties={},
),
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_reauth_flow_success(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
@@ -1,17 +1,24 @@
"""The tests for the Command line Binary sensor platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant import setup
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.command_line.binary_sensor import CommandBinarySensor
from homeassistant.components.command_line.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
from homeassistant.util import dt as dt_util
from tests.common import async_fire_time_changed
async def test_setup_platform_yaml(hass: HomeAssistant) -> None:
@@ -189,3 +196,59 @@ async def test_return_code(
)
await hass.async_block_till_done()
assert "return code 33" in caplog.text
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandBinarySensor(CommandBinarySensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.binary_sensor.CommandBinarySensor",
side_effect=MockCommandBinarySensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"binary_sensor": {
"name": "Test",
"command": "echo 1",
"payload_on": "1",
"payload_off": "0",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 2
assert (
"Updating Command Line Binary Sensor Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)
@@ -1,6 +1,8 @@
"""The tests the cover command line platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import os
import tempfile
from unittest.mock import patch
@@ -9,6 +11,7 @@ import pytest
from homeassistant import config as hass_config, setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.cover import CommandCover
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -320,3 +323,58 @@ async def test_unique_id(
assert entity_registry.async_get_entity_id(
"cover", "command_line", "not-so-unique-anymore"
)
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandCover(CommandCover):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.cover.CommandCover",
side_effect=MockCommandCover,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"cover": {
"command_state": "echo 1",
"value_template": "{{ value }}",
"name": "Test",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 0
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Cover Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)
@@ -1,6 +1,7 @@
"""The tests for the Command line sensor platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from typing import Any
from unittest.mock import patch
@@ -9,6 +10,7 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.sensor import CommandSensor
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -530,3 +532,57 @@ async def test_unique_id(
assert entity_registry.async_get_entity_id(
"sensor", "command_line", "not-so-unique-anymore"
)
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandSensor(CommandSensor):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.sensor.CommandSensor",
side_effect=MockCommandSensor,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo 1",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Sensor Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 2
assert (
"Updating Command Line Sensor Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)
@@ -1,6 +1,8 @@
"""The tests for the Command line switch platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import json
import os
import subprocess
@@ -11,6 +13,7 @@ import pytest
from homeassistant import setup
from homeassistant.components.command_line import DOMAIN
from homeassistant.components.command_line.switch import CommandSwitch
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -637,3 +640,59 @@ async def test_templating(hass: HomeAssistant) -> None:
assert entity_state.attributes.get("icon") == "mdi:on"
assert entity_state2.state == STATE_ON
assert entity_state2.attributes.get("icon") == "mdi:on"
async def test_updating_to_often(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test handling updating when command already running."""
called = []
class MockCommandSwitch(CommandSwitch):
"""Mock entity that updates slow."""
async def _async_update(self) -> None:
"""Update slow."""
called.append(1)
# Add waiting time
await asyncio.sleep(1)
with patch(
"homeassistant.components.command_line.switch.CommandSwitch",
side_effect=MockCommandSwitch,
):
await setup.async_setup_component(
hass,
DOMAIN,
{
"command_line": [
{
"switch": {
"command_state": "echo 1",
"command_on": "echo 2",
"command_off": "echo 3",
"name": "Test",
"scan_interval": 0.1,
}
}
]
},
)
await hass.async_block_till_done()
assert len(called) == 0
assert (
"Updating Command Line Switch Test took longer than the scheduled update interval"
not in caplog.text
)
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(called) == 1
assert (
"Updating Command Line Switch Test took longer than the scheduled update interval"
in caplog.text
)
await asyncio.sleep(0.2)
+34 -1
View File
@@ -6,7 +6,9 @@ import time
from typing import Any
from unittest.mock import patch
from gspread.exceptions import APIError
import pytest
from requests.models import Response
from homeassistant.components.application_credentials import (
ClientCredential,
@@ -15,7 +17,7 @@ from homeassistant.components.application_credentials import (
from homeassistant.components.google_sheets import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@@ -212,6 +214,37 @@ async def test_append_sheet(
assert len(mock_client.mock_calls) == 8
async def test_append_sheet_api_error(
hass: HomeAssistant,
setup_integration: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test append to sheet service call API error."""
await setup_integration()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
response = Response()
response.status_code = 503
with pytest.raises(HomeAssistantError), patch(
"homeassistant.components.google_sheets.Client.request",
side_effect=APIError(response),
):
await hass.services.async_call(
DOMAIN,
"append_sheet",
{
"config_entry": config_entry.entry_id,
"worksheet": "Sheet1",
"data": {"foo": "bar"},
},
blocking=True,
)
async def test_append_sheet_invalid_config_entry(
hass: HomeAssistant,
setup_integration: ComponentSetup,
+20
View File
@@ -24,7 +24,12 @@ TEST_MESSAGE_HEADERS2 = (
b"Subject: Test subject\r\n"
)
TEST_MESSAGE_HEADERS3 = b""
TEST_MESSAGE = TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2
TEST_MESSAGE_NO_SUBJECT_TO_FROM = (
TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS3
)
TEST_MESSAGE_ALT = TEST_MESSAGE_HEADERS1 + DATE_HEADER2 + TEST_MESSAGE_HEADERS2
TEST_INVALID_DATE1 = (
TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID1 + TEST_MESSAGE_HEADERS2
@@ -204,4 +209,19 @@ TEST_FETCH_RESPONSE_MULTIPART = (
],
)
TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM = (
"OK",
[
b"1 FETCH (BODY[] {"
+ str(len(TEST_MESSAGE_NO_SUBJECT_TO_FROM + TEST_CONTENT_TEXT_PLAIN)).encode(
"utf-8"
)
+ b"}",
bytearray(TEST_MESSAGE_NO_SUBJECT_TO_FROM + TEST_CONTENT_TEXT_PLAIN),
b")",
b"Fetch completed (0.0001 + 0.000 secs).",
],
)
RESPONSE_BAD = ("BAD", [])
+40 -1
View File
@@ -1,6 +1,6 @@
"""Test the imap entry initialization."""
import asyncio
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
@@ -22,6 +22,7 @@ from .const import (
TEST_FETCH_RESPONSE_INVALID_DATE2,
TEST_FETCH_RESPONSE_INVALID_DATE3,
TEST_FETCH_RESPONSE_MULTIPART,
TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM,
TEST_FETCH_RESPONSE_TEXT_BARE,
TEST_FETCH_RESPONSE_TEXT_OTHER,
TEST_FETCH_RESPONSE_TEXT_PLAIN,
@@ -153,6 +154,44 @@ async def test_receiving_message_successfully(
)
@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE])
@pytest.mark.parametrize("imap_fetch", [TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM])
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
async def test_receiving_message_no_subject_to_from(
hass: HomeAssistant, mock_imap_protocol: MagicMock
) -> None:
"""Test receiving a message successfully without subject, to and from in body."""
event_called = async_capture_events(hass, "imap_content")
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Make sure we have had one update (when polling)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
state = hass.states.get("sensor.imap_email_email_com")
# we should have received one message
assert state is not None
assert state.state == "1"
# we should have received one event
assert len(event_called) == 1
data: dict[str, Any] = event_called[0].data
assert data["server"] == "imap.server.com"
assert data["username"] == "email@email.com"
assert data["search"] == "UnSeen UnDeleted"
assert data["folder"] == "INBOX"
assert data["sender"] == ""
assert data["subject"] == ""
assert data["date"] == datetime(
2023, 3, 24, 13, 52, tzinfo=timezone(timedelta(seconds=3600))
)
assert data["text"] == "Test body\r\n\r\n"
assert data["headers"]["Return-Path"] == ("<john.doe@example.com>",)
assert data["headers"]["Delivered-To"] == ("notify@example.com",)
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
@pytest.mark.parametrize(
("imap_login_state", "success"), [(AUTH, True), (NONAUTH, False)]
+14 -12
View File
@@ -102,12 +102,13 @@ async def test_select_option(hass: HomeAssistant) -> None:
state = hass.states.get(entity_id)
assert state.state == "another option"
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "non existing option"},
blocking=True,
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "non existing option"},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "another option"
@@ -305,12 +306,13 @@ async def test_set_options_service(hass: HomeAssistant) -> None:
state = hass.states.get(entity_id)
assert state.state == "test1"
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "first option"},
blocking=True,
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "first option"},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == "test1"
@@ -2,6 +2,7 @@
import pytest
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.state import async_reproduce_state
from homeassistant.setup import async_setup_component
@@ -60,7 +61,8 @@ async def test_reproducing_states(
assert hass.states.get(ENTITY).state == VALID_OPTION3
# Test setting state to invalid state
await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)])
with pytest.raises(HomeAssistantError):
await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)])
# The entity state should be unchanged
assert hass.states.get(ENTITY).state == VALID_OPTION3
+2 -2
View File
@@ -1,7 +1,7 @@
"""The tests for lastfm."""
from unittest.mock import patch
from pylast import Track, WSError
from pylast import PyLastError, Track
from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS
from homeassistant.const import CONF_API_KEY
@@ -65,7 +65,7 @@ class MockUser:
def get_friends(self):
"""Get mock friends."""
if self._has_friends is False:
raise WSError("network", "status", "Page not found")
raise PyLastError("network", "status", "Page not found")
return [MockUser(None, None, True, USERNAME_2)]
+1 -1
View File
@@ -95,7 +95,7 @@ async def test_async_browse_media(hass: HomeAssistant) -> None:
media = await media_source.async_browse_media(hass, const.URI_SCHEME)
assert isinstance(media, media_source.models.BrowseMediaSource)
assert len(media.children) == 1
assert media.children[0].title == "Local Media"
assert media.children[0].title == "My media"
async def test_async_resolve_media(hass: HomeAssistant) -> None:
+31 -2
View File
@@ -84,6 +84,8 @@ async def test_create_network(
with patch(
"python_otbr_api.OTBR.create_active_dataset"
) as create_dataset_mock, patch(
"python_otbr_api.OTBR.delete_active_dataset"
) as delete_dataset_mock, patch(
"python_otbr_api.OTBR.set_enabled"
) as set_enabled_mock, patch(
"python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16
@@ -99,6 +101,7 @@ async def test_create_network(
create_dataset_mock.assert_called_once_with(
python_otbr_api.models.ActiveDataSet(channel=15, network_name="home-assistant")
)
delete_dataset_mock.assert_called_once_with()
assert len(set_enabled_mock.mock_calls) == 2
assert set_enabled_mock.mock_calls[0][1][0] is False
assert set_enabled_mock.mock_calls[1][1][0] is True
@@ -151,7 +154,7 @@ async def test_create_network_fails_2(
), patch(
"python_otbr_api.OTBR.create_active_dataset",
side_effect=python_otbr_api.OTBRError,
):
), patch("python_otbr_api.OTBR.delete_active_dataset"):
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
msg = await websocket_client.receive_json()
@@ -171,6 +174,8 @@ async def test_create_network_fails_3(
side_effect=[None, python_otbr_api.OTBRError],
), patch(
"python_otbr_api.OTBR.create_active_dataset",
), patch(
"python_otbr_api.OTBR.delete_active_dataset"
):
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
msg = await websocket_client.receive_json()
@@ -191,6 +196,8 @@ async def test_create_network_fails_4(
), patch(
"python_otbr_api.OTBR.get_active_dataset_tlvs",
side_effect=python_otbr_api.OTBRError,
), patch(
"python_otbr_api.OTBR.delete_active_dataset"
):
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
msg = await websocket_client.receive_json()
@@ -208,7 +215,9 @@ async def test_create_network_fails_5(
"""Test create network."""
with patch("python_otbr_api.OTBR.set_enabled"), patch(
"python_otbr_api.OTBR.create_active_dataset"
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None):
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch(
"python_otbr_api.OTBR.delete_active_dataset"
):
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
msg = await websocket_client.receive_json()
@@ -216,6 +225,26 @@ async def test_create_network_fails_5(
assert msg["error"]["code"] == "get_active_dataset_tlvs_empty"
async def test_create_network_fails_6(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
otbr_config_entry,
websocket_client,
) -> None:
"""Test create network."""
with patch("python_otbr_api.OTBR.set_enabled"), patch(
"python_otbr_api.OTBR.create_active_dataset"
), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch(
"python_otbr_api.OTBR.delete_active_dataset",
side_effect=python_otbr_api.OTBRError,
):
await websocket_client.send_json_auto_id({"type": "otbr/create_network"})
msg = await websocket_client.receive_json()
assert not msg["success"]
assert msg["error"]["code"] == "delete_active_dataset_failed"
async def test_set_network(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
@@ -25,7 +25,6 @@ async def test_create(hass: HomeAssistant) -> None:
assert len(notifications) == 1
notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_UNREAD
assert notification["message"] == "Hello World 2"
assert notification["title"] == "2 beers"
assert notification["created_at"] is not None
@@ -66,39 +65,6 @@ async def test_dismiss_notification(hass: HomeAssistant) -> None:
assert len(notifications) == 0
async def test_mark_read(hass: HomeAssistant) -> None:
"""Ensure notification is marked as Read."""
notifications = pn._async_get_or_create_notifications(hass)
assert len(notifications) == 0
await hass.services.async_call(
pn.DOMAIN,
"create",
{"notification_id": "Beer 2", "message": "test"},
blocking=True,
)
assert len(notifications) == 1
notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_UNREAD
await hass.services.async_call(
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True
)
assert len(notifications) == 1
notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_READ
await hass.services.async_call(
pn.DOMAIN,
"dismiss",
{"notification_id": "Beer 2"},
blocking=True,
)
assert len(notifications) == 0
async def test_ws_get_notifications(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
@@ -128,19 +94,8 @@ async def test_ws_get_notifications(
assert notification["notification_id"] == "Beer 2"
assert notification["message"] == "test"
assert notification["title"] is None
assert notification["status"] == pn.STATUS_UNREAD
assert notification["created_at"] is not None
# Mark Read
await hass.services.async_call(
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}
)
await client.send_json({"id": 7, "type": "persistent_notification/get"})
msg = await client.receive_json()
notifications = msg["result"]
assert len(notifications) == 1
assert notifications[0]["status"] == pn.STATUS_READ
# Dismiss
pn.async_dismiss(hass, "Beer 2")
await client.send_json({"id": 8, "type": "persistent_notification/get"})
@@ -186,24 +141,8 @@ async def test_ws_get_subscribe(
assert notification["notification_id"] == "Beer 2"
assert notification["message"] == "test"
assert notification["title"] is None
assert notification["status"] == pn.STATUS_UNREAD
assert notification["created_at"] is not None
# Mark Read
await hass.services.async_call(
pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}
)
msg = await client.receive_json()
assert msg["id"] == 5
assert msg["type"] == "event"
assert msg["event"]
event = msg["event"]
assert event["type"] == "updated"
notifications = event["notifications"]
assert len(notifications) == 1
notification = notifications[list(notifications)[0]]
assert notification["status"] == pn.STATUS_READ
# Dismiss
pn.async_dismiss(hass, "Beer 2")
msg = await client.receive_json()
@@ -212,3 +151,27 @@ async def test_ws_get_subscribe(
assert msg["event"]
event = msg["event"]
assert event["type"] == "removed"
async def test_manual_notification_id_round_trip(hass: HomeAssistant) -> None:
"""Test that a manual notification id can be round tripped."""
notifications = pn._async_get_or_create_notifications(hass)
assert len(notifications) == 0
await hass.services.async_call(
pn.DOMAIN,
"create",
{"notification_id": "synology_diskstation_hub_notification", "message": "test"},
blocking=True,
)
assert len(notifications) == 1
await hass.services.async_call(
pn.DOMAIN,
"dismiss",
{"notification_id": "synology_diskstation_hub_notification"},
blocking=True,
)
assert len(notifications) == 0
+3 -2
View File
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .mock_data import BASE_URL, HOME_DATA, PROP, USER_DATA, USER_EMAIL
from .mock_data import BASE_URL, HOME_DATA, NETWORK_INFO, PROP, USER_DATA, USER_EMAIL
from tests.common import MockConfigEntry
@@ -54,7 +54,8 @@ async def setup_entry(
"homeassistant.components.roborock.RoborockApiClient.get_home_data",
return_value=HOME_DATA,
), patch(
"homeassistant.components.roborock.RoborockMqttClient.get_networking"
"homeassistant.components.roborock.RoborockMqttClient.get_networking",
return_value=NETWORK_INFO,
), patch(
"homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop",
return_value=PROP,
+4
View File
@@ -1,6 +1,8 @@
"""Mock data for Roborock tests."""
from __future__ import annotations
import datetime
from roborock.containers import (
CleanRecord,
CleanSummary,
@@ -320,6 +322,8 @@ DND_TIMER = DnDTimer.from_dict(
"enabled": 1,
}
)
DND_TIMER.start_time = datetime.datetime(year=2023, month=6, day=1, hour=22)
DND_TIMER.end_time = datetime.datetime(year=2023, month=6, day=2, hour=7)
STATUS = S7Status.from_dict(
{
@@ -0,0 +1,303 @@
# serializer version: 1
# name: test_diagnostics
dict({
'config_entry': dict({
'base_url': 'https://usiot.roborock.com',
'user_data': dict({
'avatarurl': 'https://files.roborock.com/iottest/default_avatar.png',
'country': 'US',
'countrycode': '1',
'nickname': 'user_nickname',
'region': 'us',
'rriot': dict({
'h': 'abc123',
'k': 'abc123',
'r': dict({
'a': 'https://api-us.roborock.com',
'l': 'https://wood-us.roborock.com',
'm': 'ssl://mqtt-us-2.roborock.com:8883',
'r': 'US',
}),
's': 'abc123',
'u': 'abc123',
}),
'rruid': '**REDACTED**',
'token': '**REDACTED**',
'tokentype': '',
'tuyaDeviceState': 2,
'uid': '**REDACTED**',
}),
'username': '**REDACTED**',
}),
'coordinators': dict({
'**REDACTED-0**': dict({
'api': dict({
}),
'roborock_device_info': dict({
'device': dict({
'activeTime': 1672364449,
'deviceStatus': dict({
'120': 0,
'121': 8,
'122': 100,
'123': 102,
'124': 203,
'125': 94,
'126': 90,
'127': 87,
'128': 0,
'133': 1,
}),
'duid': '**REDACTED**',
'extra': '{"RRPhotoPrivacyVersion": "1"}',
'featureSet': '2234201184108543',
'fv': '02.56.02',
'iconUrl': '',
'localKey': '**REDACTED**',
'name': 'Roborock S7 MaxV',
'newFeatureSet': '0000000000002041',
'online': True,
'productId': 'abc123',
'pv': '1.0',
'roomId': 2362003,
'share': False,
'silentOtaSwitch': True,
'sn': 'abc123',
'timeZoneId': 'America/Los_Angeles',
'tuyaMigrated': False,
}),
'network_info': dict({
'bssid': '**REDACTED**',
'ip': '123.232.12.1',
'mac': '**REDACTED**',
'rssi': 90,
'ssid': 'wifi',
}),
'product': dict({
'capability': 0,
'category': 'robot.vacuum.cleaner',
'code': 'a27',
'id': 'abc123',
'model': 'roborock.vacuum.a27',
'name': 'Roborock S7 MaxV',
'schema': list([
dict({
'code': 'rpc_request',
'id': '101',
'mode': 'rw',
'name': 'rpc_request',
'type': 'RAW',
}),
dict({
'code': 'rpc_response',
'id': '102',
'mode': 'rw',
'name': 'rpc_response',
'type': 'RAW',
}),
dict({
'code': 'error_code',
'id': '120',
'mode': 'ro',
'name': '错误代码',
'type': 'ENUM',
}),
dict({
'code': 'state',
'id': '121',
'mode': 'ro',
'name': '设备状态',
'type': 'ENUM',
}),
dict({
'code': 'battery',
'id': '122',
'mode': 'ro',
'name': '设备电量',
'type': 'ENUM',
}),
dict({
'code': 'fan_power',
'id': '123',
'mode': 'rw',
'name': '清扫模式',
'type': 'ENUM',
}),
dict({
'code': 'water_box_mode',
'id': '124',
'mode': 'rw',
'name': '拖地模式',
'type': 'ENUM',
}),
dict({
'code': 'main_brush_life',
'id': '125',
'mode': 'rw',
'name': '主刷寿命',
'type': 'VALUE',
}),
dict({
'code': 'side_brush_life',
'id': '126',
'mode': 'rw',
'name': '边刷寿命',
'type': 'VALUE',
}),
dict({
'code': 'filter_life',
'id': '127',
'mode': 'rw',
'name': '滤网寿命',
'type': 'VALUE',
}),
dict({
'code': 'additional_props',
'id': '128',
'mode': 'ro',
'name': '额外状态',
'type': 'RAW',
}),
dict({
'code': 'task_complete',
'id': '130',
'mode': 'ro',
'name': '完成事件',
'type': 'RAW',
}),
dict({
'code': 'task_cancel_low_power',
'id': '131',
'mode': 'ro',
'name': '电量不足任务取消',
'type': 'RAW',
}),
dict({
'code': 'task_cancel_in_motion',
'id': '132',
'mode': 'ro',
'name': '运动中任务取消',
'type': 'RAW',
}),
dict({
'code': 'charge_status',
'id': '133',
'mode': 'ro',
'name': '充电状态',
'type': 'RAW',
}),
dict({
'code': 'drying_status',
'id': '134',
'mode': 'ro',
'name': '烘干状态',
'type': 'RAW',
}),
]),
}),
'props': dict({
'cleanSummary': dict({
'cleanArea': 1159182500,
'cleanCount': 31,
'cleanTime': 74382,
'dustCollectionCount': 25,
'records': list([
1672543330,
1672458041,
]),
'squareMeterCleanArea': 1159.2,
}),
'consumable': dict({
'cleaningBrushWorkTimes': 65,
'dustCollectionWorkTimes': 25,
'filterElementWorkTime': 0,
'filterTimeLeft': 465618,
'filterWorkTime': 74382,
'mainBrushTimeLeft': 1005618,
'mainBrushWorkTime': 74382,
'sensorDirtyTime': 74382,
'sensorTimeLeft': 33618,
'sideBrushTimeLeft': 645618,
'sideBrushWorkTime': 74382,
'strainerWorkTimes': 65,
}),
'dndTimer': dict({
'enabled': 1,
'endHour': 7,
'endMinute': 0,
'endTime': '2023-06-02T07:00:00',
'startHour': 22,
'startMinute': 0,
'startTime': '2023-06-01T22:00:00',
}),
'lastCleanRecord': dict({
'area': 20965000,
'avoidCount': 19,
'begin': 1672543330,
'cleanType': 3,
'complete': 1,
'duration': 1176,
'dustCollectionStatus': 1,
'end': 1672544638,
'error': 0,
'finishReason': 56,
'mapFlag': 0,
'squareMeterArea': 21.0,
'startType': 2,
'washCount': 2,
}),
'status': dict({
'adbumperStatus': list([
0,
0,
0,
]),
'autoDustCollection': 1,
'avoidCount': 19,
'backType': -1,
'battery': 100,
'cameraStatus': 3457,
'chargeStatus': 1,
'cleanArea': 20965000,
'cleanTime': 1176,
'collisionAvoidStatus': 1,
'debugMode': 0,
'dndEnabled': 0,
'dockErrorStatus': 0,
'dockType': 3,
'dustCollectionStatus': 0,
'errorCode': 0,
'fanPower': 102,
'homeSecEnablePassword': 0,
'homeSecStatus': 0,
'inCleaning': 0,
'inFreshState': 1,
'inReturning': 0,
'isExploring': 0,
'isLocating': 0,
'labStatus': 1,
'lockStatus': 0,
'mapPresent': 1,
'mapStatus': 3,
'mopForbiddenEnable': 1,
'mopMode': 300,
'msgSeq': 458,
'msgVer': 2,
'squareMeterCleanArea': 21.0,
'state': 8,
'switchMapMode': 0,
'unsaveMapFlag': 0,
'unsaveMapReason': 0,
'washPhase': 0,
'washReady': 0,
'waterBoxCarriageStatus': 1,
'waterBoxMode': 203,
'waterBoxStatus': 1,
'waterShortageStatus': 0,
}),
}),
}),
}),
}),
})
# ---
@@ -0,0 +1,23 @@
"""Tests for the diagnostics data provided by the Roborock integration."""
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
bypass_api_fixture,
setup_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics for config entry."""
result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry)
assert isinstance(result, dict)
assert result == snapshot

Some files were not shown because too many files have changed in this diff Show More