Compare commits

...

93 Commits

Author SHA1 Message Date
Franck Nijhof
dc7f0fb21c 2022.11.3 (#82197) 2022-11-16 21:00:42 +01:00
J. Nick Koston
74c2639495 Fix missing await in nexia emergency heat (#82207)
fixes undefined
2022-11-16 19:03:01 +01:00
J. Nick Koston
b0714e32b1 Fix static version in homekit tests (#82201) 2022-11-16 16:38:09 +01:00
Franck Nijhof
6371cb4ebd Bumped version to 2022.11.3 2022-11-16 15:43:18 +01:00
muppet3000
987add50cb Fix Growatt incorrect energy dashboard values for grid import (#82163)
* Fix Growatt incorrect energy dashboard values for grid import (#80905)

* Growatt - addressing review comments (#80905)

* Growatt - addressing more review comments (#80905)
2022-11-16 15:42:57 +01:00
Robert Hillis
83db9a3335 Fix Google Sheets formula input (#82157) 2022-11-16 15:42:53 +01:00
Robert Hillis
aade51248d Add missing strings in Onvif (#82141) 2022-11-16 15:42:49 +01:00
Jan Bouwhuis
228fa9f5a0 Always update attributes on an update for MQTT update (#82139) 2022-11-16 15:42:46 +01:00
Franck Nijhof
57c868e615 Update sqlalchemy to 1.4.44 (#82129) 2022-11-16 15:42:42 +01:00
J. Nick Koston
431f93e1d3 Bump PySwitchbot to 0.20.5 (#82099) 2022-11-16 15:42:38 +01:00
muppet3000
66d3891a37 Bump growattServer to 1.2.4 (#82071)
Growatt - Library bump to workaround for #81951
2022-11-16 15:42:33 +01:00
Avi Miller
2a641d1d19 Restore color_temp handling for lifx.set_state (#82067)
fixes undefined
2022-11-16 15:42:29 +01:00
J. Nick Koston
f8b5a97e72 Bump pySwitchbot to 0.20.4 (#82055)
Fixes for curtain firmware v6

changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.3...0.20.4
2022-11-16 15:42:25 +01:00
J. Nick Koston
609438d929 Make sure the config_flow key is set for brands (#82038)
Fixes https://github.com/home-assistant/frontend/issues/14376
2022-11-16 15:42:22 +01:00
Vincent Giorgi
76cc26ad17 Bump airthings-ble to 0.5.3 (#82033)
Bump airthings-ble
2022-11-16 15:42:18 +01:00
Allen Porter
533efa2880 Bump gcal_sync 4.0.2 (#82017) 2022-11-16 15:42:15 +01:00
J. Nick Koston
3bf3a1fd85 Bump oralb-ble to 0.14.2 (#81969)
fixes detection of the black 9000 model

fixes #81967

changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.14.1...v0.14.2
2022-11-16 15:42:11 +01:00
Jeef
5306b32a48 Increasing device usage update interval for Flume (#81968) 2022-11-16 15:42:08 +01:00
David F. Mulcahey
4e82f134b2 Bump ZHA quirks lib to 0.0.86 (#81966) 2022-11-16 15:42:04 +01:00
Diogo Gomes
0457a74428 Fix ONVIF subscription errors (#81965)
fixes undefined
2022-11-16 15:42:01 +01:00
J. Nick Koston
8f3449d942 Bump PySwitchbot to 0.20.3 (#81938)
changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.2...0.20.3

releases the connection sooner to reduce the risk
of running out of connection slots on the ble adapter
2022-11-16 15:41:57 +01:00
J. Nick Koston
97929bd234 Bump bleak-retry-connector to 2.8.4 (#81937)
changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.3...v2.8.4

reduces the risk that the operation will fail because the adapter
temporarily runs out of connection slots
2022-11-16 15:41:52 +01:00
J. Nick Koston
a9d461a109 Fix esphome bleak client seeing disconnects too late (#81932)
* Fix esphome bleak client seeing disconnects too late

Because allbacks are delivered asynchronously its possible
that we find out during the operation before the callback
is delivered telling us about the disconnected.

We now watch for error code -1 which indicates the connection
dropped out from under us

* debug logging

* cleanup comment

* Fix comment grammar

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-11-16 15:41:49 +01:00
Aaron Bach
f24549f7d1 Bump aioridwell to 2022.11.0 (#81929) 2022-11-16 15:41:44 +01:00
J. Nick Koston
0d62d80038 Fix bluetooth adapters with missing firmware patch files not being discovered (#81926) 2022-11-16 15:41:40 +01:00
Allen Porter
223d864b04 Revert google calendar back to old API for free/busy readers (#81894)
* Revert google calendar back to old API for free/busy readers

* Update homeassistant/components/google/calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-11-16 15:41:36 +01:00
David F. Mulcahey
082d4079ef Fix ZHA configuration APIs (#81874)
* Fix ZHA configuration loading and saving issues

* add tests
2022-11-16 15:41:32 +01:00
Allen Porter
70b360b1f8 Bump gcal_sync to 4.0.1 to fix Google Calendar config flow (#81873)
Bump gcal_sync to 4.0.1

This reverts test chagnes from PR #81562 that were actually incorrect given
the calendar "get" API returns less information that the "CalendarList" api.
2022-11-16 15:41:28 +01:00
J. Nick Koston
5488e9d5f3 Bump oralb-ble to 0.14.1 (#81869) 2022-11-16 15:41:25 +01:00
Jc2k
c8177f48ce Fix homekit_controller climate entity not becoming active when changing modes (#81868) 2022-11-16 15:41:21 +01:00
J. Nick Koston
930dc3615e Bump aiohomekit to 2.2.19 (#81867) 2022-11-16 15:41:18 +01:00
epenet
1ecb7ab887 Fix rest schema (#81857) 2022-11-16 15:41:14 +01:00
Erik Montnemery
248ed3660f Fix statistic_during_period for data with holes (#81847) 2022-11-16 15:41:10 +01:00
J. Nick Koston
3a60466e7c Fix switchbot not becoming available again after unavailable (#81822)
* Fix switchbot not becoming available again after unavailable

If the advertisment was the same and we were previously
marked as unavailable we would not mark the device as
available again until the advertisment changed. For lights
there is a counter but for the bots there is no counter
which means the bots would show unavailable even though
they were available again

* naming

* naming
2022-11-16 15:41:07 +01:00
Phil Bruckner
04fda5638c Change life360 timeouts & retries (#81799)
Change from single timeout of 10 to socket timeout of 15, total timeout of 60, and retry up to 3 times.

Bump life360 package to 5.3.0.
2022-11-16 15:41:03 +01:00
Ville Skyttä
252941ae26 Upgrade huawei-lte-api to 1.6.7, fixes empty username issues (#81751)
Recentish versions of huawei-lte-api behave differently with regards to
empty/default username compared to the older versions this integration
was originally written against. 1.6.5+ changes the behavior so that our
existing implementation works as expected when no username is supplied
for the config entry.

https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.4
https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.5
https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.6
https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.7
2022-11-16 15:40:59 +01:00
Oliver Dippel
2eacbef061 Fix ibeacon source attribute not being updated (#81740)
fixes undefined
2022-11-16 15:40:55 +01:00
chpego
3f666396c9 Fix Fully Kiosk start application service field (#81738)
Fix attributes services to start_application
2022-11-16 15:40:52 +01:00
rappenze
7d20bb0532 Fix accelerator sensor in fibaro integration (#81237)
* Fix accelerator sensor in fibaro integration

* Implement suggestions from code review

* Implement suggestions from code review

* Changes as suggested in code review

* Adjust as suggested in code review
2022-11-16 15:40:48 +01:00
J. Nick Koston
d94e969dc1 Fix instability with HomeKit trigger accessories (#80703)
fixes https://github.com/home-assistant/core/issues/78774
fixes https://github.com/home-assistant/core/issues/81685
2022-11-16 15:40:44 +01:00
Yukon Vinecki
18842ef571 Fix Z-Wave JS cover stop support (#78723)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-11-16 15:40:38 +01:00
Robert Svensson
f9ebbb936a Fix UniFi block client switches on 2022.11.2 (#81884)
fixes undefined
2022-11-15 18:30:56 +01:00
Franck Nijhof
c757c9b99f 2022.11.2 (#81780) 2022-11-08 17:41:05 +01:00
J. Nick Koston
d88b2bf19c Fix off by one in HomeKit iid allocator (#81793) 2022-11-08 16:24:15 +01:00
ztamas83
7ab2029071 Retry tibber setup (#81785)
* Handle integration setup retries

* Fix black error

* Update to falsy check

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove duplicated log

* Update exception message

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-11-08 16:24:11 +01:00
Bram Kragten
59ec829106 Update frontend to 20221108.0 (#81787) 2022-11-08 15:01:23 +01:00
epenet
345d356e9a Fix rest import (#81784) 2022-11-08 13:35:12 +01:00
Franck Nijhof
42c09d8811 Bumped version to 2022.11.2 2022-11-08 12:44:44 +01:00
Allen Porter
f614df29bd Partially revert google local sync for search cases (#81761)
fixes undefined
2022-11-08 12:43:39 +01:00
J. Nick Koston
815249eaeb Use more efficient async_progress_by_handler call in async_start_reauth (#81757) 2022-11-08 12:43:36 +01:00
J. Nick Koston
1f878433ac Fix check for duplicate config entry reauth when context is passed or augmented (#81753)
fixes https://github.com/home-assistant/core/issues/77578
2022-11-08 12:43:32 +01:00
epenet
797ea3ace4 Adjust REST schema validation (#81723)
fixes undefined
2022-11-08 12:43:28 +01:00
J. Nick Koston
3e8bea8fbd Fix flapping logbook tests (#81695) 2022-11-08 12:43:25 +01:00
Robert Hillis
b9db84ed57 Bump aiopyarr to 22.11.0 (#81694) 2022-11-08 12:43:21 +01:00
J. Nick Koston
823ec88c52 Bump aiohomekit to 2.2.18 (#81693) 2022-11-08 12:43:18 +01:00
Simone Chemelli
34ae83b4e2 Restore negative values for shelly power factors (#81689)
fixes undefined
2022-11-08 12:43:15 +01:00
J. Nick Koston
efb984aa83 Bump bleak to 0.19.2 (#81688) 2022-11-08 12:43:11 +01:00
J. Nick Koston
7ca5bd341b Bump aioesphomeapi to 11.4.3 (#81676) 2022-11-08 12:43:08 +01:00
J. Nick Koston
c8ed3fd302 Bump bleak-retry-connector to 2.8.3 (#81675)
Improves chances of making a BLE connection with the ESP32s

changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.2...v2.8.3
2022-11-08 12:43:05 +01:00
J. Nick Koston
a8a3f012f6 Bump bluetooth-adapters to 0.7.0 (#81576)
changelog: https://github.com/Bluetooth-Devices/bluetooth-adapters/releases/tag/v0.7.0
2022-11-08 12:43:00 +01:00
Aaron Bach
11013bd780 Fix missing RainMachine restrictions switches (#81673) 2022-11-08 12:33:17 +01:00
J. Nick Koston
2684a6e8ed Bump aiohomekit to 2.2.17 (#81657)
Improve BLE pairing reliability, especially with esp32 proxies

changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.16...2.2.17
2022-11-08 12:33:13 +01:00
Artem Draft
63afb30f57 Fix Bravia TV options flow when device is off (#81644)
* Fix options flow when tv is off

* abort with message
2022-11-08 12:33:10 +01:00
Tobias Sauerwein
f861137de4 Bump pyatmo to 7.4.0 (#81636) 2022-11-08 12:33:06 +01:00
J. Nick Koston
08debee94f Add missing h2 dep to iaqualink (#81630)
fixes https://github.com/home-assistant/core/issues/81439
2022-11-08 12:33:02 +01:00
Robert Svensson
c8981f78b7 Fix situation where deCONZ sensor platform setup would fail (#81629)
* Fix situation where deCONZ sensor platform setup would fail

* Don't use try
2022-11-08 12:32:58 +01:00
J. Nick Koston
e4269ff8b2 Fix creating multiple ElkM1 systems with TLS 1.2 (#81627)
fixes https://github.com/home-assistant/core/issues/81516
2022-11-08 12:32:55 +01:00
J. Nick Koston
6fb5c93182 Bump oralb-ble to 0.13.0 (#81622)
* Bump oralb-ble to 0.11.1

adds some more missing pressure mappings

changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.2...v0.11.1

* bump again to update for additional reports

* bump again for more data from issue reports
2022-11-08 12:32:51 +01:00
J. Nick Koston
6fa69022f4 Bump aiohomekit to 2.2.16 (#81621) 2022-11-08 12:32:48 +01:00
J. Nick Koston
4391640734 Ignore unspecified addresses from zeroconf (#81620) 2022-11-08 12:32:44 +01:00
Tim Rightnour
c60c99bd74 Bump venstarcolortouch to 0.19 to fix API rev 3 devices (#81614) 2022-11-08 12:32:41 +01:00
J. Nick Koston
ac15f2cf9d Fix homekit bridge iid allocations (#81613)
fixes undefined
2022-11-08 12:32:35 +01:00
Bouwe Westerdijk
b9757235a7 Bump plugwise to v0.25.7 (#81612) 2022-11-08 12:32:31 +01:00
Steven Looman
9beb9f6fc0 Fix repeating SSDP errors by checking address scope_ids and proper hostname (#81611) 2022-11-08 12:32:27 +01:00
David F. Mulcahey
9771147a1e Fix invalid min and max color temp in bad ZHA light devices (#81604)
* Fix ZHA default color temps

* update test
2022-11-08 12:32:24 +01:00
Sebastian Muszynski
0983f8aadf Bump PyXiaomiGateway to 0.14.3 (#81603)
Fixes: #80249
2022-11-08 12:32:20 +01:00
Maciej Bieniek
5a6423a944 Always use Celsius in Shelly integration, part 2 (#81602)
* Always use Celsius in Shelly integration

* Update homeassistant/components/shelly/sensor.py

Co-authored-by: Aarni Koskela <akx@iki.fi>

* Restore unit from the registry during HA startup

Co-authored-by: Aarni Koskela <akx@iki.fi>
2022-11-08 12:32:16 +01:00
David F. Mulcahey
f9c7732090 Bump ZHA quirks and associated changes (#81587) 2022-11-08 12:32:13 +01:00
J. Nick Koston
55c87c733a Ensure HomeKit temperature controls appear before fan controls on thermostat accessories (#81586) 2022-11-08 12:32:09 +01:00
J. Nick Koston
6110700e18 Fix HomeKit reset accessory procedure (#81573)
fixes https://github.com/home-assistant/core/issues/81571
2022-11-08 12:32:06 +01:00
Nathan Spencer
a8e1afb966 Bump pylitterbot to 2022.11.0 (#81572) 2022-11-08 12:32:02 +01:00
Klaas Schoute
d24e272d5e Fix watermeter issue for old P1 Monitor versions (#81570)
* Bump the python package version

* Add exception to check if user has a water meter
2022-11-08 12:31:59 +01:00
Shay Levy
bf5ecc30ed Fix Shelly Plus HT missing battery entity (#81564) 2022-11-08 12:31:55 +01:00
Allen Porter
2a34d3a56f Bump gcal_sync to 4.0.0 (#81562)
* Bump gcal_sync to 2.2.4

* Bump gcal sync to 4.0.0

* Add Calendar accessRole fields which are now required
2022-11-08 12:31:52 +01:00
Aaron Bach
7124cedd7a Bump pyairvisual to 2022.11.1 (#81556) 2022-11-08 12:31:46 +01:00
J. Nick Koston
087ede959d Bump oralb-ble to 0.10.2 (#81537)
Fixes some more missing pressure mappings

changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.1...v0.10.2
2022-11-08 12:31:43 +01:00
J. Nick Koston
7b769b39c2 Add additional coverage for adding multiple elkm1 instances (#81528)
* Add additional coverage for adding multiple elkm1 instances

* fix copy error
2022-11-08 12:31:40 +01:00
Avi Miller
51ab5d1808 Fix lifx.set_state so it works with kelvin and color_temp_kelvin and color names (#81515) 2022-11-08 12:31:36 +01:00
J. Nick Koston
7832a7fd80 Bump oralb-ble to 0.10.1 (#81491)
fixes #81489

changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.0...v0.10.1
2022-11-08 12:31:33 +01:00
J. Nick Koston
c93c13d8bf Bump nexia to 2.0.6 (#81474)
* Bump nexia to 2.0.6

- Marks thermostat unavailable when it is offline

* is property
2022-11-08 12:31:30 +01:00
J. Nick Koston
42444872b9 Align esphome ble client notify behavior to match BlueZ (#81463) 2022-11-08 12:31:26 +01:00
Steven Looman
d3bd80b876 Fix ignored upnp discoveries not being matched when device changes its unique identifier (#81240)
Fixes https://github.com/home-assistant/core/issues/78454
fixes undefined
2022-11-08 12:31:20 +01:00
epenet
a53d1e072d Fix scrape scan interval (#81763) 2022-11-08 10:56:08 +01:00
130 changed files with 3541 additions and 653 deletions

View File

@@ -3,7 +3,7 @@
"name": "Airthings BLE",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"requirements": ["airthings-ble==0.5.2"],
"requirements": ["airthings-ble==0.5.3"],
"dependencies": ["bluetooth"],
"codeowners": ["@vincegio"],
"iot_class": "local_polling",

View File

@@ -7,13 +7,9 @@ from math import ceil
from typing import Any
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
UnauthorizedError,
)
from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (

View File

@@ -6,14 +6,14 @@ from collections.abc import Mapping
from typing import Any
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
from pyairvisual.cloud_api import (
InvalidKeyError,
KeyExpiredError,
NodeProError,
NotFoundError,
UnauthorizedError,
)
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
import voluptuous as vol
from homeassistant import config_entries

View File

@@ -3,7 +3,7 @@
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==2022.07.0"],
"requirements": ["pyairvisual==2022.11.1"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"],

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from asyncio import Future
from collections.abc import Callable, Iterable
import datetime
import logging
import platform
from typing import TYPE_CHECKING, cast
@@ -21,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, discovery_flow
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
@@ -33,6 +35,7 @@ from .const import (
ADAPTER_ADDRESS,
ADAPTER_HW_VERSION,
ADAPTER_SW_VERSION,
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_ADAPTER,
CONF_DETAILS,
CONF_PASSIVE,
@@ -40,6 +43,7 @@ from .const import (
DEFAULT_ADDRESS,
DOMAIN,
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
SOURCE_LOCAL,
AdapterDetails,
)
@@ -298,9 +302,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await async_discover_adapters(hass, discovered_adapters)
discovery_debouncer = Debouncer(
hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters
hass,
_LOGGER,
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
immediate=False,
function=_async_rediscover_adapters,
)
async def _async_call_debouncer(now: datetime.datetime) -> None:
"""Call the debouncer at a later time."""
await discovery_debouncer.async_call()
def _async_trigger_discovery() -> None:
# There are so many bluetooth adapter models that
# we check the bus whenever a usb device is plugged in
@@ -310,6 +322,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# present.
_LOGGER.debug("Triggering bluetooth usb discovery")
hass.async_create_task(discovery_debouncer.async_call())
# Because it can take 120s for the firmware loader
# fallback to timeout we need to wait that plus
# the debounce time to ensure we do not miss the
# adapter becoming available to DBus since otherwise
# we will never see the new adapter until
# Home Assistant is restarted
async_call_later(
hass,
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
_async_call_debouncer,
)
cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery)
hass.bus.async_listen_once(

View File

@@ -59,6 +59,15 @@ SCANNER_WATCHDOG_TIMEOUT: Final = 90
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30)
# When the linux kernel is configured with
# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it
# can take up to 120s before the USB device
# is available if the firmware files
# are not present
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5
class AdapterDetails(TypedDict, total=False):
"""Adapter details."""

View File

@@ -6,9 +6,9 @@
"after_dependencies": ["hassio"],
"quality_scale": "internal",
"requirements": [
"bleak==0.19.1",
"bleak-retry-connector==2.8.2",
"bluetooth-adapters==0.6.0",
"bleak==0.19.2",
"bleak-retry-connector==2.8.4",
"bluetooth-adapters==0.7.0",
"bluetooth-auto-recovery==0.3.6",
"dbus-fast==1.61.1"
],

View File

@@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
self.config_entry.entry_id
]
await coordinator.async_update_sources()
try:
await coordinator.async_update_sources()
except BraviaTVError:
return self.async_abort(reason="failed_update")
sources = coordinator.source_map.values()
self.source_list = [item["title"] for item in sources]
return await self.async_step_user()

View File

@@ -48,6 +48,9 @@
"ignored_sources": "List of ignored sources"
}
}
},
"abort": {
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
}
}
}

View File

@@ -41,6 +41,9 @@
}
},
"options": {
"abort": {
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
},
"step": {
"user": {
"data": {

View File

@@ -89,6 +89,7 @@ T = TypeVar(
class DeconzSensorDescriptionMixin(Generic[T]):
"""Required values when describing secondary sensor attributes."""
supported_fn: Callable[[T], bool]
update_key: str
value_fn: Callable[[T], datetime | StateType]
@@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi
ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
DeconzSensorDescription[AirQuality](
key="air_quality",
supported_fn=lambda device: device.air_quality is not None,
update_key="airquality",
value_fn=lambda device: device.air_quality,
instance_check=AirQuality,
@@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[AirQuality](
key="air_quality_ppb",
supported_fn=lambda device: device.air_quality_ppb is not None,
update_key="airqualityppb",
value_fn=lambda device: device.air_quality_ppb,
instance_check=AirQuality,
@@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Consumption](
key="consumption",
supported_fn=lambda device: device.consumption is not None,
update_key="consumption",
value_fn=lambda device: device.scaled_consumption,
instance_check=Consumption,
@@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Daylight](
key="daylight_status",
supported_fn=lambda device: True,
update_key="status",
value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status],
instance_check=Daylight,
@@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[GenericStatus](
key="status",
supported_fn=lambda device: device.status is not None,
update_key="status",
value_fn=lambda device: device.status,
instance_check=GenericStatus,
),
DeconzSensorDescription[Humidity](
key="humidity",
supported_fn=lambda device: device.humidity is not None,
update_key="humidity",
value_fn=lambda device: device.scaled_humidity,
instance_check=Humidity,
@@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[LightLevel](
key="light_level",
supported_fn=lambda device: device.light_level is not None,
update_key="lightlevel",
value_fn=lambda device: device.scaled_light_level,
instance_check=LightLevel,
@@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Power](
key="power",
supported_fn=lambda device: device.power is not None,
update_key="power",
value_fn=lambda device: device.power,
instance_check=Power,
@@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Pressure](
key="pressure",
supported_fn=lambda device: device.pressure is not None,
update_key="pressure",
value_fn=lambda device: device.pressure,
instance_check=Pressure,
@@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Temperature](
key="temperature",
supported_fn=lambda device: device.temperature is not None,
update_key="temperature",
value_fn=lambda device: device.scaled_temperature,
instance_check=Temperature,
@@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[Time](
key="last_set",
supported_fn=lambda device: device.last_set is not None,
update_key="lastset",
value_fn=lambda device: dt_util.parse_datetime(device.last_set),
instance_check=Time,
@@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[SensorResources](
key="battery",
supported_fn=lambda device: device.battery is not None,
update_key="battery",
value_fn=lambda device: device.battery,
name_suffix="Battery",
@@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
),
DeconzSensorDescription[SensorResources](
key="internal_temperature",
supported_fn=lambda device: device.internal_temperature is not None,
update_key="temperature",
value_fn=lambda device: device.internal_temperature,
name_suffix="Temperature",
@@ -268,7 +282,7 @@ async def async_setup_entry(
continue
no_sensor_data = False
if description.value_fn(sensor) is None:
if not description.supported_fn(sensor):
no_sensor_data = True
if description.instance_check is None:

View File

@@ -3,7 +3,7 @@
"name": "DLNA Digital Media Renderer",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@@ -3,7 +3,7 @@
"name": "DLNA Digital Media Server",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["ssdp"],
"after_dependencies": ["media_source"],
"ssdp": [

View File

@@ -7,11 +7,11 @@ import logging
import re
from types import MappingProxyType
from typing import Any, cast
from urllib.parse import urlparse
import async_timeout
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk
from elkm1_lib.util import parse_url
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema(
)
def hostname_from_url(url: str) -> str:
"""Return the hostname from a url."""
return parse_url(url)[1]
def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"):
@@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elk-M1 Control from a config entry."""
conf: MappingProxyType[str, Any] = entry.data
host = urlparse(entry.data[CONF_HOST]).hostname
host = hostname_from_url(entry.data[CONF_HOST])
_LOGGER.debug("Setting up elkm1 %s", conf["host"])

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
import logging
from typing import Any
from urllib.parse import urlparse
from elkm1_lib.discovery import ElkSystem
from elkm1_lib.elk import Elk
@@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util.network import is_ip_address
from . import async_wait_for_elk_to_sync
from . import async_wait_for_elk_to_sync, hostname_from_url
from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT
from .discovery import (
_short_mac,
@@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for entry in self._async_current_entries(include_ignore=False):
if (
entry.unique_id == mac
or urlparse(entry.data[CONF_HOST]).hostname == host
or hostname_from_url(entry.data[CONF_HOST]) == host
):
if async_update_entry_from_discovery(self.hass, entry, device):
self.hass.async_create_task(
@@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
current_unique_ids = self._async_current_ids()
current_hosts = {
urlparse(entry.data[CONF_HOST]).hostname
hostname_from_url(entry.data[CONF_HOST])
for entry in self._async_current_entries(include_ignore=False)
}
discovered_devices = await async_discover_devices(
@@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self._url_already_configured(url):
return self.async_abort(reason="address_already_configured")
host = urlparse(url).hostname
host = hostname_from_url(url)
_LOGGER.debug(
"Importing is trying to fill unique id from discovery for %s", host
)
@@ -367,10 +366,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _url_already_configured(self, url: str) -> bool:
"""See if we already have a elkm1 matching user input configured."""
existing_hosts = {
urlparse(entry.data[CONF_HOST]).hostname
hostname_from_url(entry.data[CONF_HOST])
for entry in self._async_current_entries()
}
return urlparse(url).hostname in existing_hosts
return hostname_from_url(url) in existing_hosts
class InvalidAuth(exceptions.HomeAssistantError):

View File

@@ -13,6 +13,7 @@ from aioesphomeapi import (
BLEConnectionError,
)
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
from aioesphomeapi.core import BluetoothGATTAPIError
import async_timeout
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, NotifyCallback
@@ -83,6 +84,24 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
return await func(self, *args, **kwargs)
except TimeoutAPIError as err:
raise asyncio.TimeoutError(str(err)) from err
except BluetoothGATTAPIError as ex:
# If the device disconnects in the middle of an operation
# be sure to mark it as disconnected so any library using
# the proxy knows to reconnect.
#
# Because callbacks are delivered asynchronously it's possible
# that we find out about the disconnection during the operation
# before the callback is delivered.
if ex.error.error == -1:
_LOGGER.debug(
"%s: %s - %s: BLE device disconnected during %s operation",
self._source, # pylint: disable=protected-access
self._ble_device.name, # pylint: disable=protected-access
self._ble_device.address, # pylint: disable=protected-access
func.__name__,
)
self._async_ble_device_disconnected() # pylint: disable=protected-access
raise BleakError(str(ex)) from ex
except APIConnectionError as err:
raise BleakError(str(err)) from err
@@ -137,6 +156,7 @@ class ESPHomeClient(BaseBleakClient):
was_connected = self._is_connected
self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
self._is_connected = False
self._notify_cancels.clear()
if self._disconnected_event:
self._disconnected_event.set()
self._disconnected_event = None
@@ -463,12 +483,20 @@ class ESPHomeClient(BaseBleakClient):
UUID or directly by the BleakGATTCharacteristic object representing it.
callback (function): The function to be called on notification.
"""
ble_handle = characteristic.handle
if ble_handle in self._notify_cancels:
raise BleakError(
"Notifications are already enabled on "
f"service:{characteristic.service_uuid} "
f"characteristic:{characteristic.uuid} "
f"handle:{ble_handle}"
)
cancel_coro = await self._client.bluetooth_gatt_start_notify(
self._address_as_int,
characteristic.handle,
ble_handle,
lambda handle, data: callback(data),
)
self._notify_cancels[characteristic.handle] = cancel_coro
self._notify_cancels[ble_handle] = cancel_coro
@api_error_as_bleak_error
async def stop_notify(
@@ -483,5 +511,7 @@ class ESPHomeClient(BaseBleakClient):
directly by the BleakGATTCharacteristic object representing it.
"""
characteristic = self._resolve_characteristic(char_specifier)
coro = self._notify_cancels.pop(characteristic.handle)
await coro()
# Do not raise KeyError if notifications are not enabled on this characteristic
# to be consistent with the behavior of the BlueZ backend
if coro := self._notify_cancels.pop(characteristic.handle, None):
await coro()

View File

@@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==11.4.2"],
"requirements": ["aioesphomeapi==11.4.3"],
"zeroconf": ["_esphomelib._tcp.local."],
"dhcp": [{ "registered_devices": true }],
"codeowners": ["@OttoWinter", "@jesserockz"],

View File

@@ -84,6 +84,7 @@ FIBARO_TYPEMAP = {
"com.fibaro.thermostatDanfoss": Platform.CLIMATE,
"com.fibaro.doorLock": Platform.LOCK,
"com.fibaro.binarySensor": Platform.BINARY_SENSOR,
"com.fibaro.accelerometer": Platform.BINARY_SENSOR,
}
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema(

View File

@@ -1,6 +1,8 @@
"""Support for Fibaro binary sensors."""
from __future__ import annotations
from collections.abc import Mapping
import json
from typing import Any
from homeassistant.components.binary_sensor import (
@@ -28,6 +30,11 @@ SENSOR_TYPES = {
"com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE],
"com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION],
"com.fibaro.heatDetector": ["Heat", "mdi:fire", BinarySensorDeviceClass.HEAT],
"com.fibaro.accelerometer": [
"Moving",
"mdi:axis-arrow",
BinarySensorDeviceClass.MOVING,
],
}
@@ -55,15 +62,50 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
"""Initialize the binary_sensor."""
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None
self._own_extra_state_attributes: Mapping[str, Any] = {}
self._fibaro_sensor_type = None
if fibaro_device.type in SENSOR_TYPES:
stype = fibaro_device.type
self._fibaro_sensor_type = fibaro_device.type
elif fibaro_device.baseType in SENSOR_TYPES:
stype = fibaro_device.baseType
if stype:
self._attr_device_class = SENSOR_TYPES[stype][2]
self._attr_icon = SENSOR_TYPES[stype][1]
self._fibaro_sensor_type = fibaro_device.baseType
if self._fibaro_sensor_type:
self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2]
self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1]
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the extra state attributes of the device."""
return super().extra_state_attributes | self._own_extra_state_attributes
def update(self) -> None:
"""Get the latest data and update the state."""
self._attr_is_on = self.current_binary_state
if self._fibaro_sensor_type == "com.fibaro.accelerometer":
# Accelerator sensors have values for the three axis x, y and z
moving_values = self._get_moving_values()
self._attr_is_on = self._is_moving(moving_values)
self._own_extra_state_attributes = self._get_xyz_moving(moving_values)
else:
self._attr_is_on = self.current_binary_state
def _get_xyz_moving(self, moving_values: Mapping[str, Any]) -> Mapping[str, Any]:
"""Return x y z values of the accelerator sensor value."""
attrs = {}
for axis_name in ("x", "y", "z"):
attrs[axis_name] = float(moving_values[axis_name])
return attrs
def _is_moving(self, moving_values: Mapping[str, Any]) -> bool:
"""Return that a moving is detected when one axis reports a value."""
for axis_name in ("x", "y", "z"):
if float(moving_values[axis_name]) != 0:
return True
return False
def _get_moving_values(self) -> Mapping[str, Any]:
"""Get the moving values of the accelerator sensor in a dict."""
value = self.fibaro_device.properties.value
if isinstance(value, str):
# HC2 returns dict as str
return json.loads(value)
# HC3 returns a real dict
return value

View File

@@ -17,7 +17,7 @@ DEFAULT_NAME = "Flume Sensor"
# Flume API limits individual endpoints to 120 queries per hour
NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1)
DEVICE_SCAN_INTERVAL = timedelta(minutes=1)
DEVICE_SCAN_INTERVAL = timedelta(minutes=5)
DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1)
_LOGGER = logging.getLogger(__package__)

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20221102.1"],
"requirements": ["home-assistant-frontend==20221108.0"],
"dependencies": [
"api",
"auth",

View File

@@ -20,7 +20,7 @@ start_application:
device:
integration: fully_kiosk
fields:
url:
application:
name: Application
description: Package name of the application to start.
example: "de.ozerov.fully"

View File

@@ -3,13 +3,14 @@
from __future__ import annotations
import asyncio
from collections.abc import Iterable
from datetime import datetime, timedelta
import logging
from typing import Any
from gcal_sync.api import SyncEventsRequest
from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest
from gcal_sync.exceptions import ApiException
from gcal_sync.model import DateOrDatetime, Event
from gcal_sync.model import AccessRole, DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore
from gcal_sync.sync import CalendarEventSyncManager
from gcal_sync.timeline import Timeline
@@ -196,21 +197,36 @@ async def async_setup_entry(
entity_registry.async_remove(
entity_entry.entity_id,
)
request_template = SyncEventsRequest(
calendar_id=calendar_id,
search=data.get(CONF_SEARCH),
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
)
sync = CalendarEventSyncManager(
calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name),
request_template=request_template,
)
coordinator = CalendarUpdateCoordinator(
hass,
sync,
data[CONF_NAME],
)
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
# Prefer calendar sync down of resources when possible. However, sync does not work
# for search. Also free-busy calendars denormalize recurring events as individual
# events which is not efficient for sync
if (
search := data.get(CONF_SEARCH)
or calendar_item.access_role == AccessRole.FREE_BUSY_READER
):
coordinator = CalendarQueryUpdateCoordinator(
hass,
calendar_service,
data[CONF_NAME],
calendar_id,
search,
)
else:
request_template = SyncEventsRequest(
calendar_id=calendar_id,
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
)
sync = CalendarEventSyncManager(
calendar_service,
store=ScopedCalendarStore(store, unique_id or entity_name),
request_template=request_template,
)
coordinator = CalendarSyncUpdateCoordinator(
hass,
sync,
data[CONF_NAME],
)
entities.append(
GoogleCalendarEntity(
coordinator,
@@ -242,8 +258,8 @@ async def async_setup_entry(
)
class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls."""
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls that use an efficient sync."""
def __init__(
self,
@@ -251,7 +267,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
sync: CalendarEventSyncManager,
name: str,
) -> None:
"""Create the Calendar event device."""
"""Create the CalendarSyncUpdateCoordinator."""
super().__init__(
hass,
_LOGGER,
@@ -271,6 +287,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
dt_util.DEFAULT_TIME_ZONE
)
async def async_get_events(
self, start_date: datetime, end_date: datetime
) -> Iterable[Event]:
"""Get all events in a specific time frame."""
if not self.data:
raise HomeAssistantError(
"Unable to get events: Sync from server has not completed"
)
return self.data.overlapping(
dt_util.as_local(start_date),
dt_util.as_local(end_date),
)
@property
def upcoming(self) -> Iterable[Event] | None:
"""Return upcoming events if any."""
if self.data:
return self.data.active_after(dt_util.now())
return None
class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]):
"""Coordinator for calendar RPC calls.
This sends a polling RPC, not using sync, as a workaround
for limitations in the calendar API for supporting search.
"""
def __init__(
self,
hass: HomeAssistant,
calendar_service: GoogleCalendarService,
name: str,
calendar_id: str,
search: str | None,
) -> None:
"""Create the CalendarQueryUpdateCoordinator."""
super().__init__(
hass,
_LOGGER,
name=name,
update_interval=MIN_TIME_BETWEEN_UPDATES,
)
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self._search = search
async def async_get_events(
self, start_date: datetime, end_date: datetime
) -> Iterable[Event]:
"""Get all events in a specific time frame."""
request = ListEventsRequest(
calendar_id=self.calendar_id,
start_time=start_date,
end_time=end_date,
search=self._search,
)
result_items = []
try:
result = await self.calendar_service.async_list_events(request)
async for result_page in result:
result_items.extend(result_page.items)
except ApiException as err:
self.async_set_update_error(err)
raise HomeAssistantError(str(err)) from err
return result_items
async def _async_update_data(self) -> list[Event]:
"""Fetch data from API endpoint."""
request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search)
try:
result = await self.calendar_service.async_list_events(request)
except ApiException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return result.items
@property
def upcoming(self) -> Iterable[Event] | None:
"""Return the next upcoming event if any."""
return self.data
class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
"""A calendar event entity."""
@@ -279,7 +376,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def __init__(
self,
coordinator: CalendarUpdateCoordinator,
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
calendar_id: str,
data: dict[str, Any],
entity_id: str,
@@ -352,14 +449,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame."""
if not (timeline := self.coordinator.data):
raise HomeAssistantError(
"Unable to get events: Sync from server has not completed"
)
result_items = timeline.overlapping(
dt_util.as_local(start_date),
dt_util.as_local(end_date),
)
result_items = await self.coordinator.async_get_events(start_date, end_date)
return [
_get_calendar_event(event)
for event in filter(self._event_filter, result_items)
@@ -367,14 +457,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
def _apply_coordinator_update(self) -> None:
"""Copy state from the coordinator to this entity."""
if (timeline := self.coordinator.data) and (
api_event := next(
filter(
self._event_filter,
timeline.active_after(dt_util.now()),
),
None,
)
if api_event := next(
filter(
self._event_filter,
self.coordinator.upcoming or [],
),
None,
):
self._event = _get_calendar_event(api_event)
(self._event.summary, self._offset_value) = extract_offset(

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
"requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"],
"requirements": ["gcal-sync==4.0.2", "oauth2client==4.1.3"],
"codeowners": ["@allenporter"],
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"]

View File

@@ -7,6 +7,7 @@ import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -100,7 +101,7 @@ async def async_setup_service(hass: HomeAssistant) -> None:
columns.append(key)
worksheet.update_cell(1, len(columns), key)
row.append(value)
worksheet.append_row(row)
worksheet.append_row(row, value_input_option=ValueInputOption.user_entered)
async def append_to_sheet(call: ServiceCall) -> None:
"""Append new line of data to a Google Sheets document."""

View File

@@ -3,7 +3,7 @@
"name": "Growatt",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/growatt_server/",
"requirements": ["growattServer==1.2.3"],
"requirements": ["growattServer==1.2.4"],
"codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"],
"iot_class": "cloud_polling",
"loggers": ["growattServer"]

View File

@@ -32,7 +32,7 @@ from .sensor_types.total import TOTAL_SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = datetime.timedelta(minutes=1)
SCAN_INTERVAL = datetime.timedelta(minutes=5)
def get_device_list(api, config):
@@ -159,7 +159,7 @@ class GrowattInverter(SensorEntity):
@property
def native_value(self):
"""Return the state of the sensor."""
result = self.probe.get_data(self.entity_description.api_key)
result = self.probe.get_data(self.entity_description)
if self.entity_description.precision is not None:
result = round(result, self.entity_description.precision)
return result
@@ -168,7 +168,7 @@ class GrowattInverter(SensorEntity):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor, if any."""
if self.entity_description.currency:
return self.probe.get_data("currency")
return self.probe.get_currency()
return super().native_unit_of_measurement
def update(self) -> None:
@@ -187,6 +187,7 @@ class GrowattData:
self.device_id = device_id
self.plant_id = None
self.data = {}
self.previous_values = {}
self.username = username
self.password = password
@@ -254,9 +255,61 @@ class GrowattData:
**mix_detail,
**dashboard_values_for_mix,
}
_LOGGER.debug(
"Finished updating data for %s (%s)",
self.device_id,
self.growatt_type,
)
except json.decoder.JSONDecodeError:
_LOGGER.error("Unable to fetch data from Growatt server")
def get_data(self, variable):
def get_currency(self):
"""Get the currency."""
return self.data.get("currency")
def get_data(self, entity_description):
"""Get the data."""
return self.data.get(variable)
_LOGGER.debug(
"Data request for: %s",
entity_description.name,
)
variable = entity_description.api_key
api_value = self.data.get(variable)
previous_value = self.previous_values.get(variable)
return_value = api_value
# If we have a 'drop threshold' specified, then check it and correct if needed
if (
entity_description.previous_value_drop_threshold is not None
and previous_value is not None
and api_value is not None
):
_LOGGER.debug(
"%s - Drop threshold specified (%s), checking for drop... API Value: %s, Previous Value: %s",
entity_description.name,
entity_description.previous_value_drop_threshold,
api_value,
previous_value,
)
diff = float(api_value) - float(previous_value)
# Check if the value has dropped (negative value i.e. < 0) and it has only dropped by a
# small amount, if so, use the previous value.
# Note - The energy dashboard takes care of drops within 10% of the current value,
# however if the value is low e.g. 0.2 and drops by 0.1 it classes as a reset.
if -(entity_description.previous_value_drop_threshold) <= diff < 0:
_LOGGER.debug(
"Diff is negative, but only by a small amount therefore not a nightly reset, "
"using previous value (%s) instead of api value (%s)",
previous_value,
api_value,
)
return_value = previous_value
else:
_LOGGER.debug(
"%s - No drop detected, using API value", entity_description.name
)
self.previous_values[variable] = return_value
return return_value

View File

@@ -241,5 +241,6 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
previous_value_drop_threshold=0.2,
),
)

View File

@@ -19,3 +19,4 @@ class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKey
precision: int | None = None
currency: bool = False
previous_value_drop_threshold: float | None = None

View File

@@ -10,8 +10,10 @@ import os
from typing import Any, cast
from aiohttp import web
from pyhap.characteristic import Characteristic
from pyhap.const import STANDALONE_AID
from pyhap.loader import get_loader
from pyhap.service import Service
import voluptuous as vol
from zeroconf.asyncio import AsyncZeroconf
@@ -21,6 +23,9 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.device_automation.trigger import (
async_validate_trigger_config,
)
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
from homeassistant.components.network import MDNS_TARGET_IP
@@ -74,13 +79,7 @@ from . import ( # noqa: F401
type_switches,
type_thermostats,
)
from .accessories import (
HomeAccessory,
HomeBridge,
HomeDriver,
HomeIIDManager,
get_accessory,
)
from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
from .aidmanager import AccessoryAidStorage
from .const import (
ATTR_INTEGRATION,
@@ -139,7 +138,7 @@ STATUS_WAIT = 3
PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
_HOMEKIT_CONFIG_UPDATE_TIME = (
5 # number of seconds to wait for homekit to see the c# change
10 # number of seconds to wait for homekit to see the c# change
)
@@ -529,6 +528,7 @@ class HomeKit:
self.status = STATUS_READY
self.driver: HomeDriver | None = None
self.bridge: HomeBridge | None = None
self._reset_lock = asyncio.Lock()
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
"""Set up bridge and accessory driver."""
@@ -548,7 +548,7 @@ class HomeKit:
async_zeroconf_instance=async_zeroconf_instance,
zeroconf_server=f"{uuid}-hap.local.",
loader=get_loader(),
iid_manager=HomeIIDManager(self.iid_storage),
iid_storage=self.iid_storage,
)
# If we do not load the mac address will be wrong
@@ -558,21 +558,24 @@ class HomeKit:
async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reset the accessory to load the latest configuration."""
if not self.bridge:
await self.async_reset_accessories_in_accessory_mode(entity_ids)
return
await self.async_reset_accessories_in_bridge_mode(entity_ids)
async with self._reset_lock:
if not self.bridge:
await self.async_reset_accessories_in_accessory_mode(entity_ids)
return
await self.async_reset_accessories_in_bridge_mode(entity_ids)
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
"""Shutdown an accessory."""
assert self.driver is not None
await accessory.stop()
# Deallocate the IIDs for the accessory
iid_manager = self.driver.iid_manager
for service in accessory.services:
iid_manager.remove_iid(iid_manager.remove_obj(service))
for char in service.characteristics:
iid_manager.remove_iid(iid_manager.remove_obj(char))
iid_manager = accessory.iid_manager
services: list[Service] = accessory.services
for service in services:
iid_manager.remove_obj(service)
characteristics: list[Characteristic] = service.characteristics
for char in characteristics:
iid_manager.remove_obj(char)
async def async_reset_accessories_in_accessory_mode(
self, entity_ids: Iterable[str]
@@ -581,7 +584,6 @@ class HomeKit:
assert self.driver is not None
acc = cast(HomeAccessory, self.driver.accessory)
await self._async_shutdown_accessory(acc)
if acc.entity_id not in entity_ids:
return
if not (state := self.hass.states.get(acc.entity_id)):
@@ -589,6 +591,7 @@ class HomeKit:
"The underlying entity %s disappeared during reset", acc.entity_id
)
return
await self._async_shutdown_accessory(acc)
if new_acc := self._async_create_single_accessory([state]):
self.driver.accessory = new_acc
self.hass.async_add_job(new_acc.run)
@@ -906,29 +909,47 @@ class HomeKit:
self.bridge = HomeBridge(self.hass, self.driver, self._name)
for state in entity_states:
self.add_bridge_accessory(state)
dev_reg = device_registry.async_get(self.hass)
if self._devices:
valid_device_ids = []
for device_id in self._devices:
if not dev_reg.async_get(device_id):
_LOGGER.warning(
"HomeKit %s cannot add device %s because it is missing from the device registry",
self._name,
device_id,
)
else:
valid_device_ids.append(device_id)
for device_id, device_triggers in (
await device_automation.async_get_device_automations(
self.hass,
device_automation.DeviceAutomationType.TRIGGER,
valid_device_ids,
)
).items():
if device := dev_reg.async_get(device_id):
self.add_bridge_triggers_accessory(device, device_triggers)
await self._async_add_trigger_accessories()
return self.bridge
async def _async_add_trigger_accessories(self) -> None:
"""Add devices with triggers to the bridge."""
dev_reg = device_registry.async_get(self.hass)
valid_device_ids = []
for device_id in self._devices:
if not dev_reg.async_get(device_id):
_LOGGER.warning(
"HomeKit %s cannot add device %s because it is missing from the device registry",
self._name,
device_id,
)
else:
valid_device_ids.append(device_id)
for device_id, device_triggers in (
await device_automation.async_get_device_automations(
self.hass,
device_automation.DeviceAutomationType.TRIGGER,
valid_device_ids,
)
).items():
device = dev_reg.async_get(device_id)
assert device is not None
valid_device_triggers: list[dict[str, Any]] = []
for trigger in device_triggers:
try:
await async_validate_trigger_config(self.hass, trigger)
except vol.Invalid as ex:
_LOGGER.debug(
"%s: cannot add unsupported trigger %s because it requires additional inputs which are not supported by HomeKit: %s",
self._name,
trigger,
ex,
)
continue
valid_device_triggers.append(trigger)
self.add_bridge_triggers_accessory(device, valid_device_triggers)
async def _async_create_accessories(self) -> bool:
"""Create the accessories."""
assert self.driver is not None

View File

@@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
driver=driver,
display_name=cleanup_name_for_homekit(name),
aid=aid,
iid_manager=driver.iid_manager,
iid_manager=HomeIIDManager(driver.iid_storage),
*args,
**kwargs,
)
@@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc]
def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
"""Initialize a Bridge object."""
super().__init__(driver, name, iid_manager=driver.iid_manager)
super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage))
self.set_info_service(
firmware_revision=format_version(__version__),
manufacturer=MANUFACTURER,
@@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
entry_id: str,
bridge_name: str,
entry_title: str,
iid_manager: HomeIIDManager,
iid_storage: AccessoryIIDStorage,
**kwargs: Any,
) -> None:
"""Initialize a AccessoryDriver object."""
@@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
self._entry_id = entry_id
self._bridge_name = bridge_name
self._entry_title = entry_title
self.iid_manager = iid_manager
self.iid_storage = iid_storage
@pyhap_callback # type: ignore[misc]
def pair(
@@ -653,7 +653,7 @@ class HomeIIDManager(IIDManager): # type: ignore[misc]
"""Get IID for object."""
aid = obj.broker.aid
if isinstance(obj, Characteristic):
service = obj.service
service: Service = obj.service
iid = self._iid_storage.get_or_allocate_iid(
aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id
)

View File

@@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics(
"options": dict(entry.options),
},
}
if homekit.iid_storage:
data["iid_storage"] = homekit.iid_storage.allocations
if not homekit.driver: # not started yet or startup failed
return data
driver: AccessoryDriver = homekit.driver
@@ -65,13 +67,16 @@ def _get_accessory_diagnostics(
hass: HomeAssistant, accessory: HomeAccessory
) -> dict[str, Any]:
"""Return diagnostics for an accessory."""
return {
entity_state = None
if accessory.entity_id:
entity_state = hass.states.get(accessory.entity_id)
data = {
"aid": accessory.aid,
"config": accessory.config,
"category": accessory.category,
"name": accessory.display_name,
"entity_id": accessory.entity_id,
"entity_state": async_redact_data(
hass.states.get(accessory.entity_id), TO_REDACT
),
}
if entity_state:
data["entity_state"] = async_redact_data(entity_state, TO_REDACT)
return data

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store
from .util import get_iid_storage_filename_for_entry_id
IID_MANAGER_STORAGE_VERSION = 1
IID_MANAGER_STORAGE_VERSION = 2
IID_MANAGER_SAVE_DELAY = 2
ALLOCATIONS_KEY = "allocations"
@@ -26,6 +26,40 @@ IID_MIN = 1
IID_MAX = 18446744073709551615
ACCESSORY_INFORMATION_SERVICE = "3E"
class IIDStorage(Store):
"""Storage class for IIDManager."""
async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict,
):
"""Migrate to the new version."""
if old_major_version == 1:
# Convert v1 to v2 format which uses a unique iid set per accessory
# instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
# to always have iid 1 for each bridged accessory as well as the bridge
old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
new_allocation: dict[str, dict[str, int]] = {}
old_data[ALLOCATIONS_KEY] = new_allocation
for allocation_key, iid in old_allocations.items():
aid_str, new_allocation_key = allocation_key.split("_", 1)
service_type, _, char_type, *_ = new_allocation_key.split("_")
accessory_allocation = new_allocation.setdefault(aid_str, {})
if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
accessory_allocation[new_allocation_key] = 1
elif iid != 1:
accessory_allocation[new_allocation_key] = iid
return old_data
raise NotImplementedError
class AccessoryIIDStorage:
"""
Provide stable allocation of IIDs for the lifetime of an accessory.
@@ -37,15 +71,15 @@ class AccessoryIIDStorage:
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new iid store."""
self.hass = hass
self.allocations: dict[str, int] = {}
self.allocated_iids: list[int] = []
self.allocations: dict[str, dict[str, int]] = {}
self.allocated_iids: dict[str, list[int]] = {}
self.entry_id = entry_id
self.store: Store | None = None
self.store: IIDStorage | None = None
async def async_initialize(self) -> None:
"""Load the latest IID data."""
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
if not (raw_storage := await self.store.async_load()):
# There is no data about iid allocations yet
@@ -53,7 +87,8 @@ class AccessoryIIDStorage:
assert isinstance(raw_storage, dict)
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
self.allocated_iids = sorted(self.allocations.values())
for aid_str, allocations in self.allocations.items():
self.allocated_iids[aid_str] = sorted(allocations.values())
def get_or_allocate_iid(
self,
@@ -68,16 +103,25 @@ class AccessoryIIDStorage:
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
# Allocation key must be a string since we are saving it to JSON
allocation_key = (
f'{aid}_{service_hap_type}_{service_unique_id or ""}_'
f'{service_hap_type}_{service_unique_id or ""}_'
f'{char_hap_type or ""}_{char_unique_id or ""}'
)
if allocation_key in self.allocations:
return self.allocations[allocation_key]
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
self.allocations[allocation_key] = next_iid
self.allocated_iids.append(next_iid)
# AID must be a string since JSON keys cannot be int
aid_str = str(aid)
accessory_allocation = self.allocations.setdefault(aid_str, {})
accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
return 1
if allocation_key in accessory_allocation:
return accessory_allocation[allocation_key]
if accessory_allocated_iids:
allocated_iid = accessory_allocated_iids[-1] + 1
else:
allocated_iid = 2
accessory_allocation[allocation_key] = allocated_iid
accessory_allocated_iids.append(allocated_iid)
self._async_schedule_save()
return next_iid
return allocated_iid
@callback
def _async_schedule_save(self) -> None:
@@ -91,6 +135,6 @@ class AccessoryIIDStorage:
return await self.store.async_save(self._data_to_save())
@callback
def _data_to_save(self) -> dict[str, dict[str, int]]:
def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
"""Return data of entity map to store in a file."""
return {ALLOCATIONS_KEY: self.allocations}

View File

@@ -306,7 +306,7 @@ class Thermostat(HomeAccessory):
if attributes.get(ATTR_HVAC_ACTION) is not None:
self.fan_chars.append(CHAR_CURRENT_FAN_STATE)
serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars)
serv_fan.add_linked_service(serv_thermostat)
serv_thermostat.add_linked_service(serv_fan)
self.char_active = serv_fan.configure_char(
CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active
)

View File

@@ -7,9 +7,11 @@ from typing import Any
from pyhap.const import CATEGORY_SENSOR
from homeassistant.core import CALLBACK_TYPE, Context
from homeassistant.helpers import entity_registry
from homeassistant.helpers.trigger import async_initialize_triggers
from .accessories import TYPES, HomeAccessory
from .aidmanager import get_system_unique_id
from .const import (
CHAR_NAME,
CHAR_PROGRAMMABLE_SWITCH_EVENT,
@@ -18,6 +20,7 @@ from .const import (
SERV_SERVICE_LABEL,
SERV_STATELESS_PROGRAMMABLE_SWITCH,
)
from .util import cleanup_name_for_homekit
_LOGGER = logging.getLogger(__name__)
@@ -39,13 +42,22 @@ class DeviceTriggerAccessory(HomeAccessory):
self._remove_triggers: CALLBACK_TYPE | None = None
self.triggers = []
assert device_triggers is not None
ent_reg = entity_registry.async_get(self.hass)
for idx, trigger in enumerate(device_triggers):
type_ = trigger["type"]
subtype = trigger.get("subtype")
type_: str = trigger["type"]
subtype: str | None = trigger.get("subtype")
unique_id = f'{type_}-{subtype or ""}'
trigger_name = (
f"{type_.title()} {subtype.title()}" if subtype else type_.title()
)
if (entity_id := trigger.get("entity_id")) and (
entry := ent_reg.async_get(entity_id)
):
unique_id += f"-entity_unique_id:{get_system_unique_id(entry)}"
trigger_name_parts = []
if entity_id and (state := self.hass.states.get(entity_id)):
trigger_name_parts.append(state.name)
trigger_name_parts.append(type_.replace("_", " ").title())
if subtype:
trigger_name_parts.append(subtype.replace("_", " ").title())
trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts))
serv_stateless_switch = self.add_preload_service(
SERV_STATELESS_PROGRAMMABLE_SWITCH,
[CHAR_NAME, CHAR_SERVICE_LABEL_INDEX],

View File

@@ -209,6 +209,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
)
await self.async_put_characteristics(
{
CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE,
CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[
hvac_mode
],

View File

@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==2.2.14"],
"requirements": ["aiohomekit==2.2.19"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [
"huawei-lte-api==1.6.3",
"huawei-lte-api==1.6.7",
"stringcase==1.2.0",
"url-normalize==1.4.3"
],

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
"codeowners": ["@flz"],
"requirements": ["iaqualink==0.5.0"],
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
"iot_class": "cloud_polling",
"loggers": ["iaqualink"]
}

View File

@@ -396,7 +396,11 @@ class IBeaconCoordinator:
)
continue
if service_info.rssi != ibeacon_advertisement.rssi:
if (
service_info.rssi != ibeacon_advertisement.rssi
or service_info.source != ibeacon_advertisement.source
):
ibeacon_advertisement.source = service_info.source
ibeacon_advertisement.update_rssi(service_info.rssi)
async_dispatcher_send(
self.hass,

View File

@@ -2,7 +2,7 @@
"domain": "lidarr",
"name": "Lidarr",
"documentation": "https://www.home-assistant.io/integrations/lidarr",
"requirements": ["aiopyarr==22.10.0"],
"requirements": ["aiopyarr==22.11.0"],
"codeowners": ["@tkdrob"],
"config_flow": true,
"iot_class": "local_polling",

View File

@@ -3,11 +3,14 @@
from datetime import timedelta
import logging
from aiohttp import ClientTimeout
DOMAIN = "life360"
LOGGER = logging.getLogger(__package__)
ATTRIBUTION = "Data provided by life360.com"
COMM_TIMEOUT = 10
COMM_MAX_RETRIES = 3
COMM_TIMEOUT = ClientTimeout(sock_connect=15, total=60)
SPEED_FACTOR_MPH = 2.25
SPEED_DIGITS = 1
UPDATE_INTERVAL = timedelta(seconds=10)

View File

@@ -26,6 +26,7 @@ from homeassistant.util.unit_conversion import DistanceConverter
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import (
COMM_MAX_RETRIES,
COMM_TIMEOUT,
CONF_AUTHORIZATION,
DOMAIN,
@@ -106,6 +107,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
self._api = Life360(
session=async_get_clientsession(hass),
timeout=COMM_TIMEOUT,
max_retries=COMM_MAX_RETRIES,
authorization=entry.data[CONF_AUTHORIZATION],
)
self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/life360",
"codeowners": ["@pnbruckner"],
"requirements": ["life360==5.1.1"],
"requirements": ["life360==5.3.0"],
"iot_class": "cloud_polling",
"loggers": ["life360"]
}

View File

@@ -14,8 +14,12 @@ from awesomeversion import AwesomeVersion
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_XY_COLOR,
)
@@ -24,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
import homeassistant.util.color as color_util
from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
FIX_MAC_FW = AwesomeVersion("3.70")
@@ -80,6 +84,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
"""
hue, saturation, brightness, kelvin = [None] * 4
if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None:
try:
hue, saturation = color_util.color_RGB_to_hs(
*color_util.color_name_to_rgb(color_name)
)
except ValueError:
_LOGGER.warning(
"Got unknown color %s, falling back to neutral white", color_name
)
hue, saturation = (0, 0)
if ATTR_HS_COLOR in kwargs:
hue, saturation = kwargs[ATTR_HS_COLOR]
elif ATTR_RGB_COLOR in kwargs:
@@ -93,6 +108,19 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
saturation = int(saturation / 100 * 65535)
kelvin = 3500
if ATTR_KELVIN in kwargs:
_LOGGER.warning(
"The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls"
)
kelvin = kwargs.pop(ATTR_KELVIN)
saturation = 0
if ATTR_COLOR_TEMP in kwargs:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs.pop(ATTR_COLOR_TEMP)
)
saturation = 0
if ATTR_COLOR_TEMP_KELVIN in kwargs:
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
saturation = 0
@@ -100,6 +128,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
if ATTR_BRIGHTNESS_PCT in kwargs:
brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100))
hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None] * 4 else hsbk

View File

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

View File

@@ -196,19 +196,19 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
self._attr_latest_version = json_payload["latest_version"]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_TITLE in json_payload and not self._attr_title:
if CONF_TITLE in json_payload:
self._attr_title = json_payload[CONF_TITLE]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary:
if CONF_RELEASE_SUMMARY in json_payload:
self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_RELEASE_URL in json_payload and not self._attr_release_url:
if CONF_RELEASE_URL in json_payload:
self._attr_release_url = json_payload[CONF_RELEASE_URL]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture:
if CONF_ENTITY_PICTURE in json_payload:
self._entity_picture = json_payload[CONF_ENTITY_PICTURE]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)

View File

@@ -2,7 +2,7 @@
"domain": "netatmo",
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": ["pyatmo==7.3.0"],
"requirements": ["pyatmo==7.4.0"],
"after_dependencies": ["cloud", "media_source"],
"dependencies": ["application_credentials", "webhook"],
"codeowners": ["@cgtobi"],

View File

@@ -378,7 +378,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
async def async_turn_aux_heat_on(self) -> None:
"""Turn Aux Heat on."""
self._thermostat.set_emergency_heat(True)
await self._thermostat.set_emergency_heat(True)
self._signal_thermostat_update()
async def async_turn_off(self) -> None:

View File

@@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity):
self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}"
)
@property
def available(self) -> bool:
"""Return True if thermostat is available and data is available."""
return super().available and self._thermostat.is_online
class NexiaThermostatZoneEntity(NexiaThermostatEntity):
"""Base class for nexia devices attached to a thermostat."""

View File

@@ -1,7 +1,7 @@
{
"domain": "nexia",
"name": "Nexia/American Standard/Trane",
"requirements": ["nexia==2.0.5"],
"requirements": ["nexia==2.0.6"],
"codeowners": ["@bdraco"],
"documentation": "https://www.home-assistant.io/integrations/nexia",
"config_flow": true,

View File

@@ -8,7 +8,7 @@ import datetime as dt
from httpx import RemoteProtocolError, TransportError
from onvif import ONVIFCamera, ONVIFService
from zeep.exceptions import Fault
from zeep.exceptions import Fault, XMLParseError
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
@@ -20,6 +20,7 @@ from .parsers import PARSERS
UNHANDLED_TOPICS = set()
SUBSCRIPTION_ERRORS = (
XMLParseError,
Fault,
asyncio.TimeoutError,
TransportError,
@@ -153,7 +154,8 @@ class EventManager:
.isoformat(timespec="seconds")
.replace("+00:00", "Z")
)
await self._subscription.Renew(termination_time)
with suppress(*SUBSCRIPTION_ERRORS):
await self._subscription.Renew(termination_time)
def async_schedule_pull(self) -> None:
"""Schedule async_pull_messages to run."""

View File

@@ -48,7 +48,8 @@
"onvif_devices": {
"data": {
"extra_arguments": "Extra FFMPEG arguments",
"rtsp_transport": "RTSP transport mechanism"
"rtsp_transport": "RTSP transport mechanism",
"use_wallclock_as_timestamps": "Use wall clock as timestamps"
},
"title": "ONVIF Device Options"
}

View File

@@ -48,7 +48,8 @@
"onvif_devices": {
"data": {
"extra_arguments": "Extra FFMPEG arguments",
"rtsp_transport": "RTSP transport mechanism"
"rtsp_transport": "RTSP transport mechanism",
"use_wallclock_as_timestamps": "Use wall clock as timestamps"
},
"title": "ONVIF Device Options"
}

View File

@@ -8,7 +8,7 @@
"manufacturer_id": 220
}
],
"requirements": ["oralb-ble==0.10.0"],
"requirements": ["oralb-ble==0.14.2"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"

View File

@@ -5,6 +5,7 @@ from typing import TypedDict
from p1monitor import (
P1Monitor,
P1MonitorConnectionError,
P1MonitorNoDataError,
Phases,
Settings,
@@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
try:
data[SERVICE_WATERMETER] = await self.p1monitor.watermeter()
self.has_water_meter = True
except P1MonitorNoDataError:
LOGGER.debug("No watermeter data received from P1 Monitor")
except (P1MonitorNoDataError, P1MonitorConnectionError):
LOGGER.debug("No water meter data received from P1 Monitor")
if self.has_water_meter is None:
self.has_water_meter = False

View File

@@ -3,7 +3,7 @@
"name": "P1 Monitor",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/p1_monitor",
"requirements": ["p1monitor==2.1.0"],
"requirements": ["p1monitor==2.1.1"],
"codeowners": ["@klaasnicolaas"],
"quality_scale": "platinum",
"iot_class": "local_polling",

View File

@@ -2,7 +2,7 @@
"domain": "plugwise",
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["plugwise==0.25.3"],
"requirements": ["plugwise==0.25.7"],
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true,

View File

@@ -2,7 +2,7 @@
"domain": "radarr",
"name": "Radarr",
"documentation": "https://www.home-assistant.io/integrations/radarr",
"requirements": ["aiopyarr==22.10.0"],
"requirements": ["aiopyarr==22.11.0"],
"codeowners": ["@tkdrob"],
"config_flow": true,
"iot_class": "local_polling",

View File

@@ -237,6 +237,7 @@ async def async_setup_entry(
# Add switches to control restrictions:
for description in RESTRICTIONS_SWITCH_DESCRIPTIONS:
coordinator = data.coordinators[description.api_category]
if not key_exists(coordinator.data, description.data_key):
continue
entities.append(RainMachineRestrictionSwitch(entry, data, description))

View File

@@ -2,7 +2,7 @@
"domain": "recorder",
"name": "Recorder",
"documentation": "https://www.home-assistant.io/integrations/recorder",
"requirements": ["sqlalchemy==1.4.42", "fnvhash==0.1.0"],
"requirements": ["sqlalchemy==1.4.44", "fnvhash==0.1.0"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal",
"iot_class": "local_push",

View File

@@ -1216,11 +1216,29 @@ def _get_max_mean_min_statistic(
return result
def _first_statistic(
session: Session,
table: type[Statistics | StatisticsShortTerm],
metadata_id: int,
) -> datetime | None:
"""Return the data of the oldest statistic row for a given metadata id."""
stmt = lambda_stmt(
lambda: select(table.start)
.filter(table.metadata_id == metadata_id)
.order_by(table.start.asc())
.limit(1)
)
if stats := execute_stmt_lambda_element(session, stmt):
return process_timestamp(stats[0].start) # type: ignore[no-any-return]
return None
def _get_oldest_sum_statistic(
session: Session,
head_start_time: datetime | None,
main_start_time: datetime | None,
tail_start_time: datetime | None,
oldest_stat: datetime | None,
tail_only: bool,
metadata_id: int,
) -> float | None:
@@ -1231,10 +1249,10 @@ def _get_oldest_sum_statistic(
start_time: datetime | None,
table: type[Statistics | StatisticsShortTerm],
metadata_id: int,
) -> tuple[float | None, datetime | None]:
) -> float | None:
"""Return the oldest non-NULL sum during the period."""
stmt = lambda_stmt(
lambda: select(table.sum, table.start)
lambda: select(table.sum)
.filter(table.metadata_id == metadata_id)
.filter(table.sum.is_not(None))
.order_by(table.start.asc())
@@ -1248,49 +1266,49 @@ def _get_oldest_sum_statistic(
else:
period = start_time.replace(minute=0, second=0, microsecond=0)
prev_period = period - table.duration
stmt += lambda q: q.filter(table.start == prev_period)
stmt += lambda q: q.filter(table.start >= prev_period)
stats = execute_stmt_lambda_element(session, stmt)
return (
(stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None)
)
return stats[0].sum if stats else None
oldest_start: datetime | None
oldest_sum: float | None = None
if head_start_time is not None:
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
session, head_start_time, StatisticsShortTerm, metadata_id
# This function won't be called if tail_only is False and main_start_time is None
# the extra checks are added to satisfy MyPy
if not tail_only and main_start_time is not None and oldest_stat is not None:
period = main_start_time.replace(minute=0, second=0, microsecond=0)
prev_period = period - Statistics.duration
if prev_period < oldest_stat:
return 0
if (
head_start_time is not None
and (
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, head_start_time, StatisticsShortTerm, metadata_id
)
)
if (
oldest_start is not None
and oldest_start < head_start_time
and oldest_sum is not None
):
return oldest_sum
is not None
):
return oldest_sum
if not tail_only:
assert main_start_time is not None
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
session, main_start_time, Statistics, metadata_id
)
if (
oldest_start is not None
and oldest_start < main_start_time
and oldest_sum is not None
):
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, main_start_time, Statistics, metadata_id
)
) is not None:
return oldest_sum
return 0
if tail_start_time is not None:
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
session, tail_start_time, StatisticsShortTerm, metadata_id
if (
tail_start_time is not None
and (
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, tail_start_time, StatisticsShortTerm, metadata_id
)
)
if (
oldest_start is not None
and oldest_start < tail_start_time
and oldest_sum is not None
):
return oldest_sum
) is not None:
return oldest_sum
return 0
@@ -1373,51 +1391,79 @@ def statistic_during_period(
result: dict[str, Any] = {}
# To calculate the summary, data from the statistics (hourly) and short_term_statistics
# (5 minute) tables is combined
# - The short term statistics table is used for the head and tail of the period,
# if the period it doesn't start or end on a full hour
# - The statistics table is used for the remainder of the time
now = dt_util.utcnow()
if end_time is not None and end_time > now:
end_time = now
tail_only = (
start_time is not None
and end_time is not None
and end_time - start_time < timedelta(hours=1)
)
# Calculate the head period
head_start_time: datetime | None = None
head_end_time: datetime | None = None
if not tail_only and start_time is not None and start_time.minute:
head_start_time = start_time
head_end_time = start_time.replace(
minute=0, second=0, microsecond=0
) + timedelta(hours=1)
# Calculate the tail period
tail_start_time: datetime | None = None
tail_end_time: datetime | None = None
if end_time is None:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
elif end_time.minute:
tail_start_time = (
start_time
if tail_only
else end_time.replace(minute=0, second=0, microsecond=0)
)
tail_end_time = end_time
# Calculate the main period
main_start_time: datetime | None = None
main_end_time: datetime | None = None
if not tail_only:
main_start_time = start_time if head_end_time is None else head_end_time
main_end_time = end_time if tail_start_time is None else tail_start_time
with session_scope(hass=hass) as session:
# Fetch metadata for the given statistic_id
if not (
metadata := get_metadata_with_session(session, statistic_ids=[statistic_id])
):
return result
metadata_id = metadata[statistic_id][0]
oldest_stat = _first_statistic(session, Statistics, metadata_id)
oldest_5_min_stat = None
if not valid_statistic_id(statistic_id):
oldest_5_min_stat = _first_statistic(
session, StatisticsShortTerm, metadata_id
)
# To calculate the summary, data from the statistics (hourly) and
# short_term_statistics (5 minute) tables is combined
# - The short term statistics table is used for the head and tail of the period,
# if the period it doesn't start or end on a full hour
# - The statistics table is used for the remainder of the time
now = dt_util.utcnow()
if end_time is not None and end_time > now:
end_time = now
tail_only = (
start_time is not None
and end_time is not None
and end_time - start_time < timedelta(hours=1)
)
# Calculate the head period
head_start_time: datetime | None = None
head_end_time: datetime | None = None
if (
not tail_only
and oldest_stat is not None
and oldest_5_min_stat is not None
and oldest_5_min_stat - oldest_stat < timedelta(hours=1)
and (start_time is None or start_time < oldest_5_min_stat)
):
# To improve accuracy of averaged for statistics which were added within
# recorder's retention period.
head_start_time = oldest_5_min_stat
head_end_time = oldest_5_min_stat.replace(
minute=0, second=0, microsecond=0
) + timedelta(hours=1)
elif not tail_only and start_time is not None and start_time.minute:
head_start_time = start_time
head_end_time = start_time.replace(
minute=0, second=0, microsecond=0
) + timedelta(hours=1)
# Calculate the tail period
tail_start_time: datetime | None = None
tail_end_time: datetime | None = None
if end_time is None:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
elif end_time.minute:
tail_start_time = (
start_time
if tail_only
else end_time.replace(minute=0, second=0, microsecond=0)
)
tail_end_time = end_time
# Calculate the main period
main_start_time: datetime | None = None
main_end_time: datetime | None = None
if not tail_only:
main_start_time = start_time if head_end_time is None else head_end_time
main_end_time = end_time if tail_start_time is None else tail_start_time
# Fetch metadata for the given statistic_id
metadata = get_metadata_with_session(session, statistic_ids=[statistic_id])
if not metadata:
@@ -1449,6 +1495,7 @@ def statistic_during_period(
head_start_time,
main_start_time,
tail_start_time,
oldest_stat,
tail_only,
metadata_id,
)

View File

@@ -89,6 +89,12 @@ COMBINED_SCHEMA = vol.Schema(
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
{
DOMAIN: vol.All(
cv.ensure_list,
cv.remove_falsy,
[COMBINED_SCHEMA],
)
},
extra=vol.ALLOW_EXTRA,
)

View File

@@ -3,7 +3,7 @@
"name": "Ridwell",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ridwell",
"requirements": ["aioridwell==2022.03.0"],
"requirements": ["aioridwell==2022.11.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"loggers": ["aioridwell"],

View File

@@ -7,7 +7,7 @@
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.5.0",
"wakeonlan==2.1.0",
"async-upnp-client==0.32.1"
"async-upnp-client==0.32.2"
],
"ssdp": [
{

View File

@@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
@@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10)
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
CONF_ATTR = "attribute"
CONF_SELECT = "select"
@@ -111,7 +112,8 @@ async def async_setup_platform(
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL)
scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
coordinator = ScrapeCoordinator(hass, rest, scan_interval)
await coordinator.async_refresh()
if coordinator.data is None:
raise PlatformNotReady

View File

@@ -9,6 +9,7 @@ from aioshelly.block_device import Block
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry, entity, entity_registry
@@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
"""Initialize the sleeping sensor."""
self.sensors = sensors
self.last_state: StateType = None
self.last_unit: str | None = None
self.coordinator = coordinator
self.attribute = attribute
self.block: Block | None = block # type: ignore[assignment]
@@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
if last_state is not None:
self.last_state = last_state.state
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
@callback
def _update_callback(self) -> None:
@@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
) -> None:
"""Initialize the sleeping sensor."""
self.last_state: StateType = None
self.last_unit: str | None = None
self.coordinator = coordinator
self.key = key
self.attribute = attribute
@@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
if last_state is not None:
self.last_state = last_state.state
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

View File

@@ -47,12 +47,7 @@ from .entity import (
async_setup_entry_rest,
async_setup_entry_rpc,
)
from .utils import (
get_device_entry_gen,
get_device_uptime,
is_rpc_device_externally_powered,
temperature_unit,
)
from .utils import get_device_entry_gen, get_device_uptime
@dataclass
@@ -84,7 +79,7 @@ SENSORS: Final = {
("device", "deviceTemp"): BlockSensorDescription(
key="device|deviceTemp",
name="Device Temperature",
unit_fn=temperature_unit,
native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -145,7 +140,7 @@ SENSORS: Final = {
key="emeter|powerFactor",
name="Power Factor",
native_unit_of_measurement=PERCENTAGE,
value=lambda value: abs(round(value * 100, 1)),
value=lambda value: round(value * 100, 1),
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -226,7 +221,7 @@ SENSORS: Final = {
("sensor", "temp"): BlockSensorDescription(
key="sensor|temp",
name="Temperature",
unit_fn=temperature_unit,
native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -235,7 +230,7 @@ SENSORS: Final = {
("sensor", "extTemp"): BlockSensorDescription(
key="sensor|extTemp",
name="Temperature",
unit_fn=temperature_unit,
native_unit_of_measurement=TEMP_CELSIUS,
value=lambda value: round(value, 1),
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -407,7 +402,6 @@ RPC_SENSORS: Final = {
value=lambda status, _: status["percent"],
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=is_rpc_device_externally_powered,
entity_registry_enabled_default=True,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -505,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
super().__init__(coordinator, block, attribute, description)
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if unit_fn := description.unit_fn:
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
@property
def native_value(self) -> StateType:
@@ -553,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
"""Initialize the sleeping sensor."""
super().__init__(coordinator, block, attribute, description, entry, sensors)
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
if block and (unit_fn := description.unit_fn):
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
@@ -565,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
return self.last_state
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor, if any."""
if self.block is not None:
return self.entity_description.native_unit_of_measurement
return self.last_unit
class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
"""Represent a RPC sleeping sensor."""
@@ -578,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
return self.attribute_value
return self.last_state
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor, if any."""
if self.coordinator.device.initialized:
return self.entity_description.native_unit_of_measurement
return self.last_unit

View File

@@ -5,13 +5,13 @@ from datetime import datetime, timedelta
from typing import Any, cast
from aiohttp.web import Request, WebSocketResponse
from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
from aioshelly.block_device import COAP, Block, BlockDevice
from aioshelly.const import MODEL_NAMES
from aioshelly.rpc_device import RpcDevice, WsServer
from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry, entity_registry, singleton
from homeassistant.helpers.typing import EventType
@@ -43,13 +43,6 @@ def async_remove_shelly_entity(
entity_reg.async_remove(entity_id)
def temperature_unit(block_info: dict[str, Any]) -> str:
"""Detect temperature unit."""
if block_info[BLOCK_VALUE_UNIT] == "F":
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
def get_block_device_name(device: BlockDevice) -> str:
"""Naming for device."""
return cast(str, device.settings["name"] or device.settings["device"]["hostname"])
@@ -364,13 +357,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
return con_types is not None and con_types[channel].lower().startswith("light")
def is_rpc_device_externally_powered(
config: dict[str, Any], status: dict[str, Any], key: str
) -> bool:
"""Return true if device has external power instead of battery."""
return cast(bool, status[key]["external"]["present"])
def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
"""Return list of input triggers for RPC device."""
triggers = []

View File

@@ -3,7 +3,7 @@
"name": "Sonarr",
"documentation": "https://www.home-assistant.io/integrations/sonarr",
"codeowners": ["@ctalkington"],
"requirements": ["aiopyarr==22.10.0"],
"requirements": ["aiopyarr==22.11.0"],
"config_flow": true,
"quality_scale": "silver",
"iot_class": "local_polling",

View File

@@ -2,7 +2,7 @@
"domain": "sql",
"name": "SQL",
"documentation": "https://www.home-assistant.io/integrations/sql",
"requirements": ["sqlalchemy==1.4.42"],
"requirements": ["sqlalchemy==1.4.44"],
"codeowners": ["@dgomes", "@gjohansson-ST"],
"config_flow": true,
"iot_class": "local_polling"

View File

@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["async-upnp-client==0.32.1"],
"requirements": ["async-upnp-client==0.32.2"],
"dependencies": ["network"],
"after_dependencies": ["zeroconf"],
"codeowners": [],

View File

@@ -61,6 +61,15 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
self.base_unique_id = base_unique_id
self.model = model
self._ready_event = asyncio.Event()
self._was_unavailable = True
@callback
def _async_handle_unavailable(
self, service_info: bluetooth.BluetoothServiceInfoBleak
) -> None:
"""Handle the device going unavailable."""
super()._async_handle_unavailable(service_info)
self._was_unavailable = True
@callback
def _async_handle_bluetooth_event(
@@ -70,16 +79,20 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
) -> None:
"""Handle a Bluetooth event."""
self.ble_device = service_info.device
if adv := switchbot.parse_advertisement_data(
service_info.device, service_info.advertisement
if not (
adv := switchbot.parse_advertisement_data(
service_info.device, service_info.advertisement
)
):
if "modelName" in adv.data:
self._ready_event.set()
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
if not self.device.advertisement_changed(adv):
return
self.data = flatten_sensors_data(adv.data)
self.device.update_from_advertisement(adv)
return
if "modelName" in adv.data:
self._ready_event.set()
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
if not self.device.advertisement_changed(adv) and not self._was_unavailable:
return
self._was_unavailable = False
self.data = flatten_sensors_data(adv.data)
self.device.update_from_advertisement(adv)
super()._async_handle_bluetooth_event(service_info, change)
async def async_wait_ready(self) -> bool:

View File

@@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.20.2"],
"requirements": ["PySwitchbot==0.20.5"],
"config_flow": true,
"dependencies": ["bluetooth"],
"codeowners": [

View File

@@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await tibber_connection.update_info()
if not tibber_connection.name:
raise ConfigEntryNotReady("Could not fetch Tibber data.")
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:

View File

@@ -367,8 +367,6 @@ class UnifiBlockClientSwitch(SwitchEntity):
self.hass.async_create_task(self.remove_item({self._obj_id}))
return
client = self.controller.api.clients[self._obj_id]
self._attr_is_on = not client.blocked
self._attr_available = self.controller.available
self.async_write_ha_state()

View File

@@ -20,6 +20,7 @@ from .const import (
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DOMAIN,
DOMAIN_DISCOVERIES,
LOGGER,
ST_IGD_V1,
ST_IGD_V2,
@@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
)
async def _async_discover_igd_devices(
async def _async_discovered_igd_devices(
hass: HomeAssistant,
) -> list[ssdp.SsdpServiceInfo]:
"""Discovery IGD devices."""
@@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
# - user(None): scan --> user({...}) --> create_entry()
def __init__(self) -> None:
"""Initialize the UPnP/IGD config flow."""
self._discoveries: list[SsdpServiceInfo] | None = None
@property
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
"""Get current discoveries."""
domain_data: dict = self.hass.data.setdefault(DOMAIN, {})
return domain_data.setdefault(DOMAIN_DISCOVERIES, {})
def _add_discovery(self, discovery: SsdpServiceInfo) -> None:
"""Add a discovery."""
self._discoveries[discovery.ssdp_usn] = discovery
def _remove_discovery(self, usn: str) -> SsdpServiceInfo:
"""Remove a discovery by its USN/unique_id."""
return self._discoveries.pop(usn)
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
@@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery = next(
iter(
discovery
for discovery in self._discoveries
for discovery in self._discoveries.values()
if discovery.ssdp_usn == user_input["unique_id"]
)
)
@@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_create_entry_from_discovery(discovery)
# Discover devices.
discoveries = await _async_discover_igd_devices(self.hass)
discoveries = await _async_discovered_igd_devices(self.hass)
# Store discoveries which have not been configured.
current_unique_ids = {
entry.unique_id for entry in self._async_current_entries()
}
self._discoveries = [
discovery
for discovery in discoveries
for discovery in discoveries:
if (
_is_complete_discovery(discovery)
and _is_igd_device(discovery)
and discovery.ssdp_usn not in current_unique_ids
)
]
):
self._add_discovery(discovery)
# Ensure anything to add.
if not self._discoveries:
@@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required("unique_id"): vol.In(
{
discovery.ssdp_usn: _friendly_name_from_discovery(discovery)
for discovery in self._discoveries
for discovery in self._discoveries.values()
}
),
}
@@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
host = discovery_info.ssdp_headers["_host"]
self._abort_if_unique_id_configured(
# Store mac address for older entries.
# Store mac address and other data for older entries.
# The location is stored in the config entry such that when the location changes, the entry is reloaded.
updates={
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
CONFIG_ENTRY_HOST: host,
CONFIG_ENTRY_ST: discovery_info.ssdp_st,
},
)
@@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="config_entry_updated")
# Store discovery.
self._discoveries = [discovery_info]
self._add_discovery(discovery_info)
# Ensure user recognizable.
self.context["title_placeholders"] = {
@@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(step_id="ssdp_confirm")
assert self._discoveries
discovery = self._discoveries[0]
assert self.unique_id
discovery = self._remove_discovery(self.unique_id)
return await self._async_create_entry_from_discovery(discovery)
async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult:
"""Ignore this config flow."""
usn = user_input["unique_id"]
discovery = self._remove_discovery(usn)
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
data = {
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_ST: discovery.ssdp_st,
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
}
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
return self.async_create_entry(title=user_input["title"], data=data)
async def _async_create_entry_from_discovery(
self,
discovery: SsdpServiceInfo,
@@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
}
return self.async_create_entry(title=title, data=data)

View File

@@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS
LOGGER = logging.getLogger(__package__)
DOMAIN = "upnp"
DOMAIN_DISCOVERIES = "discoveries"
BYTES_RECEIVED = "bytes_received"
BYTES_SENT = "bytes_sent"
PACKETS_RECEIVED = "packets_received"

View File

@@ -3,7 +3,7 @@
"name": "UPnP/IGD",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"],
"requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"],
"dependencies": ["network", "ssdp"],
"codeowners": ["@StevenLooman"],
"ssdp": [

View File

@@ -3,7 +3,7 @@
"name": "Venstar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/venstar",
"requirements": ["venstarcolortouch==0.18"],
"requirements": ["venstarcolortouch==0.19"],
"codeowners": ["@garbled1"],
"iot_class": "local_polling",
"loggers": ["venstarcolortouch"]

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Gateway (Aqara)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
"requirements": ["PyXiaomiGateway==0.14.1"],
"requirements": ["PyXiaomiGateway==0.14.3"],
"after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"],
"zeroconf": ["_miio._udp.local."],

View File

@@ -2,7 +2,7 @@
"domain": "yeelight",
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"],
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"],
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
"config_flow": true,
"dependencies": ["network"],

View File

@@ -552,12 +552,20 @@ def _first_non_link_local_address(
"""Return the first ipv6 or non-link local ipv4 address, preferring IPv4."""
for address in addresses:
ip_addr = ip_address(address)
if not ip_addr.is_link_local and ip_addr.version == 4:
if (
not ip_addr.is_link_local
and not ip_addr.is_unspecified
and ip_addr.version == 4
):
return str(ip_addr)
# If we didn't find a good IPv4 address, check for IPv6 addresses.
for address in addresses:
ip_addr = ip_address(address)
if not ip_addr.is_link_local and ip_addr.version == 6:
if (
not ip_addr.is_link_local
and not ip_addr.is_unspecified
and ip_addr.version == 6
):
return str(ip_addr)
return None

View File

@@ -1090,11 +1090,17 @@ async def websocket_update_zha_configuration(
):
data_to_save[CUSTOM_CONFIGURATION][section].pop(entry)
# remove entire section block if empty
if not data_to_save[CUSTOM_CONFIGURATION][section]:
if (
not data_to_save[CUSTOM_CONFIGURATION].get(section)
and section in data_to_save[CUSTOM_CONFIGURATION]
):
data_to_save[CUSTOM_CONFIGURATION].pop(section)
# remove entire custom_configuration block if empty
if not data_to_save[CUSTOM_CONFIGURATION]:
if (
not data_to_save.get(CUSTOM_CONFIGURATION)
and CUSTOM_CONFIGURATION in data_to_save
):
data_to_save.pop(CUSTOM_CONFIGURATION)
_LOGGER.info(

View File

@@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel):
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
if min_mireds == 0:
self.warning(
"[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
self.MIN_MIREDS,
)
min_mireds = self.MIN_MIREDS
return min_mireds
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
if max_mireds == 0:
self.warning(
"[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
self.MAX_MIREDS,
)
max_mireds = self.MAX_MIREDS
return max_mireds
@property
def hs_supported(self) -> bool:

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from zigpy import types
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
from zigpy.exceptions import ZigbeeException
import zigpy.zcl
@@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel):
class InovelliConfigEntityChannel(ZigbeeChannel):
"""Inovelli Configuration Entity channel."""
class LEDEffectType(types.enum8):
"""Effect type for Inovelli Blue Series switch."""
Off = 0x00
Solid = 0x01
Fast_Blink = 0x02
Slow_Blink = 0x03
Pulse = 0x04
Chase = 0x05
Open_Close = 0x06
Small_To_Big = 0x07
Clear = 0xFF
REPORT_CONFIG = ()
ZCL_INIT_ATTRS = {
"dimming_speed_up_remote": False,
"dimming_speed_up_local": False,
"ramp_rate_off_to_on_local": False,
"ramp_rate_off_to_on_remote": False,
"dimming_speed_down_remote": False,
"dimming_speed_down_local": False,
"ramp_rate_on_to_off_local": False,
"ramp_rate_on_to_off_remote": False,
"minimum_level": False,
"maximum_level": False,
"invert_switch": False,
"auto_off_timer": False,
"default_level_local": False,
"default_level_remote": False,
"state_after_power_restored": False,
"load_level_indicator_timeout": False,
"active_power_reports": False,
"periodic_power_and_energy_reports": False,
"active_energy_reports": False,
"dimming_speed_up_remote": True,
"dimming_speed_up_local": True,
"ramp_rate_off_to_on_local": True,
"ramp_rate_off_to_on_remote": True,
"dimming_speed_down_remote": True,
"dimming_speed_down_local": True,
"ramp_rate_on_to_off_local": True,
"ramp_rate_on_to_off_remote": True,
"minimum_level": True,
"maximum_level": True,
"invert_switch": True,
"auto_off_timer": True,
"default_level_local": True,
"default_level_remote": True,
"state_after_power_restored": True,
"load_level_indicator_timeout": True,
"active_power_reports": True,
"periodic_power_and_energy_reports": True,
"active_energy_reports": True,
"power_type": False,
"switch_type": False,
"button_delay": False,
"smart_bulb_mode": False,
"double_tap_up_for_full_brightness": False,
"led_color_when_on": False,
"led_color_when_off": False,
"led_intensity_when_on": False,
"led_intensity_when_off": False,
"double_tap_up_for_full_brightness": True,
"led_color_when_on": True,
"led_color_when_off": True,
"led_intensity_when_on": True,
"led_intensity_when_off": True,
"local_protection": False,
"output_mode": False,
"on_off_led_mode": False,
"firmware_progress_led": False,
"relay_click_in_on_off_mode": False,
"on_off_led_mode": True,
"firmware_progress_led": True,
"relay_click_in_on_off_mode": True,
"disable_clear_notifications_double_tap": True,
}
async def issue_all_led_effect(
self,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,
@@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
async def issue_individual_led_effect(
self,
led_number: int = 1,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,

View File

@@ -221,11 +221,13 @@ def async_get_zha_config_value(
)
def async_cluster_exists(hass, cluster_id):
def async_cluster_exists(hass, cluster_id, skip_coordinator=True):
"""Determine if a device containing the specified in cluster is paired."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
zha_devices = zha_gateway.devices.values()
for zha_device in zha_devices:
if skip_coordinator and zha_device.is_coordinator:
continue
clusters_by_endpoint = zha_device.async_get_clusters()
for clusters in clusters_by_endpoint.values():
if (

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
from .core.channels.manufacturerspecific import InovelliConfigEntityChannel
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
from .core.helpers import async_get_zha_device
@@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(
"effect_type"
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
vol.Required("effect_type"): AllLEDEffectType.__getitem__,
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT,
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
vol.Required("effect_type"): SingleLEDEffectType.__getitem__,
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
}
)
ACTION_SCHEMA_MAP = {
INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
}
ACTION_SCHEMA = vol.Any(
INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
@@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = {
DEVICE_ACTION_SCHEMAS = {
INOVELLI_ALL_LED_EFFECT: vol.Schema(
{
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = {
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
{
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
@@ -127,6 +127,15 @@ async def async_call_action_from_config(
)
async def async_validate_action_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA)
config = schema(config)
return config
async def async_get_actions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:

View File

@@ -7,7 +7,7 @@
"bellows==0.34.2",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.84",
"zha-quirks==0.0.86",
"zigpy-deconz==0.19.0",
"zigpy==0.51.5",
"zigpy-xbee==0.16.2",

View File

@@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode(
_zcl_attribute: str = "relay_click_in_on_off_mode"
_attr_name: str = "Disable relay click in on off mode"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliDisableDoubleTapClearNotificationsMode(
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
):
"""Inovelli disable clear notifications double tap control."""
_zcl_attribute: str = "disable_clear_notifications_double_tap"
_attr_name: str = "Disable config 2x tap to clear notifications"

View File

@@ -7,9 +7,6 @@ from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY
from zwave_js_server.const.command_class.barrier_operator import BarrierState
from zwave_js_server.const.command_class.multilevel_switch import (
COVER_CLOSE_PROPERTY,
COVER_DOWN_PROPERTY,
COVER_OFF_PROPERTY,
COVER_ON_PROPERTY,
COVER_OPEN_PROPERTY,
COVER_UP_PROPERTY,
@@ -156,23 +153,14 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
open_value = (
cover_property = (
self.get_zwave_value(COVER_OPEN_PROPERTY)
or self.get_zwave_value(COVER_UP_PROPERTY)
or self.get_zwave_value(COVER_ON_PROPERTY)
)
if open_value:
# Stop the cover if it's opening
await self.info.node.async_set_value(open_value, False)
close_value = (
self.get_zwave_value(COVER_CLOSE_PROPERTY)
or self.get_zwave_value(COVER_DOWN_PROPERTY)
or self.get_zwave_value(COVER_OFF_PROPERTY)
)
if close_value:
# Stop the cover if it's closing
await self.info.node.async_set_value(close_value, False)
if cover_property:
# Stop the cover, will stop regardless of the actual direction of travel.
await self.info.node.async_set_value(cover_property, False)
class ZWaveTiltCover(ZWaveCover):

View File

@@ -660,24 +660,25 @@ class ConfigEntry:
data: dict[str, Any] | None = None,
) -> None:
"""Start a reauth flow."""
flow_context = {
"source": SOURCE_REAUTH,
"entry_id": self.entry_id,
"title_placeholders": {"name": self.title},
"unique_id": self.unique_id,
}
if context:
flow_context.update(context)
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain):
if flow["context"] == flow_context:
return
if any(
flow
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain)
if flow["context"].get("source") == SOURCE_REAUTH
and flow["context"].get("entry_id") == self.entry_id
):
# Reauth flow already in progress for this entry
return
hass.async_create_task(
hass.config_entries.flow.async_init(
self.domain,
context=flow_context,
context={
"source": SOURCE_REAUTH,
"entry_id": self.entry_id,
"title_placeholders": {"name": self.title},
"unique_id": self.unique_id,
}
| (context or {}),
data=self.data | (data or {}),
)
)

View File

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

View File

@@ -159,21 +159,25 @@
"integrations": {
"alexa": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Amazon Alexa"
},
"amazon_polly": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Amazon Polly"
},
"aws": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Amazon Web Services (AWS)"
},
"route53": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "AWS Route53"
}
@@ -284,6 +288,7 @@
},
"itunes": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Apple iTunes"
}
@@ -336,11 +341,13 @@
"integrations": {
"aruba": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Aruba"
},
"cppm_tracker": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Aruba ClearPass"
}
@@ -363,11 +370,13 @@
"integrations": {
"asterisk_cdr": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Asterisk Call Detail Records"
},
"asterisk_mbox": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Asterisk Voicemail"
}
@@ -710,16 +719,19 @@
"integrations": {
"cisco_ios": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Cisco IOS"
},
"cisco_mobility_express": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Cisco Mobility Express"
},
"cisco_webex_teams": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Cisco Webex Teams"
}
@@ -748,11 +760,13 @@
"integrations": {
"clicksend": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "ClickSend SMS"
},
"clicksend_tts": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "ClickSend TTS"
}
@@ -944,6 +958,7 @@
"integrations": {
"denon": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Denon Network Receivers"
},
@@ -1245,6 +1260,7 @@
"integrations": {
"avea": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Elgato Avea"
},
@@ -1291,11 +1307,13 @@
"integrations": {
"emoncms": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Emoncms"
},
"emoncms_history": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Emoncms History"
}
@@ -1377,6 +1395,7 @@
},
"epsonworkforce": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Epson Workforce"
}
@@ -1387,11 +1406,13 @@
"integrations": {
"eq3btsmart": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "eQ-3 Bluetooth Smart Thermostats"
},
"maxcube": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "eQ-3 MAX!"
}
@@ -1480,15 +1501,18 @@
"integrations": {
"ffmpeg": {
"integration_type": "hub",
"config_flow": false,
"name": "FFmpeg"
},
"ffmpeg_motion": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "calculated",
"name": "FFmpeg Motion"
},
"ffmpeg_noise": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "calculated",
"name": "FFmpeg Noise"
}
@@ -1871,11 +1895,13 @@
"integrations": {
"gc100": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Global Cach\u00e9 GC-100"
},
"itach": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "assumed_state",
"name": "Global Cach\u00e9 iTach TCP/IP to IR"
}
@@ -1910,26 +1936,31 @@
"integrations": {
"google_assistant": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Google Assistant"
},
"google_cloud": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Google Cloud Platform"
},
"google_domains": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Google Domains"
},
"google_maps": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Google Maps"
},
"google_pubsub": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Google Pub/Sub"
},
@@ -1941,6 +1972,7 @@
},
"google_translate": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Google Translate Text-to-Speech"
},
@@ -1951,6 +1983,7 @@
},
"google_wifi": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Google Wifi"
},
@@ -2119,11 +2152,13 @@
"integrations": {
"hikvision": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Hikvision"
},
"hikvisioncam": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Hikvision"
}
@@ -2176,6 +2211,7 @@
"integrations": {
"homematic": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Homematic"
},
@@ -2204,6 +2240,7 @@
},
"evohome": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Honeywell Total Connect Comfort (Europe)"
},
@@ -2297,11 +2334,13 @@
"integrations": {
"watson_iot": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "IBM Watson IoT Platform"
},
"watson_tts": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "IBM Watson TTS"
}
@@ -2342,6 +2381,7 @@
"integrations": {
"symfonisk": {
"integration_type": "virtual",
"config_flow": false,
"supported_by": "sonos",
"name": "IKEA SYMFONISK"
},
@@ -2720,6 +2760,7 @@
"integrations": {
"lg_netcast": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "LG Netcast"
},
@@ -2855,6 +2896,7 @@
},
"ue_smart_radio": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Logitech UE Smart Radio"
},
@@ -2901,6 +2943,7 @@
"integrations": {
"lutron": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Lutron"
},
@@ -2912,6 +2955,7 @@
},
"homeworks": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Lutron Homeworks"
}
@@ -3021,6 +3065,7 @@
},
"raincloud": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Melnor RainCloud"
}
@@ -3097,31 +3142,37 @@
},
"azure_service_bus": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Azure Service Bus"
},
"microsoft_face_detect": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face Detect"
},
"microsoft_face_identify": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face Identify"
},
"microsoft_face": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face"
},
"microsoft": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Text-to-Speech (TTS)"
},
"msteams": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Teams"
},
@@ -3133,6 +3184,7 @@
},
"xbox_live": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Xbox Live"
}
@@ -3260,6 +3312,7 @@
"integrations": {
"manual_mqtt": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Manual MQTT Alarm Control Panel"
},
@@ -3271,21 +3324,25 @@
},
"mqtt_eventstream": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "MQTT Eventstream"
},
"mqtt_json": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "MQTT JSON"
},
"mqtt_room": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "MQTT Room Presence"
},
"mqtt_statestream": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "MQTT Statestream"
}
@@ -3404,6 +3461,7 @@
},
"netgear_lte": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "NETGEAR LTE"
}
@@ -3765,11 +3823,13 @@
"integrations": {
"luci": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "OpenWrt (luci)"
},
"ubus": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "OpenWrt (ubus)"
}
@@ -3846,6 +3906,7 @@
"integrations": {
"panasonic_bluray": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Panasonic Blu-Ray Player"
},
@@ -4140,6 +4201,7 @@
"integrations": {
"qnap": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "QNAP"
},
@@ -4228,6 +4290,7 @@
"integrations": {
"rpi_camera": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Raspberry Pi Camera"
},
@@ -4238,6 +4301,7 @@
},
"remote_rpi_gpio": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Raspberry Pi Remote GPIO"
}
@@ -4437,11 +4501,13 @@
"integrations": {
"russound_rio": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_push",
"name": "Russound RIO"
},
"russound_rnet": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Russound RNET"
}
@@ -4464,6 +4530,7 @@
"integrations": {
"familyhub": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Samsung Family Hub"
},
@@ -4845,6 +4912,7 @@
},
"solaredge_local": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "SolarEdge Local"
}
@@ -4908,6 +4976,7 @@
},
"sony_projector": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Sony Projector"
},
@@ -5121,6 +5190,7 @@
"integrations": {
"synology_chat": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Synology Chat"
},
@@ -5132,6 +5202,7 @@
},
"synology_srm": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Synology SRM"
}
@@ -5218,11 +5289,13 @@
"integrations": {
"telegram": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Telegram"
},
"telegram_bot": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Telegram bot"
}
@@ -5239,6 +5312,7 @@
},
"tellstick": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "assumed_state",
"name": "TellStick"
}
@@ -5522,11 +5596,13 @@
},
"twilio_call": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Twilio Call"
},
"twilio_sms": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Twilio SMS"
}
@@ -5555,6 +5631,7 @@
"integrations": {
"ultraloq": {
"integration_type": "virtual",
"config_flow": false,
"iot_standards": [
"zwave"
],
@@ -5573,11 +5650,13 @@
},
"unifi_direct": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "UniFi AP"
},
"unifiled": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "UniFi LED"
},
@@ -5754,6 +5833,7 @@
"integrations": {
"vlc": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "VLC media player"
},
@@ -5978,11 +6058,13 @@
},
"xiaomi_tv": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "assumed_state",
"name": "Xiaomi TV"
},
"xiaomi": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Xiaomi"
}
@@ -6040,11 +6122,13 @@
"integrations": {
"yandex_transport": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling",
"name": "Yandex Transport"
},
"yandextts": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Yandex TTS"
}
@@ -6061,6 +6145,7 @@
},
"yeelightsunflower": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "local_polling",
"name": "Yeelight Sunflower"
}

View File

@@ -4,15 +4,15 @@ aiodiscover==1.4.13
aiohttp==3.8.1
aiohttp_cors==0.7.0
astral==2.2
async-upnp-client==0.32.1
async-upnp-client==0.32.2
async_timeout==4.0.2
atomicwrites-homeassistant==1.4.1
attrs==21.2.0
awesomeversion==22.9.0
bcrypt==3.1.7
bleak-retry-connector==2.8.2
bleak==0.19.1
bluetooth-adapters==0.6.0
bleak-retry-connector==2.8.4
bleak==0.19.2
bluetooth-adapters==0.7.0
bluetooth-auto-recovery==0.3.6
certifi>=2021.5.30
ciso8601==2.2.0
@@ -21,7 +21,7 @@ dbus-fast==1.61.1
fnvhash==0.1.0
hass-nabucasa==0.56.0
home-assistant-bluetooth==1.6.0
home-assistant-frontend==20221102.1
home-assistant-frontend==20221108.0
httpx==0.23.0
ifaddr==0.1.7
jinja2==3.1.2
@@ -37,7 +37,7 @@ pyudev==0.23.2
pyyaml==6.0
requests==2.28.1
scapy==2.4.5
sqlalchemy==1.4.42
sqlalchemy==1.4.44
typing-extensions>=4.4.0,<5.0
voluptuous-serialize==2.5.0
voluptuous==0.13.1

View File

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

View File

@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.20.2
PySwitchbot==0.20.5
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7
PyViCare==2.17.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.1
PyXiaomiGateway==0.14.3
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -153,7 +153,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==11.4.2
aioesphomeapi==11.4.3
# homeassistant.components.flo
aioflo==2021.11.0
@@ -171,7 +171,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.2.14
aiohomekit==2.2.19
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -237,7 +237,7 @@ aiopvpc==3.0.0
# homeassistant.components.lidarr
# homeassistant.components.radarr
# homeassistant.components.sonarr
aiopyarr==22.10.0
aiopyarr==22.11.0
# homeassistant.components.qnap_qsw
aioqsw==0.2.2
@@ -246,7 +246,7 @@ aioqsw==0.2.2
aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2022.03.0
aioridwell==2022.11.0
# homeassistant.components.senseme
aiosenseme==0.6.1
@@ -294,7 +294,7 @@ aioymaps==1.2.2
airly==1.1.0
# homeassistant.components.airthings_ble
airthings-ble==0.5.2
airthings-ble==0.5.3
# homeassistant.components.airthings
airthings_cloud==0.1.0
@@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.ssdp
# homeassistant.components.upnp
# homeassistant.components.yeelight
async-upnp-client==0.32.1
async-upnp-client==0.32.2
# homeassistant.components.supla
asyncpysupla==0.0.5
@@ -413,10 +413,10 @@ bimmer_connected==0.10.4
bizkaibus==0.1.1
# homeassistant.components.bluetooth
bleak-retry-connector==2.8.2
bleak-retry-connector==2.8.4
# homeassistant.components.bluetooth
bleak==0.19.1
bleak==0.19.2
# homeassistant.components.blebox
blebox_uniapi==2.1.3
@@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0
# bluepy==1.3.0
# homeassistant.components.bluetooth
bluetooth-adapters==0.6.0
bluetooth-adapters==0.7.0
# homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.6
@@ -725,7 +725,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0
# homeassistant.components.google
gcal-sync==2.2.3
gcal-sync==4.0.2
# homeassistant.components.geniushub
geniushub-client==0.6.30
@@ -804,7 +804,7 @@ greenwavereality==0.5.1
gridnet==4.0.0
# homeassistant.components.growatt_server
growattServer==1.2.3
growattServer==1.2.4
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -815,6 +815,9 @@ gstreamer-player==1.1.2
# homeassistant.components.profiler
guppy3==3.1.2
# homeassistant.components.iaqualink
h2==4.1.0
# homeassistant.components.homekit
ha-HAP-python==4.5.2
@@ -868,7 +871,7 @@ hole==0.7.0
holidays==0.16
# homeassistant.components.frontend
home-assistant-frontend==20221102.1
home-assistant-frontend==20221108.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -886,7 +889,7 @@ horimote==0.4.1
httplib2==0.20.4
# homeassistant.components.huawei_lte
huawei-lte-api==1.6.3
huawei-lte-api==1.6.7
# homeassistant.components.hydrawise
hydrawiser==0.2
@@ -1003,7 +1006,7 @@ librouteros==3.2.0
libsoundtouch==0.8
# homeassistant.components.life360
life360==5.1.1
life360==5.3.0
# homeassistant.components.osramlightify
lightify==1.0.7.3
@@ -1135,7 +1138,7 @@ nettigo-air-monitor==1.4.2
neurio==0.3.1
# homeassistant.components.nexia
nexia==2.0.5
nexia==2.0.6
# homeassistant.components.nextcloud
nextcloudmonitor==1.1.0
@@ -1238,7 +1241,7 @@ openwrt-luci-rpc==1.1.11
openwrt-ubus-rpc==0.0.2
# homeassistant.components.oralb
oralb-ble==0.10.0
oralb-ble==0.14.2
# homeassistant.components.oru
oru==0.1.11
@@ -1250,7 +1253,7 @@ orvibo==1.1.1
ovoenergy==1.2.0
# homeassistant.components.p1_monitor
p1monitor==2.1.0
p1monitor==2.1.1
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@@ -1312,7 +1315,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.25.3
plugwise==0.25.7
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1433,7 +1436,7 @@ pyaftership==21.11.0
pyairnow==1.1.0
# homeassistant.components.airvisual
pyairvisual==2022.07.0
pyairvisual==2022.11.1
# homeassistant.components.almond
pyalmond==0.0.2
@@ -1442,7 +1445,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==7.3.0
pyatmo==7.4.0
# homeassistant.components.atome
pyatome==0.1.1
@@ -1688,7 +1691,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0
# homeassistant.components.litterrobot
pylitterbot==2022.10.2
pylitterbot==2022.11.0
# homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1
@@ -2311,7 +2314,7 @@ spotipy==2.20.0
# homeassistant.components.recorder
# homeassistant.components.sql
sqlalchemy==1.4.42
sqlalchemy==1.4.44
# homeassistant.components.srp_energy
srpenergy==1.3.6
@@ -2487,7 +2490,7 @@ vehicle==0.4.0
velbus-aio==2022.10.4
# homeassistant.components.venstar
venstarcolortouch==0.18
venstarcolortouch==0.19
# homeassistant.components.vilfo
vilfo-api-client==0.3.2
@@ -2607,7 +2610,7 @@ zengge==0.2
zeroconf==0.39.4
# homeassistant.components.zha
zha-quirks==0.0.84
zha-quirks==0.0.86
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9

View File

@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.20.2
PySwitchbot==0.20.5
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7
PyViCare==2.17.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.1
PyXiaomiGateway==0.14.3
# homeassistant.components.remember_the_milk
RtmAPI==0.7.2
@@ -140,7 +140,7 @@ aioecowitt==2022.09.3
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==11.4.2
aioesphomeapi==11.4.3
# homeassistant.components.flo
aioflo==2021.11.0
@@ -155,7 +155,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.2.14
aiohomekit==2.2.19
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -212,7 +212,7 @@ aiopvpc==3.0.0
# homeassistant.components.lidarr
# homeassistant.components.radarr
# homeassistant.components.sonarr
aiopyarr==22.10.0
aiopyarr==22.11.0
# homeassistant.components.qnap_qsw
aioqsw==0.2.2
@@ -221,7 +221,7 @@ aioqsw==0.2.2
aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2022.03.0
aioridwell==2022.11.0
# homeassistant.components.senseme
aiosenseme==0.6.1
@@ -269,7 +269,7 @@ aioymaps==1.2.2
airly==1.1.0
# homeassistant.components.airthings_ble
airthings-ble==0.5.2
airthings-ble==0.5.3
# homeassistant.components.airthings
airthings_cloud==0.1.0
@@ -307,7 +307,7 @@ arcam-fmj==0.12.0
# homeassistant.components.ssdp
# homeassistant.components.upnp
# homeassistant.components.yeelight
async-upnp-client==0.32.1
async-upnp-client==0.32.2
# homeassistant.components.sleepiq
asyncsleepiq==1.2.3
@@ -337,10 +337,10 @@ bellows==0.34.2
bimmer_connected==0.10.4
# homeassistant.components.bluetooth
bleak-retry-connector==2.8.2
bleak-retry-connector==2.8.4
# homeassistant.components.bluetooth
bleak==0.19.1
bleak==0.19.2
# homeassistant.components.blebox
blebox_uniapi==2.1.3
@@ -352,7 +352,7 @@ blinkpy==0.19.2
bluemaestro-ble==0.2.0
# homeassistant.components.bluetooth
bluetooth-adapters==0.6.0
bluetooth-adapters==0.7.0
# homeassistant.components.bluetooth
bluetooth-auto-recovery==0.3.6
@@ -541,7 +541,7 @@ gTTS==2.2.4
garages-amsterdam==3.0.0
# homeassistant.components.google
gcal-sync==2.2.3
gcal-sync==4.0.2
# homeassistant.components.geocaching
geocachingapi==0.2.1
@@ -599,7 +599,7 @@ greeneye_monitor==3.0.3
gridnet==4.0.0
# homeassistant.components.growatt_server
growattServer==1.2.3
growattServer==1.2.4
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -607,6 +607,9 @@ gspread==5.5.0
# homeassistant.components.profiler
guppy3==3.1.2
# homeassistant.components.iaqualink
h2==4.1.0
# homeassistant.components.homekit
ha-HAP-python==4.5.2
@@ -648,7 +651,7 @@ hole==0.7.0
holidays==0.16
# homeassistant.components.frontend
home-assistant-frontend==20221102.1
home-assistant-frontend==20221108.0
# homeassistant.components.home_connect
homeconnect==0.7.2
@@ -663,7 +666,7 @@ homepluscontrol==0.0.5
httplib2==0.20.4
# homeassistant.components.huawei_lte
huawei-lte-api==1.6.3
huawei-lte-api==1.6.7
# homeassistant.components.hyperion
hyperion-py==0.7.5
@@ -741,7 +744,7 @@ librouteros==3.2.0
libsoundtouch==0.8
# homeassistant.components.life360
life360==5.1.1
life360==5.3.0
# homeassistant.components.logi_circle
logi_circle==0.2.3
@@ -825,7 +828,7 @@ netmap==0.7.0.2
nettigo-air-monitor==1.4.2
# homeassistant.components.nexia
nexia==2.0.5
nexia==2.0.6
# homeassistant.components.discord
nextcord==2.0.0a8
@@ -883,13 +886,13 @@ open-meteo==0.2.1
openerz-api==0.1.0
# homeassistant.components.oralb
oralb-ble==0.10.0
oralb-ble==0.14.2
# homeassistant.components.ovo_energy
ovoenergy==1.2.0
# homeassistant.components.p1_monitor
p1monitor==2.1.0
p1monitor==2.1.1
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@@ -939,7 +942,7 @@ plexauth==0.0.6
plexwebsocket==0.0.13
# homeassistant.components.plugwise
plugwise==0.25.3
plugwise==0.25.7
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1021,7 +1024,7 @@ pyaehw4a1==0.3.9
pyairnow==1.1.0
# homeassistant.components.airvisual
pyairvisual==2022.07.0
pyairvisual==2022.11.1
# homeassistant.components.almond
pyalmond==0.0.2
@@ -1030,7 +1033,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==7.3.0
pyatmo==7.4.0
# homeassistant.components.apple_tv
pyatv==0.10.3
@@ -1189,7 +1192,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0
# homeassistant.components.litterrobot
pylitterbot==2022.10.2
pylitterbot==2022.11.0
# homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1
@@ -1596,7 +1599,7 @@ spotipy==2.20.0
# homeassistant.components.recorder
# homeassistant.components.sql
sqlalchemy==1.4.42
sqlalchemy==1.4.44
# homeassistant.components.srp_energy
srpenergy==1.3.6
@@ -1721,7 +1724,7 @@ vehicle==0.4.0
velbus-aio==2022.10.4
# homeassistant.components.venstar
venstarcolortouch==0.18
venstarcolortouch==0.19
# homeassistant.components.vilfo
vilfo-api-client==0.3.2
@@ -1808,7 +1811,7 @@ zamg==0.1.1
zeroconf==0.39.4
# homeassistant.components.zha
zha-quirks==0.0.84
zha-quirks==0.0.86
# homeassistant.components.zha
zigpy-deconz==0.19.0

View File

@@ -113,8 +113,9 @@ def _populate_brand_integrations(
metadata = {
"integration_type": integration.integration_type,
}
if integration.config_flow:
metadata["config_flow"] = integration.config_flow
# Always set the config_flow key to avoid breaking the frontend
# https://github.com/home-assistant/frontend/issues/14376
metadata["config_flow"] = bool(integration.config_flow)
if integration.iot_class:
metadata["iot_class"] = integration.iot_class
if integration.supported_by:

View File

@@ -1,14 +1,14 @@
"""Define tests for the AirVisual config flow."""
from unittest.mock import patch
from pyairvisual.errors import (
AirVisualError,
from pyairvisual.cloud_api import (
InvalidKeyError,
KeyExpiredError,
NodeProError,
NotFoundError,
UnauthorizedError,
)
from pyairvisual.errors import AirVisualError
from pyairvisual.node import NodeProError
import pytest
from homeassistant import data_entry_flow

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