Compare commits

..

82 Commits

Author SHA1 Message Date
Paulus Schoutsen
bfb2867e8d Merge pull request #76398 from home-assistant/rc 2022-08-07 12:59:26 -04:00
Paulus Schoutsen
c9581f6a2e Bumped version to 2022.8.2 2022-08-07 12:13:08 -04:00
Joakim Plate
e96903fddf Postpone broadlink platform switch until config entry is ready (#76371) 2022-08-07 12:13:03 -04:00
Jean-François Roy
5026bff426 Bump aiobafi6 to 0.7.2 to unblock #76328 (#76330) 2022-08-07 12:13:02 -04:00
J. Nick Koston
4b63aa7f15 Bump pySwitchbot to 0.18.4 (#76322)
* Bump pySwitchbot to 0.18.3

Fixes #76321

Changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.17.3...0.18.3

* bump
2022-08-07 12:13:01 -04:00
David F. Mulcahey
1c2dd78e4c Fix ZHA light color temp support (#76305) 2022-08-07 12:13:00 -04:00
Robert Svensson
9cf11cf6ed Bump pydeconz to v102 (#76287) 2022-08-07 12:13:00 -04:00
puddly
8971a2073e Bump ZHA dependencies (#76275) 2022-08-07 12:12:59 -04:00
Maciej Bieniek
bfa64d2e01 Fix default sensor names in NextDNS integration (#76264) 2022-08-07 12:12:58 -04:00
J. Nick Koston
9c21d56539 Ensure bluetooth recovers if Dbus gets restarted (#76249) 2022-08-07 12:12:58 -04:00
Joakim Plate
8bfc352524 Use stored philips_js system data on start (#75981)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-08-07 12:12:57 -04:00
Jc2k
0e7bf35e4a Update gree to use the network component to set discovery interfaces (#75812) 2022-08-07 12:12:56 -04:00
Franck Nijhof
1dd701a89a Merge pull request #76245 from home-assistant/rc 2022-08-04 23:22:00 +02:00
Jc2k
d266b1ced6 Fix some homekit_controller pylint warnings and (local only) test failures (#76122) 2022-08-04 22:34:39 +02:00
Franck Nijhof
6727dab330 Bumped version to 2022.8.1 2022-08-04 21:51:02 +02:00
Jc2k
42509056bd Enable strict typing for HomeKit Controller config flow module (#76233) 2022-08-04 21:50:48 +02:00
Aaron Bach
a370e4f4b0 More explicitly call out special cases with SimpliSafe authorization code (#76232) 2022-08-04 21:50:44 +02:00
Phil Bruckner
a17e99f714 Fix Life360 recovery from server errors (#76231) 2022-08-04 21:50:41 +02:00
Franck Nijhof
db227a888d Fix spelling of OpenWrt in luci integration manifest (#76219) 2022-08-04 21:50:37 +02:00
mkmer
1808dd3d84 Bump AIOAladdin Connect to 0.1.41 (#76217) 2022-08-04 21:50:34 +02:00
Maciej Bieniek
31fed328ce Bump NextDNS library (#76207) 2022-08-04 21:50:30 +02:00
J. Nick Koston
1a030f118a BLE pairing reliablity fixes for HomeKit Controller (#76199)
- Remove the cached map from memory when unpairing so
  we do not reuse it again if they unpair/repair

- Fixes for accessories that use a config number of
  0

- General reliablity improvements to the pairing process
  under the hood of aiohomekit
2022-08-04 21:50:27 +02:00
Franck Nijhof
a4049e93d8 Mark RPI Power binary sensor as diagnostic (#76198) 2022-08-04 21:50:23 +02:00
Rami Mosleh
854ca853dc Fix nullable ip_address in mikrotik (#76197) 2022-08-04 21:50:20 +02:00
On Freund
2710e4b5ec Fix arm away in Risco (#76188) 2022-08-04 21:50:16 +02:00
Aaron Bach
450af52bac Add repair item to remove no-longer-functioning Flu Near You integration (#76177)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-08-04 21:50:13 +02:00
J. Nick Koston
60da54558e Fix race in bluetooth async_process_advertisements (#76176) 2022-08-04 21:50:09 +02:00
J. Nick Koston
11319defae Fix flux_led ignored entries not being respected (#76173) 2022-08-04 21:50:06 +02:00
Diogo Gomes
6340da72a5 Remove icon attribute if device class is set (#76161) 2022-08-04 21:50:02 +02:00
Jan Bouwhuis
5c9d557b10 Allow climate operation mode fan_only as custom mode in Alexa (#76148)
* Add support for FAN_ONLY mode

* Tests for fan_only as custom mode
2022-08-04 21:49:21 +02:00
J. Nick Koston
d2955a48b0 Bump bleak to 0.15.1 (#76136) 2022-08-04 21:47:15 +02:00
Martin Hjelmare
d2b98fa285 Fix zwave_js addon info (#76044)
* Add add-on store info command

* Use add-on store info command in zwave_js

* Fix init tests

* Update tests

* Fix method for addon store info

* Fix response parsing

* Fix store addon installed response parsing

* Remove addon info log that can contain network keys

* Add supervisor store addon info test

* Default to version None if add-on not installed

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2022-08-04 21:47:10 +02:00
Franck Nijhof
8ef3ca2daf Merge pull request #76119 from home-assistant/rc 2022-08-03 14:47:19 +02:00
Franck Nijhof
80a053a4cd Bumped version to 2022.8.0 2022-08-03 11:42:18 +02:00
Robert Svensson
81ee24738b Fix deconz group log warning (#76114) 2022-08-03 11:25:07 +02:00
Heine Furubotten
29f6d7818a Bump azure-servicebus to support py3.10 (#76092)
Bump azure-servicebus
2022-08-03 11:25:03 +02:00
Franck Nijhof
bc1e371cae Bumped version to 2022.8.0b7 2022-08-03 08:58:12 +02:00
J. Nick Koston
42a1f6ca20 Bump pySwitchbot to 0.17.3 to fix hang at startup (#76103) 2022-08-03 08:57:18 +02:00
J. Nick Koston
d85129c527 Bump aiohomekit to 1.2.3 to fix hang at startup (#76102) 2022-08-03 08:57:15 +02:00
Jc2k
ad14b5f3d7 Fix Xiaomi BLE UI string issues (#76099) 2022-08-03 08:57:12 +02:00
J. Nick Koston
51a6899a60 Only stat the .dockerenv file once (#76097) 2022-08-03 08:57:09 +02:00
J. Nick Koston
d2dc83c4c7 Handle additional bluetooth start exceptions (#76096) 2022-08-03 08:57:05 +02:00
Franck Nijhof
d7a418a219 Guard imports for type hinting in Bluetooth (#75984) 2022-08-03 08:57:01 +02:00
Jc2k
a78da6a000 Fix serialization of Xiaomi BLE reauth flow (#76095)
* Use data instead of context to fix serialisation bug

* Test change to async_start_reauth
2022-08-03 08:53:53 +02:00
J. Nick Koston
690f051a87 Bump pyatv to 0.10.3 (#76091) 2022-08-03 08:53:50 +02:00
Jc2k
c22cb13bd0 Add optional context parameter to async_start_reauth (#76077) 2022-08-03 08:53:47 +02:00
Eloston
213812f087 Add support for SwitchBot Plug Mini (#76056) 2022-08-03 08:53:43 +02:00
Franck Nijhof
19b0961084 Bumped version to 2022.8.0b6 2022-08-02 19:37:52 +02:00
Zack Barett
e073f6b439 Bump Frontend to 20220802.0 (#76087) 2022-08-02 19:36:26 +02:00
David F. Mulcahey
c4906414ea Ensure ZHA devices load before validating device triggers (#76084) 2022-08-02 19:36:22 +02:00
Erik Montnemery
cc9a130f58 Refresh homeassistant_alerts when hass has started (#76083) 2022-08-02 19:36:19 +02:00
mkmer
c90a223cb6 Bump AIOAladdinConnect to 0.1.39 (#76082) 2022-08-02 19:36:16 +02:00
Jc2k
2eddbf2381 Fix typo in new xiaomi_ble string (#76076) 2022-08-02 19:36:13 +02:00
Franck Nijhof
654e26052b Remove Somfy from Overkiz title in manifest (#76073) 2022-08-02 19:36:09 +02:00
Erik Montnemery
676664022d Handle missing attributes in meater objects (#76072) 2022-08-02 19:36:06 +02:00
Franck Nijhof
ed57951571 Small title adjustment to the Home Assistant Alerts integration (#76070) 2022-08-02 19:36:03 +02:00
lunmay
b9ee81dfc3 Fix capitalization in mitemp_bt strings (#76063)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-08-02 19:35:59 +02:00
Franck Nijhof
da00f5ba1e Bumped version to 2022.8.0b5 2022-08-02 09:38:21 +02:00
J. Nick Koston
30cd087f6f Fix govee H5074 data (#76057) 2022-08-02 09:38:03 +02:00
J. Nick Koston
66afd1e696 Bump bluetooth-adapters to 0.1.3 (#76052) 2022-08-02 09:37:59 +02:00
J. Nick Koston
23488f392b Lower bluetooth startup timeout to 9s to avoid warning (#76050) 2022-08-02 09:37:56 +02:00
mkmer
7140a9d025 Bump AIOAladdinConnect to 0.1.37 (#76046) 2022-08-02 09:37:52 +02:00
Erik Montnemery
4f671bccbc Support multiple trigger instances for a single webhook (#76037) 2022-08-02 09:37:48 +02:00
David F. Mulcahey
6b588d41ff Enhance logging for ZHA device trigger validation (#76036)
* Enhance logging for ZHA device trigger validation

* use IntegrationError
2022-08-02 09:37:45 +02:00
krazos
b962a6e767 Fix capitalization of Sonos "Status light" entity name (#76035)
Tweak capitalization of "Status light" entity name

Tweak capitalization of "Status light" entity name for consistency with blog post guidance, which states that entity names should start with a capital letter, with the rest of the words lower case
2022-08-02 09:37:41 +02:00
Jc2k
a332eb154c Add reauth flow to xiaomi_ble, fixes problem adding LYWSD03MMC (#76028) 2022-08-02 09:37:38 +02:00
Erik Montnemery
75747ce319 Support MWh for gas consumption sensors (#76016) 2022-08-02 09:37:35 +02:00
Allen Porter
c6038380d6 Add repair issues for nest app auth removal and yaml deprecation (#75974)
* Add repair issues for nest app auth removal and yaml deprecation

* Apply PR feedback

* Re-apply suggestion that i force pushed over

* Update criticality level
2022-08-02 09:37:32 +02:00
Joakim Plate
990975e908 Convert fjäråskupan to built in bluetooth (#75380)
* Add bluetooth discovery

* Use home assistant standard api

* Fixup manufacture data

* Adjust config flow to use standard features

* Fixup tests

* Mock bluetooth

* Simplify device check

* Fix missing typing

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-08-02 09:37:29 +02:00
rhadamantys
2a58bf06c1 Fix invalid enocean unique_id (#74508)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-08-02 09:37:25 +02:00
Paulus Schoutsen
5ab549653b Bumped version to 2022.8.0b4 2022-07-31 13:29:52 -07:00
Franck Nijhof
ffd2813150 Fix Home Connect services not being set up (#75997) 2022-07-31 13:29:45 -07:00
J. Nick Koston
ebf91fe46b Bump pySwitchbot to 0.16.0 to fix compat with bleak 0.15 (#75991) 2022-07-31 13:29:45 -07:00
mkmer
e330147751 Bump AIOAladdinConnect to 0.1.33 (#75986)
Bump aladdin_connect 0.1.33
2022-07-31 13:29:44 -07:00
mvn23
26a3621bb3 Bump pyotgw to 2.0.2 (#75980) 2022-07-31 13:29:43 -07:00
Franck Nijhof
58265664d1 Improve authentication handling for camera view (#75979) 2022-07-31 13:29:42 -07:00
mvn23
d205fb5064 Handle failed connection attempts in opentherm_gw (#75961) 2022-07-31 13:29:42 -07:00
J. Nick Koston
38ae2f4e9e Bump govee-ble to fix H5179 sensors (#75957)
Changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.12.4...v0.12.5
2022-07-31 13:29:41 -07:00
MasonCrawford
d84bc20a58 Small fixes for LG soundbar (#75938) 2022-07-31 13:29:40 -07:00
Heine Furubotten
a3276e00b9 Bump enturclient to 0.2.4 (#75928) 2022-07-31 13:29:40 -07:00
J. Nick Koston
bdb627539e Fix switchbot failing to setup when last_run_success is not saved (#75887) 2022-07-31 13:29:39 -07:00
Aaron Bach
240890e496 Appropriately mark Guardian entities as unavailable during reboot (#75234) 2022-07-31 13:29:38 -07:00
132 changed files with 1993 additions and 612 deletions

View File

@@ -388,6 +388,7 @@ omit =
homeassistant/components/flume/__init__.py
homeassistant/components/flume/sensor.py
homeassistant/components/flunearyou/__init__.py
homeassistant/components/flunearyou/repairs.py
homeassistant/components/flunearyou/sensor.py
homeassistant/components/folder/sensor.py
homeassistant/components/folder_watcher/*

View File

@@ -128,6 +128,7 @@ homeassistant.components.homekit.util
homeassistant.components.homekit_controller
homeassistant.components.homekit_controller.alarm_control_panel
homeassistant.components.homekit_controller.button
homeassistant.components.homekit_controller.config_flow
homeassistant.components.homekit_controller.const
homeassistant.components.homekit_controller.lock
homeassistant.components.homekit_controller.select

View File

@@ -1044,8 +1044,8 @@ build.json @home-assistant/supervisor
/tests/components/switch/ @home-assistant/core
/homeassistant/components/switch_as_x/ @home-assistant/core
/tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston
/homeassistant/components/switcher_kis/ @tomerfi @thecode
/tests/components/switcher_kis/ @tomerfi @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li

View File

@@ -2,7 +2,7 @@
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["AIOAladdinConnect==0.1.31"],
"requirements": ["AIOAladdinConnect==0.1.41"],
"codeowners": ["@mkmer"],
"iot_class": "cloud_polling",
"loggers": ["aladdin_connect"],

View File

@@ -68,16 +68,19 @@ API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"}
# back to HA state.
API_THERMOSTAT_MODES = OrderedDict(
[
(climate.HVAC_MODE_HEAT, "HEAT"),
(climate.HVAC_MODE_COOL, "COOL"),
(climate.HVAC_MODE_HEAT_COOL, "AUTO"),
(climate.HVAC_MODE_AUTO, "AUTO"),
(climate.HVAC_MODE_OFF, "OFF"),
(climate.HVAC_MODE_FAN_ONLY, "OFF"),
(climate.HVAC_MODE_DRY, "CUSTOM"),
(climate.HVACMode.HEAT, "HEAT"),
(climate.HVACMode.COOL, "COOL"),
(climate.HVACMode.HEAT_COOL, "AUTO"),
(climate.HVACMode.AUTO, "AUTO"),
(climate.HVACMode.OFF, "OFF"),
(climate.HVACMode.FAN_ONLY, "CUSTOM"),
(climate.HVACMode.DRY, "CUSTOM"),
]
)
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
API_THERMOSTAT_MODES_CUSTOM = {
climate.HVACMode.DRY: "DEHUMIDIFY",
climate.HVACMode.FAN_ONLY: "FAN",
}
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode

View File

@@ -3,7 +3,7 @@
"name": "Apple TV",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.10.2"],
"requirements": ["pyatv==0.10.3"],
"dependencies": ["zeroconf"],
"zeroconf": [
"_mediaremotetv._tcp.local.",

View File

@@ -2,7 +2,7 @@
"domain": "azure_service_bus",
"name": "Azure Service Bus",
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
"requirements": ["azure-servicebus==0.50.3"],
"requirements": ["azure-servicebus==7.8.0"],
"codeowners": ["@hfurubotten"],
"iot_class": "cloud_push",
"loggers": ["azure"]

View File

@@ -2,11 +2,12 @@
import json
import logging
from azure.servicebus.aio import Message, ServiceBusClient
from azure.servicebus.common.errors import (
MessageSendFailed,
from azure.servicebus import ServiceBusMessage
from azure.servicebus.aio import ServiceBusClient
from azure.servicebus.exceptions import (
MessagingEntityNotFoundError,
ServiceBusConnectionError,
ServiceBusResourceNotFound,
ServiceBusError,
)
import voluptuous as vol
@@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None):
try:
if queue_name:
client = servicebus.get_queue(queue_name)
client = servicebus.get_queue_sender(queue_name)
else:
client = servicebus.get_topic(topic_name)
except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err:
client = servicebus.get_topic_sender(topic_name)
except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err:
_LOGGER.error(
"Connection error while creating client for queue/topic '%s'. %s",
queue_name or topic_name,
@@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService):
if data := kwargs.get(ATTR_DATA):
dto.update(data)
queue_message = Message(json.dumps(dto))
queue_message.properties.content_type = CONTENT_TYPE_JSON
queue_message = ServiceBusMessage(
json.dumps(dto), content_type=CONTENT_TYPE_JSON
)
try:
await self._client.send(queue_message)
except MessageSendFailed as err:
await self._client.send_messages(queue_message)
except ServiceBusError as err:
_LOGGER.error(
"Could not send service bus notification to %s. %s",
self._client.name,

View File

@@ -3,7 +3,7 @@
"name": "Big Ass Fans",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"requirements": ["aiobafi6==0.7.0"],
"requirements": ["aiobafi6==0.7.2"],
"codeowners": ["@bdraco", "@jfroy"],
"iot_class": "local_push",
"zeroconf": [

View File

@@ -8,12 +8,12 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
import logging
from typing import Final
import time
from typing import TYPE_CHECKING, Final
import async_timeout
from bleak import BleakError
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from dbus_next import InvalidMessageError
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
@@ -27,8 +27,8 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_bluetooth
from homeassistant.util.package import is_docker_env
from . import models
from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN
@@ -42,14 +42,25 @@ from .models import HaBleakScanner, HaBleakScannerWrapper
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
from .util import async_get_bluetooth_adapters
if TYPE_CHECKING:
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5
START_TIMEOUT = 15
START_TIMEOUT = 9
SOURCE_LOCAL: Final = "local"
SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT)
MONOTONIC_TIME = time.monotonic
@dataclass
class BluetoothServiceInfoBleak(BluetoothServiceInfo):
@@ -182,7 +193,7 @@ async def async_process_advertisements(
def _async_discovered_device(
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
) -> None:
if callback(service_info):
if not done.done() and callback(service_info):
done.set_result(service_info)
unload = async_register_callback(hass, _async_discovered_device, match_dict, mode)
@@ -246,9 +257,10 @@ async def async_setup_entry(
) -> bool:
"""Set up the bluetooth integration from a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_start(
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
async with manager.start_stop_lock:
await manager.async_start(
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
@@ -257,8 +269,6 @@ async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None:
"""Handle options update."""
manager: BluetoothManager = hass.data[DOMAIN]
manager.async_start_reload()
await hass.config_entries.async_reload(entry.entry_id)
@@ -267,7 +277,9 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_stop()
async with manager.start_stop_lock:
manager.async_start_reload()
await manager.async_stop()
return True
@@ -283,13 +295,19 @@ class BluetoothManager:
self.hass = hass
self._integration_matcher = integration_matcher
self.scanner: HaBleakScanner | None = None
self.start_stop_lock = asyncio.Lock()
self._cancel_device_detected: CALLBACK_TYPE | None = None
self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
self._cancel_stop: CALLBACK_TYPE | None = None
self._cancel_watchdog: CALLBACK_TYPE | None = None
self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {}
self._callbacks: list[
tuple[BluetoothCallback, BluetoothCallbackMatcher | None]
] = []
self._last_detection = 0.0
self._reloading = False
self._adapter: str | None = None
self._scanning_mode = BluetoothScanningMode.ACTIVE
@hass_callback
def async_setup(self) -> None:
@@ -311,6 +329,8 @@ class BluetoothManager:
) -> None:
"""Set up BT Discovery."""
assert self.scanner is not None
self._adapter = adapter
self._scanning_mode = scanning_mode
if self._reloading:
# On reload, we need to reset the scanner instance
# since the devices in its history may not be reachable
@@ -337,16 +357,70 @@ class BluetoothManager:
try:
async with async_timeout.timeout(START_TIMEOUT):
await self.scanner.start() # type: ignore[no-untyped-call]
except InvalidMessageError as ex:
self._cancel_device_detected()
_LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True)
raise ConfigEntryNotReady(
f"Invalid DBus message received: {ex}; try restarting `dbus`"
) from ex
except BrokenPipeError as ex:
self._cancel_device_detected()
_LOGGER.debug("DBus connection broken: %s", ex, exc_info=True)
if is_docker_env():
raise ConfigEntryNotReady(
f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container"
) from ex
raise ConfigEntryNotReady(
f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`"
) from ex
except FileNotFoundError as ex:
self._cancel_device_detected()
_LOGGER.debug(
"FileNotFoundError while starting bluetooth: %s", ex, exc_info=True
)
if is_docker_env():
raise ConfigEntryNotReady(
f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}"
) from ex
raise ConfigEntryNotReady(
f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}"
) from ex
except asyncio.TimeoutError as ex:
self._cancel_device_detected()
raise ConfigEntryNotReady(
f"Timed out starting Bluetooth after {START_TIMEOUT} seconds"
) from ex
except (FileNotFoundError, BleakError) as ex:
except BleakError as ex:
self._cancel_device_detected()
_LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True)
raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex
self.async_setup_unavailable_tracking()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
self._async_setup_scanner_watchdog()
self._cancel_stop = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
)
@hass_callback
def _async_setup_scanner_watchdog(self) -> None:
"""If Dbus gets restarted or updated, we need to restart the scanner."""
self._last_detection = MONOTONIC_TIME()
self._cancel_watchdog = async_track_time_interval(
self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL
)
async def _async_scanner_watchdog(self, now: datetime) -> None:
"""Check if the scanner is running."""
time_since_last_detection = MONOTONIC_TIME() - self._last_detection
if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT:
return
_LOGGER.info(
"Bluetooth scanner has gone quiet for %s, restarting",
SCANNER_WATCHDOG_INTERVAL,
)
async with self.start_stop_lock:
self.async_start_reload()
await self.async_stop()
await self.async_start(self._scanning_mode, self._adapter)
@hass_callback
def async_setup_unavailable_tracking(self) -> None:
@@ -381,6 +455,7 @@ class BluetoothManager:
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Handle a detected device."""
self._last_detection = MONOTONIC_TIME()
matched_domains = self._integration_matcher.match_domains(
device, advertisement_data
)
@@ -493,14 +568,26 @@ class BluetoothManager:
for device_adv in self.scanner.history.values()
]
async def async_stop(self, event: Event | None = None) -> None:
async def _async_hass_stopping(self, event: Event) -> None:
"""Stop the Bluetooth integration at shutdown."""
self._cancel_stop = None
await self.async_stop()
async def async_stop(self) -> None:
"""Stop bluetooth discovery."""
_LOGGER.debug("Stopping bluetooth discovery")
if self._cancel_watchdog:
self._cancel_watchdog()
self._cancel_watchdog = None
if self._cancel_device_detected:
self._cancel_device_detected()
self._cancel_device_detected = None
if self._cancel_unavailable_tracking:
self._cancel_unavailable_tracking()
self._cancel_unavailable_tracking = None
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
if self.scanner:
try:
await self.scanner.stop() # type: ignore[no-untyped-call]

View File

@@ -1,18 +1,20 @@
"""Config flow to configure the Bluetooth integration."""
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components import onboarding
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN
from .util import async_get_bluetooth_adapters
if TYPE_CHECKING:
from homeassistant.data_entry_flow import FlowResult
class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Bluetooth."""

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluetooth",
"dependencies": ["websocket_api"],
"quality_scale": "internal",
"requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"],
"requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"],
"codeowners": ["@bdraco"],
"config_flow": true,
"iot_class": "local_push"

View File

@@ -1,17 +1,21 @@
"""The bluetooth integration matchers."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import fnmatch
from typing import Final, TypedDict
from typing import TYPE_CHECKING, Final, TypedDict
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from lru import LRU # pylint: disable=no-name-in-module
from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional
if TYPE_CHECKING:
from collections.abc import Mapping
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
MAX_REMEMBER_ADDRESSES: Final = 2048

View File

@@ -4,10 +4,9 @@ from __future__ import annotations
import asyncio
import contextlib
import logging
from typing import Any, Final
from typing import TYPE_CHECKING, Any, Final
from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import (
AdvertisementData,
AdvertisementDataCallback,
@@ -16,6 +15,10 @@ from bleak.backends.scanner import (
from homeassistant.core import CALLBACK_TYPE, callback as hass_callback
if TYPE_CHECKING:
from bleak.backends.device import BLEDevice
_LOGGER = logging.getLogger(__name__)
FILTER_UUIDS: Final = "UUIDs"

View File

@@ -1,16 +1,19 @@
"""Passive update coordinator for the Bluetooth integration."""
from __future__ import annotations
from collections.abc import Callable, Generator
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
from .update_coordinator import BasePassiveBluetoothCoordinator
if TYPE_CHECKING:
from collections.abc import Callable, Generator
import logging
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
"""Class to manage passive bluetooth advertisements.

View File

@@ -1,20 +1,24 @@
"""Passive update processors for the Bluetooth integration."""
from __future__ import annotations
from collections.abc import Callable, Mapping
import dataclasses
import logging
from typing import Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
from .const import DOMAIN
from .update_coordinator import BasePassiveBluetoothCoordinator
if TYPE_CHECKING:
from collections.abc import Callable, Mapping
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
@dataclasses.dataclass(frozen=True)
class PassiveBluetoothEntityKey:

View File

@@ -26,11 +26,13 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import BroadlinkDevice
from .const import DOMAIN
from .entity import BroadlinkEntity
from .helpers import data_packet, import_device, mac_address
@@ -80,8 +82,18 @@ async def async_setup_platform(
host = config.get(CONF_HOST)
if switches := config.get(CONF_SWITCHES):
platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {})
platform_data.setdefault(mac_addr, []).extend(switches)
platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {})
async_add_entities_config_entry: AddEntitiesCallback
device: BroadlinkDevice
async_add_entities_config_entry, device = platform_data.get(
mac_addr, (None, None)
)
if not async_add_entities_config_entry:
raise PlatformNotReady
async_add_entities_config_entry(
BroadlinkRMSwitch(device, config) for config in switches
)
else:
_LOGGER.warning(
@@ -104,12 +116,8 @@ async def async_setup_entry(
switches: list[BroadlinkSwitch] = []
if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}:
platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {})
user_defined_switches = platform_data.get(device.api.mac, {})
switches.extend(
BroadlinkRMSwitch(device, config) for config in user_defined_switches
)
platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {})
platform_data[device.api.mac] = async_add_entities, device
elif device.api.type == "SP1":
switches.append(BroadlinkSP1Switch(device))

View File

@@ -14,7 +14,7 @@ import os
from random import SystemRandom
from typing import Final, Optional, cast, final
from aiohttp import web
from aiohttp import hdrs, web
import async_timeout
import attr
import voluptuous as vol
@@ -715,8 +715,11 @@ class CameraView(HomeAssistantView):
)
if not authenticated:
if request[KEY_AUTHENTICATED]:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized()
# Invalid sigAuth or camera access token
raise web.HTTPForbidden()
if not camera.is_on:

View File

@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==100"],
"requirements": ["pydeconz==102"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = (
sensor.SensorDeviceClass.GAS,
)
GAS_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR),
sensor.SensorDeviceClass.ENERGY: (
ENERGY_WATT_HOUR,
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
),
sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
}
GAS_PRICE_UNITS = tuple(

View File

@@ -5,12 +5,14 @@ from enocean.utils import combine_hex
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.const import CONF_ID, CONF_NAME
from homeassistant.const import CONF_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, LOGGER
from .device import EnOceanEntity
CONF_CHANNEL = "channel"
@@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(
def generate_unique_id(dev_id: list[int], channel: int) -> str:
"""Generate a valid unique id."""
return f"{combine_hex(dev_id)}-{channel}"
def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None:
"""Migrate old unique ids to new unique ids."""
old_unique_id = f"{combine_hex(dev_id)}"
ent_reg = entity_registry.async_get(hass)
entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id)
if entity_id is not None:
new_unique_id = generate_unique_id(dev_id, channel)
try:
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
except ValueError:
LOGGER.warning(
"Skip migration of id [%s] to [%s] because it already exists",
old_unique_id,
new_unique_id,
)
else:
LOGGER.debug(
"Migrating unique_id from [%s] to [%s]",
old_unique_id,
new_unique_id,
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the EnOcean switch platform."""
@@ -36,7 +68,8 @@ def setup_platform(
dev_id = config.get(CONF_ID)
dev_name = config.get(CONF_NAME)
add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
_migrate_to_new_unique_id(hass, dev_id, channel)
async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
class EnOceanSwitch(EnOceanEntity, SwitchEntity):
@@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity):
self._on_state = False
self._on_state2 = False
self.channel = channel
self._attr_unique_id = f"{combine_hex(dev_id)}"
self._attr_unique_id = generate_unique_id(dev_id, channel)
@property
def is_on(self):

View File

@@ -2,7 +2,7 @@
"domain": "entur_public_transport",
"name": "Entur",
"documentation": "https://www.home-assistant.io/integrations/entur_public_transport",
"requirements": ["enturclient==0.2.3"],
"requirements": ["enturclient==0.2.4"],
"codeowners": ["@hfurubotten"],
"iot_class": "cloud_polling",
"loggers": ["enturclient"]

View File

@@ -5,14 +5,20 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from bleak import BleakScanner
from fjaraskupan import Device, State, device_filter
from fjaraskupan import Device, State
from homeassistant.components.bluetooth import (
BluetoothCallbackMatcher,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfoBleak,
async_address_present,
async_register_callback,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
@@ -23,11 +29,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DISPATCH_DETECTION, DOMAIN
if TYPE_CHECKING:
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.FAN,
@@ -70,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]):
async def _async_update_data(self) -> State:
"""Handle an explicit update request."""
if self._refresh_was_scheduled:
raise UpdateFailed("No data received within schedule.")
if async_address_present(self.hass, self.device.address):
return self.device.state
raise UpdateFailed(
"No data received within schedule, and device is no longer present"
)
await self.device.update()
return self.device.state
def detection_callback(
self, ble_device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None:
"""Handle a new announcement of data."""
self.device.detection_callback(ble_device, advertisement_data)
self.device.detection_callback(service_info.device, service_info.advertisement)
self.async_set_updated_data(self.device.state)
@@ -87,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]):
class EntryState:
"""Store state of config entry."""
scanner: BleakScanner
coordinators: dict[str, Coordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Fjäråskupan from a config entry."""
scanner = BleakScanner(filters={"DuplicateData": True})
state = EntryState(scanner, {})
state = EntryState({})
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = state
async def detection_callback(
ble_device: BLEDevice, advertisement_data: AdvertisementData
def detection_callback(
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
) -> None:
if data := state.coordinators.get(ble_device.address):
_LOGGER.debug(
"Update: %s %s - %s", ble_device.name, ble_device, advertisement_data
)
data.detection_callback(ble_device, advertisement_data)
if change != BluetoothChange.ADVERTISEMENT:
return
if data := state.coordinators.get(service_info.address):
_LOGGER.debug("Update: %s", service_info)
data.detection_callback(service_info)
else:
if not device_filter(ble_device, advertisement_data):
return
_LOGGER.debug("Detected: %s", service_info)
_LOGGER.debug(
"Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data
)
device = Device(ble_device)
device = Device(service_info.device)
device_info = DeviceInfo(
identifiers={(DOMAIN, ble_device.address)},
identifiers={(DOMAIN, service_info.address)},
manufacturer="Fjäråskupan",
name="Fjäråskupan",
)
coordinator: Coordinator = Coordinator(hass, device, device_info)
coordinator.detection_callback(ble_device, advertisement_data)
coordinator.detection_callback(service_info)
state.coordinators[ble_device.address] = coordinator
state.coordinators[service_info.address] = coordinator
async_dispatcher_send(
hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator
)
scanner.register_detection_callback(detection_callback)
await scanner.start()
async def on_hass_stop(event: Event) -> None:
await scanner.stop()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
async_register_callback(
hass,
detection_callback,
BluetoothCallbackMatcher(
manufacturer_id=20296,
manufacturer_data_start=[79, 68, 70, 74, 65, 82],
),
BluetoothScanningMode.ACTIVE,
)
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@@ -177,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id)
await entry_state.scanner.stop()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -1,42 +1,25 @@
"""Config flow for Fjäråskupan integration."""
from __future__ import annotations
import asyncio
import async_timeout
from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from fjaraskupan import device_filter
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_entry_flow import register_discovery_flow
from .const import DOMAIN
CONST_WAIT_TIME = 5.0
async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
event = asyncio.Event()
service_infos = async_discovered_service_info(hass)
def detection(device: BLEDevice, advertisement_data: AdvertisementData):
if device_filter(device, advertisement_data):
event.set()
for service_info in service_infos:
if device_filter(service_info.device, service_info.advertisement):
return True
async with BleakScanner(
detection_callback=detection,
filters={"DuplicateData": True},
):
try:
async with async_timeout.timeout(CONST_WAIT_TIME):
await event.wait()
except asyncio.TimeoutError:
return False
return True
return False
register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices)

View File

@@ -6,5 +6,12 @@
"requirements": ["fjaraskupan==1.0.2"],
"codeowners": ["@elupus"],
"iot_class": "local_polling",
"loggers": ["bleak", "fjaraskupan"]
"loggers": ["bleak", "fjaraskupan"],
"dependencies": ["bluetooth"],
"bluetooth": [
{
"manufacturer_id": 20296,
"manufacturer_data_start": [79, 68, 70, 74, 65, 82]
}
]
}

View File

@@ -9,6 +9,7 @@ from typing import Any
from pyflunearyou import Client
from pyflunearyou.errors import FluNearYouError
from homeassistant.components.repairs import IssueSeverity, async_create_issue
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
@@ -26,6 +27,15 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flu Near You as config entry."""
async_create_issue(
hass,
DOMAIN,
"integration_removal",
is_fixable=True,
severity=IssueSeverity.ERROR,
translation_key="integration_removal",
)
websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession)

View File

@@ -3,6 +3,7 @@
"name": "Flu Near You",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flunearyou",
"dependencies": ["repairs"],
"requirements": ["pyflunearyou==2.0.2"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",

View File

@@ -0,0 +1,42 @@
"""Repairs platform for the Flu Near You integration."""
from __future__ import annotations
import asyncio
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.core import HomeAssistant
from .const import DOMAIN
class FluNearYouFixFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
removal_tasks = [
self.hass.config_entries.async_remove(entry.entry_id)
for entry in self.hass.config_entries.async_entries(DOMAIN)
]
await asyncio.gather(*removal_tasks)
return self.async_create_entry(title="Fixed issue", data={})
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
async def async_create_fix_flow(
hass: HomeAssistant, issue_id: str
) -> FluNearYouFixFlow:
"""Create flow."""
return FluNearYouFixFlow()

View File

@@ -16,5 +16,18 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
}
},
"issues": {
"integration_removal": {
"title": "Flu Near You is no longer available",
"fix_flow": {
"step": {
"confirm": {
"title": "Remove Flu Near You",
"description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance."
}
}
}
}
}
}

View File

@@ -16,5 +16,18 @@
"title": "Configure Flu Near You"
}
}
},
"issues": {
"integration_removal": {
"fix_flow": {
"step": {
"confirm": {
"description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.",
"title": "Remove Flu Near You"
}
}
},
"title": "Flu Near You is no longer available"
}
}
}

View File

@@ -105,27 +105,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert mac_address is not None
mac = dr.format_mac(mac_address)
await self.async_set_unique_id(mac)
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == device[ATTR_IPADDR] or (
entry.unique_id
and ":" in entry.unique_id
and mac_matches_by_one(entry.unique_id, mac)
for entry in self._async_current_entries(include_ignore=True):
if not (
entry.data.get(CONF_HOST) == device[ATTR_IPADDR]
or (
entry.unique_id
and ":" in entry.unique_id
and mac_matches_by_one(entry.unique_id, mac)
)
):
if (
async_update_entry_from_discovery(
self.hass, entry, device, None, allow_update_mac
)
or entry.state == config_entries.ConfigEntryState.SETUP_RETRY
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
else:
async_dispatcher_send(
self.hass,
FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
)
continue
if entry.source == config_entries.SOURCE_IGNORE:
raise AbortFlow("already_configured")
if (
async_update_entry_from_discovery(
self.hass, entry, device, None, allow_update_mac
)
or entry.state == config_entries.ConfigEntryState.SETUP_RETRY
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
else:
async_dispatcher_send(
self.hass,
FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
)
raise AbortFlow("already_configured")
async def _async_handle_discovery(self) -> FlowResult:
"""Handle any discovery."""

View File

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

View File

@@ -24,7 +24,7 @@
"service_uuid": "00008251-0000-1000-8000-00805f9b34fb"
}
],
"requirements": ["govee-ble==0.12.4"],
"requirements": ["govee-ble==0.12.6"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"

View File

@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_scan_update(_=None):
await gree_discovery.discovery.scan()
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr)
_LOGGER.debug("Scanning network for Gree devices")
await _async_scan_update()

View File

@@ -1,6 +1,7 @@
"""Config flow for Gree."""
from greeclimate.discovery import Discovery
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow
@@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN
async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
gree_discovery = Discovery(DISCOVERY_TIMEOUT)
devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT)
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
devices = await gree_discovery.scan(
wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr
)
return len(devices) > 0

View File

@@ -3,7 +3,8 @@
"name": "Gree Climate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gree",
"requirements": ["greeclimate==1.2.0"],
"requirements": ["greeclimate==1.3.0"],
"dependencies": ["network"],
"codeowners": ["@cmroche"],
"iot_class": "local_polling",
"loggers": ["greeclimate"]

View File

@@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# so we use a lock to ensure that only one API request is reaching it at a time:
api_lock = asyncio.Lock()
async def async_init_coordinator(
coordinator: GuardianDataUpdateCoordinator,
) -> None:
"""Initialize a GuardianDataUpdateCoordinator."""
await coordinator.async_initialize()
await coordinator.async_config_entry_first_refresh()
# Set up GuardianDataUpdateCoordinators for the valve controller:
valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {}
init_valve_controller_tasks = []
@@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api
] = GuardianDataUpdateCoordinator(
hass,
entry=entry,
client=client,
api_name=api,
api_coro=api_coro,
api_lock=api_lock,
valve_controller_uid=entry.data[CONF_UID],
)
init_valve_controller_tasks.append(coordinator.async_refresh())
init_valve_controller_tasks.append(async_init_coordinator(coordinator))
await asyncio.gather(*init_valve_controller_tasks)
@@ -352,6 +360,7 @@ class PairedSensorManager:
coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator(
self._hass,
entry=self._entry,
client=self._client,
api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}",
api_coro=lambda: cast(
@@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]):
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity.
"""Update the entity's underlying data.
This should be extended by Guardian platforms.
"""

View File

@@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity):
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity."""
"""Update the entity's underlying data."""
if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED:
self._attr_is_on = self.coordinator.data["wet"]
elif self.entity_description.key == SENSOR_KIND_MOVED:

View File

@@ -15,6 +15,7 @@ from homeassistant.components.button import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity):
raise HomeAssistantError(
f'Error while pressing button "{self.entity_id}": {err}'
) from err
async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested)

View File

@@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity):
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity."""
"""Update the entity's underlying data."""
if self.entity_description.key == SENSOR_KIND_BATTERY:
self._attr_native_value = self.coordinator.data["battery"]
elif self.entity_description.key == SENSOR_KIND_TEMPERATURE:
@@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity):
@callback
def _async_update_from_latest_data(self) -> None:
"""Update the entity."""
"""Update the entity's underlying data."""
if self.entity_description.key == SENSOR_KIND_TEMPERATURE:
self._attr_native_value = self.coordinator.data["temperature"]
elif self.entity_description.key == SENSOR_KIND_UPTIME:

View File

@@ -9,21 +9,28 @@ from typing import Any, cast
from aioguardian import Client
from aioguardian.errors import GuardianError
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30)
SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}"
class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
"""Define an extended DataUpdateCoordinator with some Guardian goodies."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
*,
entry: ConfigEntry,
client: Client,
api_name: str,
api_coro: Callable[..., Awaitable],
@@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
self._api_coro = api_coro
self._api_lock = api_lock
self._client = client
self._signal_handler_unsubs: list[Callable[..., None]] = []
self.config_entry = entry
self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format(
self.config_entry.entry_id
)
async def _async_update_data(self) -> dict[str, Any]:
"""Execute a "locked" API request against the valve controller."""
@@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
except GuardianError as err:
raise UpdateFailed(err) from err
return cast(dict[str, Any], resp["data"])
async def async_initialize(self) -> None:
"""Initialize the coordinator."""
@callback
def async_reboot_requested() -> None:
"""Respond to a reboot request."""
self.last_update_success = False
self.async_update_listeners()
self._signal_handler_unsubs.append(
async_dispatcher_connect(
self.hass, self.signal_reboot_requested, async_reboot_requested
)
)
@callback
def async_teardown() -> None:
"""Tear the coordinator down appropriately."""
for unsub in self._signal_handler_unsubs:
unsub()
self.config_entry.async_on_unload(async_teardown)

View File

@@ -223,12 +223,24 @@ HARDWARE_INTEGRATIONS = {
async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict:
"""Return add-on info.
The add-on must be installed.
The caller of the function should handle HassioAPIError.
"""
hassio = hass.data[DOMAIN]
return await hassio.get_addon_info(slug)
@api_data
async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict:
"""Return add-on store info.
The caller of the function should handle HassioAPIError.
"""
hassio: HassIO = hass.data[DOMAIN]
command = f"/store/addons/{slug}"
return await hassio.send_command(command, method="get")
@bind_hass
async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict:
"""Update Supervisor diagnostics toggle.

View File

@@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Home Connect component."""
hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
),
)
_LOGGER.warning(
"Configuration of Home Connect integration in YAML is deprecated and "
"will be removed in a future release; Your existing OAuth "
"Application Credentials have been imported into the UI "
"automatically and can be safely removed from your "
"configuration.yaml file"
)
if DOMAIN in config:
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
),
)
_LOGGER.warning(
"Configuration of Home Connect integration in YAML is deprecated and "
"will be removed in a future release; Your existing OAuth "
"Application Credentials have been imported into the UI "
"automatically and can be safely removed from your "
"configuration.yaml file"
)
async def _async_service_program(call, method):
"""Execute calls to services taking a program."""

View File

@@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity
from homeassistant.const import __version__
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.yaml import parse_yaml
@@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
coordinator = AlertUpdateCoordinator(hass)
coordinator.async_add_listener(async_schedule_update_alerts)
await coordinator.async_refresh()
async def initial_refresh(hass: HomeAssistant) -> None:
await coordinator.async_refresh()
async_at_start(hass, initial_refresh)
return True

View File

@@ -1,6 +1,6 @@
{
"domain": "homeassistant_alerts",
"name": "Home Assistant alerts",
"name": "Home Assistant Alerts",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts",
"codeowners": ["@home-assistant/core"],

View File

@@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType
from .config_flow import normalize_hkid
from .connection import HKDevice, valid_serial_number
from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS
from .storage import async_get_entity_storage
from .storage import EntityMapStorage, async_get_entity_storage
from .utils import async_get_controller, folded_name
_LOGGER = logging.getLogger(__name__)
@@ -269,7 +269,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hkid = entry.data["AccessoryPairingID"]
if hkid in hass.data[KNOWN_DEVICES]:
connection = hass.data[KNOWN_DEVICES][hkid]
connection: HKDevice = hass.data[KNOWN_DEVICES][hkid]
await connection.async_unload()
return True
@@ -280,7 +280,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
hkid = entry.data["AccessoryPairingID"]
# Remove cached type data from .storage/homekit_controller-entity-map
hass.data[ENTITY_MAP].async_delete_map(hkid)
entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP]
entity_map_storage.async_delete_map(hkid)
controller = await async_get_controller(hass)

View File

@@ -1,14 +1,17 @@
"""Config flow to configure homekit_controller."""
from __future__ import annotations
from collections.abc import Awaitable
import logging
import re
from typing import TYPE_CHECKING, Any, cast
import aiohomekit
from aiohomekit import Controller, const as aiohomekit_const
from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing
from aiohomekit.controller.abstract import (
AbstractDiscovery,
AbstractPairing,
FinishPairing,
)
from aiohomekit.exceptions import AuthenticationError
from aiohomekit.model.categories import Categories
from aiohomekit.model.status_flags import StatusFlags
@@ -17,7 +20,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers import device_registry as dr
@@ -78,7 +81,9 @@ def formatted_category(category: Categories) -> str:
@callback
def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None:
def find_existing_host(
hass: HomeAssistant, serial: str
) -> config_entries.ConfigEntry | None:
"""Return a set of the configured hosts."""
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get("AccessoryPairingID") == serial:
@@ -115,15 +120,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.category: Categories | None = None
self.devices: dict[str, AbstractDiscovery] = {}
self.controller: Controller | None = None
self.finish_pairing: Awaitable[AbstractPairing] | None = None
self.finish_pairing: FinishPairing | None = None
async def _async_setup_controller(self):
async def _async_setup_controller(self) -> None:
"""Create the controller."""
self.controller = await async_get_controller(self.hass)
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow start."""
errors = {}
errors: dict[str, str] = {}
if user_input is not None:
key = user_input["device"]
@@ -142,6 +149,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self.controller is None:
await self._async_setup_controller()
assert self.controller
self.devices = {}
async for discovery in self.controller.async_discover():
@@ -167,7 +176,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
),
)
async def async_step_unignore(self, user_input):
async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult:
"""Rediscover a previously ignored discover."""
unique_id = user_input["unique_id"]
await self.async_set_unique_id(unique_id)
@@ -175,19 +184,21 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self.controller is None:
await self._async_setup_controller()
assert self.controller
try:
discovery = await self.controller.async_find(unique_id)
except aiohomekit.AccessoryNotFoundError:
return self.async_abort(reason="accessory_not_found_error")
self.name = discovery.description.name
self.model = discovery.description.model
self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME)
self.category = discovery.description.category
self.hkid = discovery.description.id
return self._async_step_pair_show_form()
async def _hkid_is_homekit(self, hkid):
async def _hkid_is_homekit(self, hkid: str) -> bool:
"""Determine if the device is a homekit bridge or accessory."""
dev_reg = dr.async_get(self.hass)
device = dev_reg.async_get_device(
@@ -410,7 +421,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self._async_step_pair_show_form()
async def async_step_pair(self, pair_info=None):
async def async_step_pair(
self, pair_info: dict[str, Any] | None = None
) -> FlowResult:
"""Pair with a new HomeKit accessory."""
# If async_step_pair is called with no pairing code then we do the M1
# phase of pairing. If this is successful the device enters pairing
@@ -428,11 +441,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# callable. We call the callable with the pin that the user has typed
# in.
# Should never call this step without setting self.hkid
assert self.hkid
errors = {}
if self.controller is None:
await self._async_setup_controller()
assert self.controller
if pair_info and self.finish_pairing:
self.context["pairing"] = True
code = pair_info["pairing_code"]
@@ -507,21 +525,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self._async_step_pair_show_form(errors)
async def async_step_busy_error(self, user_input=None):
async def async_step_busy_error(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Retry pairing after the accessory is busy."""
if user_input is not None:
return await self.async_step_pair()
return self.async_show_form(step_id="busy_error")
async def async_step_max_tries_error(self, user_input=None):
async def async_step_max_tries_error(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Retry pairing after the accessory has reached max tries."""
if user_input is not None:
return await self.async_step_pair()
return self.async_show_form(step_id="max_tries_error")
async def async_step_protocol_error(self, user_input=None):
async def async_step_protocol_error(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Retry pairing after the accessory has a protocol error."""
if user_input is not None:
return await self.async_step_pair()
@@ -529,7 +553,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="protocol_error")
@callback
def _async_step_pair_show_form(self, errors=None):
def _async_step_pair_show_form(
self, errors: dict[str, str] | None = None
) -> FlowResult:
assert self.category
placeholders = self.context["title_placeholders"] = {
"name": self.name,
"category": formatted_category(self.category),
@@ -569,7 +597,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
entity_storage = await async_get_entity_storage(self.hass)
assert self.unique_id is not None
entity_storage.async_create_or_update_map(
self.unique_id,
pairing.id,
accessories_state.config_num,
accessories_state.accessories.serialize(),
)

View File

@@ -107,9 +107,9 @@ class HomeKitLight(HomeKitEntity, LightEntity):
return ColorMode.ONOFF
@property
def supported_color_modes(self) -> set[ColorMode | str] | None:
def supported_color_modes(self) -> set[ColorMode]:
"""Flag supported color modes."""
color_modes: set[ColorMode | str] = set()
color_modes: set[ColorMode] = set()
if self.service.has(CharacteristicsTypes.HUE) or self.service.has(
CharacteristicsTypes.SATURATION

View File

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

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
from typing import Any, TypedDict
from homeassistant.core import HomeAssistant, callback
@@ -12,6 +13,7 @@ from .const import DOMAIN, ENTITY_MAP
ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map"
ENTITY_MAP_STORAGE_VERSION = 1
ENTITY_MAP_SAVE_DELAY = 10
_LOGGER = logging.getLogger(__name__)
class Pairing(TypedDict):
@@ -68,6 +70,7 @@ class EntityMapStorage:
self, homekit_id: str, config_num: int, accessories: list[Any]
) -> Pairing:
"""Create a new pairing cache."""
_LOGGER.debug("Creating or updating entity map for %s", homekit_id)
data = Pairing(config_num=config_num, accessories=accessories)
self.storage_data[homekit_id] = data
self._async_schedule_save()
@@ -76,11 +79,17 @@ class EntityMapStorage:
@callback
def async_delete_map(self, homekit_id: str) -> None:
"""Delete pairing cache."""
if homekit_id not in self.storage_data:
return
self.storage_data.pop(homekit_id)
self._async_schedule_save()
removed_one = False
# Previously there was a bug where a lowercase homekit_id was stored
# in the storage. We need to account for that.
for hkid in (homekit_id, homekit_id.lower()):
if hkid not in self.storage_data:
continue
_LOGGER.debug("Deleting entity map for %s", hkid)
self.storage_data.pop(hkid)
removed_one = True
if removed_one:
self._async_schedule_save()
@callback
def _async_schedule_save(self) -> None:

View File

@@ -223,6 +223,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
== SensorDeviceClass.POWER
):
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_icon = None
update_state = True
if update_state:

View File

@@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity):
self._port = port
self._attr_unique_id = unique_id
self._name = None
self._volume = 0
self._volume_min = 0
self._volume_max = 0
@@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity):
elif response["msg"] == "SPK_LIST_VIEW_INFO":
if "i_vol" in data:
self._volume = data["i_vol"]
if "s_user_name" in data:
self._name = data["s_user_name"]
if "i_vol_min" in data:
self._volume_min = data["i_vol_min"]
if "i_vol_max" in data:

View File

@@ -11,8 +11,7 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"existing_instance_updated": "Updated existing configuration.",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -1,8 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Service is already configured",
"existing_instance_updated": "Updated existing configuration."
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect"

View File

@@ -91,9 +91,11 @@ class Life360Data:
members: dict[str, Life360Member] = field(init=False, default_factory=dict)
class Life360DataUpdateCoordinator(DataUpdateCoordinator):
class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
"""Life360 data update coordinator."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize data update coordinator."""
super().__init__(

View File

@@ -11,10 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_BATTERY_CHARGING
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_ADDRESS,
@@ -31,6 +28,7 @@ from .const import (
LOGGER,
SHOW_DRIVING,
)
from .coordinator import Life360DataUpdateCoordinator, Life360Member
_LOC_ATTRS = (
"address",
@@ -95,23 +93,27 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(process_data))
class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
class Life360DeviceTracker(
CoordinatorEntity[Life360DataUpdateCoordinator], TrackerEntity
):
"""Life360 Device Tracker."""
_attr_attribution = ATTRIBUTION
_attr_unique_id: str
def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None:
def __init__(
self, coordinator: Life360DataUpdateCoordinator, member_id: str
) -> None:
"""Initialize Life360 Entity."""
super().__init__(coordinator)
self._attr_unique_id = member_id
self._data = coordinator.data.members[self.unique_id]
self._data: Life360Member | None = coordinator.data.members[member_id]
self._prev_data = self._data
self._attr_name = self._data.name
self._attr_entity_picture = self._data.entity_picture
self._prev_data = self._data
@property
def _options(self) -> Mapping[str, Any]:
"""Shortcut to config entry options."""
@@ -120,16 +122,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
# Get a shortcut to this member's data. Can't guarantee it's the same dict every
# update, or that there is even data for this member every update, so need to
# update shortcut each time.
self._data = self.coordinator.data.members.get(self.unique_id)
# Get a shortcut to this Member's data. This needs to be updated each time since
# coordinator provides a new Life360Member object each time, and it's possible
# that there is no data for this Member on some updates.
if self.available:
# If nothing important has changed, then skip the update altogether.
if self._data == self._prev_data:
return
self._data = self.coordinator.data.members.get(self._attr_unique_id)
else:
self._data = None
if self._data:
# Check if we should effectively throw out new location data.
last_seen = self._data.last_seen
prev_seen = self._prev_data.last_seen
@@ -168,27 +169,21 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
"""Return True if state updates should be forced."""
return False
@property
def available(self) -> bool:
"""Return if entity is available."""
# Guard against member not being in last update for some reason.
return super().available and self._data is not None
@property
def entity_picture(self) -> str | None:
"""Return the entity picture to use in the frontend, if any."""
if self.available:
if self._data:
self._attr_entity_picture = self._data.entity_picture
return super().entity_picture
# All of the following will only be called if self.available is True.
@property
def battery_level(self) -> int | None:
"""Return the battery level of the device.
Percentage from 0-100.
"""
if not self._data:
return None
return self._data.battery_level
@property
@@ -202,11 +197,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
Value in meters.
"""
if not self._data:
return 0
return self._data.gps_accuracy
@property
def driving(self) -> bool:
"""Return if driving."""
if not self._data:
return False
if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None:
if self._data.speed >= driving_speed:
return True
@@ -222,23 +221,38 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""
if not self._data:
return None
return self._data.latitude
@property
def longitude(self) -> float | None:
"""Return longitude value of the device."""
if not self._data:
return None
return self._data.longitude
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return entity specific state attributes."""
attrs = {}
attrs[ATTR_ADDRESS] = self._data.address
attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since
attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging
attrs[ATTR_DRIVING] = self.driving
attrs[ATTR_LAST_SEEN] = self._data.last_seen
attrs[ATTR_PLACE] = self._data.place
attrs[ATTR_SPEED] = self._data.speed
attrs[ATTR_WIFI_ON] = self._data.wifi_on
return attrs
if not self._data:
return {
ATTR_ADDRESS: None,
ATTR_AT_LOC_SINCE: None,
ATTR_BATTERY_CHARGING: None,
ATTR_DRIVING: None,
ATTR_LAST_SEEN: None,
ATTR_PLACE: None,
ATTR_SPEED: None,
ATTR_WIFI_ON: None,
}
return {
ATTR_ADDRESS: self._data.address,
ATTR_AT_LOC_SINCE: self._data.at_loc_since,
ATTR_BATTERY_CHARGING: self._data.battery_charging,
ATTR_DRIVING: self.driving,
ATTR_LAST_SEEN: self._data.last_seen,
ATTR_PLACE: self._data.place,
ATTR_SPEED: self._data.speed,
ATTR_WIFI_ON: self._data.wifi_on,
}

View File

@@ -1,6 +1,6 @@
{
"domain": "luci",
"name": "OpenWRT (luci)",
"name": "OpenWrt (luci)",
"documentation": "https://www.home-assistant.io/integrations/luci",
"requirements": ["openwrt-luci-rpc==1.1.11"],
"codeowners": ["@mzdrale"],

View File

@@ -43,14 +43,18 @@ class MeaterSensorEntityDescription(
def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
"""Convert elapsed time to timestamp."""
if not probe.cook:
if not probe.cook or not hasattr(probe.cook, "time_elapsed"):
return None
return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed)
def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
"""Convert remaining time to timestamp."""
if not probe.cook or probe.cook.time_remaining < 0:
if (
not probe.cook
or not hasattr(probe.cook, "time_remaining")
or probe.cook.time_remaining < 0
):
return None
return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining)
@@ -99,7 +103,9 @@ SENSOR_TYPES = (
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
available=lambda probe: probe is not None and probe.cook is not None,
value=lambda probe: probe.cook.target_temperature if probe.cook else None,
value=lambda probe: probe.cook.target_temperature
if probe.cook and hasattr(probe.cook, "target_temperature")
else None,
),
# Peak temperature
MeaterSensorEntityDescription(
@@ -109,7 +115,9 @@ SENSOR_TYPES = (
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
available=lambda probe: probe is not None and probe.cook is not None,
value=lambda probe: probe.cook.peak_temperature if probe.cook else None,
value=lambda probe: probe.cook.peak_temperature
if probe.cook and hasattr(probe.cook, "peak_temperature")
else None,
),
# Remaining time in seconds. When unknown/calculating default is used. Default: -1
# Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time.

View File

@@ -116,7 +116,7 @@ class MikrotikDataUpdateCoordinatorTracker(
return self.device.mac
@property
def ip_address(self) -> str:
def ip_address(self) -> str | None:
"""Return the mac address of the client."""
return self.device.ip_address

View File

@@ -60,9 +60,9 @@ class Device:
return self._params.get("host-name", self.mac)
@property
def ip_address(self) -> str:
def ip_address(self) -> str | None:
"""Return device primary ip address."""
return self._params["address"]
return self._params.get("address")
@property
def mac(self) -> str:

View File

@@ -2,7 +2,7 @@
"issues": {
"replaced": {
"title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced",
"description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
"description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}

View File

@@ -1,7 +1,7 @@
{
"issues": {
"replaced": {
"description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced"
}
}

View File

@@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import (
from homeassistant.components.camera import Image, img_util
from homeassistant.components.http.const import KEY_HASS_USER
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.repairs import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_BINARY_SENSORS,
@@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, unique_id=entry.data[CONF_PROJECT_ID]
)
async_delete_issue(hass, DOMAIN, "removed_app_auth")
subscriber = await api.new_subscriber(hass, entry)
if not subscriber:
return False
@@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN:
# App Auth credentials have been deprecated and must be re-created
# by the user in the config flow
async_create_issue(
hass,
DOMAIN,
"removed_app_auth",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="removed_app_auth",
translation_placeholders={
"more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation",
"documentation_url": "https://www.home-assistant.io/integrations/nest/",
},
)
raise ConfigEntryAuthFailed(
"Google has deprecated App Auth credentials, and the integration "
"must be reconfigured in the UI to restore access to Nest Devices."
@@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
WEB_AUTH_DOMAIN,
)
_LOGGER.warning(
"Configuration of Nest integration in YAML is deprecated and "
"will be removed in a future release; Your existing configuration "
"(including OAuth Application Credentials) has been imported into "
"the UI automatically and can be safely removed from your "
"configuration.yaml file"
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2022.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)

View File

@@ -2,7 +2,7 @@
"domain": "nest",
"name": "Nest",
"config_flow": true,
"dependencies": ["ffmpeg", "http", "application_credentials"],
"dependencies": ["ffmpeg", "http", "application_credentials", "repairs"],
"after_dependencies": ["media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"],

View File

@@ -88,5 +88,15 @@
"camera_sound": "Sound detected",
"doorbell_chime": "Doorbell pressed"
}
},
"issues": {
"deprecated_yaml": {
"title": "The Nest YAML configuration is being removed",
"description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"removed_app_auth": {
"title": "Nest Authentication Credentials must be updated",
"description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information."
}
}
}

View File

@@ -10,7 +10,6 @@
"missing_configuration": "The component is not configured. Please follow the documentation.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
"reauth_successful": "Re-authentication was successful",
"single_instance_allowed": "Already configured. Only a single configuration possible.",
"unknown_authorize_url_generation": "Unknown error generating an authorize URL."
},
"create_entry": {
@@ -26,13 +25,6 @@
"wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)"
},
"step": {
"auth": {
"data": {
"code": "Access Token"
},
"description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.",
"title": "Link Google Account"
},
"auth_upgrade": {
"description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.",
"title": "Nest: App Auth Deprecation"
@@ -96,5 +88,15 @@
"camera_sound": "Sound detected",
"doorbell_chime": "Doorbell pressed"
}
},
"issues": {
"deprecated_yaml": {
"description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
"title": "The Nest YAML configuration is being removed"
},
"removed_app_auth": {
"description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.",
"title": "Nest Authentication Credentials must be updated"
}
}
}

View File

@@ -3,7 +3,7 @@
"name": "NextDNS",
"documentation": "https://www.home-assistant.io/integrations/nextdns",
"codeowners": ["@bieniu"],
"requirements": ["nextdns==1.0.1"],
"requirements": ["nextdns==1.0.2"],
"config_flow": true,
"iot_class": "cloud_polling",
"loggers": ["nextdns"]

View File

@@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries",
name="TCP queries",
native_unit_of_measurement="queries",
state_class=SensorStateClass.TOTAL,
value=lambda data: data.tcp_queries,
@@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries Ratio",
name="TCP queries ratio",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.tcp_queries_ratio,

View File

@@ -1,9 +1,11 @@
"""Support for OpenTherm Gateway devices."""
import asyncio
from datetime import date, datetime
import logging
import pyotgw
import pyotgw.vars as gw_vars
from serial import SerialException
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -23,6 +25,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
@@ -37,6 +40,7 @@ from .const import (
CONF_PRECISION,
CONF_READ_PRECISION,
CONF_SET_PRECISION,
CONNECTION_TIMEOUT,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
DOMAIN,
@@ -107,8 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
config_entry.add_update_listener(options_updated)
# Schedule directly on the loop to avoid blocking HA startup.
hass.loop.create_task(gateway.connect_and_subscribe())
try:
await asyncio.wait_for(
gateway.connect_and_subscribe(),
timeout=CONNECTION_TIMEOUT,
)
except (asyncio.TimeoutError, ConnectionError, SerialException) as ex:
raise ConfigEntryNotReady(
f"Could not connect to gateway at {gateway.device_path}: {ex}"
) from ex
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@@ -428,6 +439,9 @@ class OpenThermGatewayDevice:
async def connect_and_subscribe(self):
"""Connect to serial device and subscribe report handler."""
self.status = await self.gateway.connect(self.device_path)
if not self.status:
await self.cleanup()
raise ConnectionError
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
self.gw_version = version_string[18:] if version_string else None
_LOGGER.debug(

View File

@@ -26,6 +26,7 @@ from .const import (
CONF_READ_PRECISION,
CONF_SET_PRECISION,
CONF_TEMPORARY_OVRD_MODE,
CONNECTION_TIMEOUT,
)
@@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
otgw = pyotgw.OpenThermGateway()
status = await otgw.connect(device)
await otgw.disconnect()
if not status:
raise ConnectionError
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
try:
res = await asyncio.wait_for(test_connection(), timeout=10)
except (asyncio.TimeoutError, SerialException):
await asyncio.wait_for(
test_connection(),
timeout=CONNECTION_TIMEOUT,
)
except asyncio.TimeoutError:
return self._show_form({"base": "timeout_connect"})
except (ConnectionError, SerialException):
return self._show_form({"base": "cannot_connect"})
if res:
return self._create_entry(gw_id, name, device)
return self._create_entry(gw_id, name, device)
return self._show_form()

View File

@@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision"
CONF_SET_PRECISION = "set_precision"
CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode"
CONNECTION_TIMEOUT = 10
DATA_GATEWAYS = "gateways"
DATA_OPENTHERM_GW = "opentherm_gw"

View File

@@ -2,7 +2,7 @@
"domain": "opentherm_gw",
"name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": ["pyotgw==2.0.1"],
"requirements": ["pyotgw==2.0.2"],
"codeowners": ["@mvn23"],
"config_flow": true,
"iot_class": "local_push",

View File

@@ -12,7 +12,8 @@
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"id_exists": "Gateway id already exists",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
}
},
"options": {

View File

@@ -1,6 +1,6 @@
{
"domain": "overkiz",
"name": "Overkiz (by Somfy)",
"name": "Overkiz",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/overkiz",
"requirements": ["pyoverkiz==1.4.2"],

View File

@@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Philips TV from a config entry."""
system: SystemType | None = entry.data.get(CONF_SYSTEM)
tvapi = PhilipsTV(
entry.data[CONF_HOST],
entry.data[CONF_API_VERSION],
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
system=system,
)
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options)
await coordinator.async_refresh()
if (actual_system := tvapi.system) and actual_system != system:
data = {**entry.data, CONF_SYSTEM: actual_system}
hass.config_entries.async_update_entry(entry, data=data)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator

View File

@@ -3,7 +3,7 @@
"name": "Risco",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/risco",
"requirements": ["pyrisco==0.5.0"],
"requirements": ["pyrisco==0.5.2"],
"codeowners": ["@OnFreund"],
"quality_scale": "platinum",
"iot_class": "cloud_polling",

View File

@@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +36,7 @@ class RaspberryChargerBinarySensor(BinarySensorEntity):
"""Binary sensor representing the rpi power status."""
_attr_device_class = BinarySensorDeviceClass.PROBLEM
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:raspberry-pi"
_attr_name = "RPi Power status"
_attr_unique_id = "rpi_power" # only one sensor possible

View File

@@ -81,17 +81,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={CONF_URL: self._oauth_values.auth_url},
)
auth_code = user_input[CONF_AUTH_CODE]
if auth_code.startswith("="):
# Sometimes, users may include the "=" from the URL query param; in that
# case, strip it off and proceed:
LOGGER.debug('Stripping "=" from the start of the authorization code')
auth_code = auth_code[1:]
if len(auth_code) != 45:
# SimpliSafe authorization codes are 45 characters in length; if the user
# provides something different, stop them here:
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_SCHEMA,
errors={CONF_AUTH_CODE: "invalid_auth_code_length"},
description_placeholders={CONF_URL: self._oauth_values.auth_url},
)
errors = {}
session = aiohttp_client.async_get_clientsession(self.hass)
try:
simplisafe = await API.async_from_auth(
user_input[CONF_AUTH_CODE],
auth_code,
self._oauth_values.code_verifier,
session=session,
)
except InvalidCredentialsError:
errors = {"base": "invalid_auth"}
errors = {CONF_AUTH_CODE: "invalid_auth"}
except SimplipyError as err:
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
errors = {"base": "unknown"}

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.",
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.",
"data": {
"auth_code": "Authorization Code"
}
@@ -11,6 +11,7 @@
"error": {
"identifier_exists": "Account already registered",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {

View File

@@ -2,39 +2,21 @@
"config": {
"abort": {
"already_configured": "This SimpliSafe account is already in use.",
"email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.",
"reauth_successful": "Re-authentication was successful",
"wrong_account": "The user credentials provided do not match this SimpliSafe account."
},
"error": {
"identifier_exists": "Account already registered",
"invalid_auth": "Invalid authentication",
"invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length",
"unknown": "Unexpected error"
},
"progress": {
"email_2fa": "Check your email for a verification link from Simplisafe."
},
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "Please re-enter the password for {username}.",
"title": "Reauthenticate Integration"
},
"sms_2fa": {
"data": {
"code": "Code"
},
"description": "Input the two-factor authentication code sent to you via SMS."
},
"user": {
"data": {
"auth_code": "Authorization Code",
"password": "Password",
"username": "Username"
"auth_code": "Authorization Code"
},
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL."
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL."
}
}
},

View File

@@ -72,7 +72,7 @@ FRIENDLY_NAMES = {
ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume",
ATTR_NIGHT_SOUND: "Night sound",
ATTR_SPEECH_ENHANCEMENT: "Speech enhancement",
ATTR_STATUS_LIGHT: "Status Light",
ATTR_STATUS_LIGHT: "Status light",
ATTR_SUB_ENABLED: "Subwoofer enabled",
ATTR_SURROUND_ENABLED: "Surround enabled",
ATTR_TOUCH_CONTROLS: "Touch controls",

View File

@@ -22,6 +22,7 @@ from .const import (
ATTR_CONTACT,
ATTR_CURTAIN,
ATTR_HYGROMETER,
ATTR_PLUG,
CONF_RETRY_COUNT,
DEFAULT_RETRY_COUNT,
DOMAIN,
@@ -30,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator
PLATFORMS_BY_TYPE = {
ATTR_BOT: [Platform.SWITCH, Platform.SENSOR],
ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR],
ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR],
ATTR_HYGROMETER: [Platform.SENSOR],
ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR],
@@ -37,6 +39,7 @@ PLATFORMS_BY_TYPE = {
CLASS_BY_DEVICE = {
ATTR_CURTAIN: switchbot.SwitchbotCurtain,
ATTR_BOT: switchbot.Switchbot,
ATTR_PLUG: switchbot.SwitchbotPlugMini,
}
_LOGGER = logging.getLogger(__name__)

View File

@@ -7,12 +7,14 @@ ATTR_BOT = "bot"
ATTR_CURTAIN = "curtain"
ATTR_HYGROMETER = "hygrometer"
ATTR_CONTACT = "contact"
ATTR_PLUG = "plug"
DEFAULT_NAME = "Switchbot"
SUPPORTED_MODEL_TYPES = {
"WoHand": ATTR_BOT,
"WoCurtain": ATTR_CURTAIN,
"WoSensorTH": ATTR_HYGROMETER,
"WoContact": ATTR_CONTACT,
"WoPlug": ATTR_PLUG,
}
# Config Defaults

View File

@@ -80,9 +80,12 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes:
return
self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION]
self._last_run_success = last_state.attributes["last_run_success"]
self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20
self._attr_current_cover_position = last_state.attributes.get(
ATTR_CURRENT_POSITION
)
self._last_run_success = last_state.attributes.get("last_run_success")
if self._attr_current_cover_position is not None:
self._attr_is_closed = self._attr_current_cover_position <= 20
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the curtain."""

View File

@@ -2,10 +2,16 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.15.2"],
"requirements": ["PySwitchbot==0.18.4"],
"config_flow": true,
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"],
"codeowners": [
"@bdraco",
"@danielhiversen",
"@RenierM26",
"@murtas",
"@Eloston"
],
"bluetooth": [
{
"service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb"

View File

@@ -33,6 +33,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"wifi_rssi": SensorEntityDescription(
key="wifi_rssi",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"battery": SensorEntityDescription(
key="battery",
native_unit_of_measurement=PERCENTAGE,
@@ -98,7 +105,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
super().__init__(coordinator, unique_id, address, name=switchbot_name)
self._sensor = sensor
self._attr_unique_id = f"{unique_id}-{sensor}"
self._attr_name = f"{switchbot_name} {sensor.title()}"
self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}"
self.entity_description = SENSOR_TYPES[sensor]
@property

View File

@@ -33,7 +33,7 @@ async def async_setup_entry(
assert unique_id is not None
async_add_entities(
[
SwitchBotBotEntity(
SwitchBotSwitch(
coordinator,
unique_id,
entry.data[CONF_ADDRESS],
@@ -44,8 +44,8 @@ async def async_setup_entry(
)
class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity):
"""Representation of a Switchbot."""
class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity):
"""Representation of a Switchbot switch."""
_attr_device_class = SwitchDeviceClass.SWITCH
@@ -69,7 +69,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity):
if not (last_state := await self.async_get_last_state()):
return
self._attr_is_on = last_state.state == STATE_ON
self._last_run_success = last_state.attributes["last_run_success"]
self._last_run_success = last_state.attributes.get("last_run_success")
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn device on."""

View File

@@ -1,5 +1,7 @@
"""Offer webhook triggered automation rules."""
from functools import partial
from __future__ import annotations
from dataclasses import dataclass
from aiohttp import hdrs
import voluptuous as vol
@@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from . import async_register, async_unregister
from . import DOMAIN, async_register, async_unregister
# mypy: allow-untyped-defs
@@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
}
)
WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers"
async def _handle_webhook(job, trigger_data, hass, webhook_id, request):
@dataclass
class TriggerInstance:
"""Attached trigger settings."""
automation_info: AutomationTriggerInfo
job: HassJob
async def _handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook."""
result = {"platform": "webhook", "webhook_id": webhook_id}
base_result = {"platform": "webhook", "webhook_id": webhook_id}
if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""):
result["json"] = await request.json()
base_result["json"] = await request.json()
else:
result["data"] = await request.post()
base_result["data"] = await request.post()
result["query"] = request.query
result["description"] = "webhook"
result.update(**trigger_data)
hass.async_run_hass_job(job, {"trigger": result})
base_result["query"] = request.query
base_result["description"] = "webhook"
triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault(
WEBHOOK_TRIGGERS, {}
)
for trigger in triggers[webhook_id]:
result = {**base_result, **trigger.automation_info["trigger_data"]}
hass.async_run_hass_job(trigger.job, {"trigger": result})
async def async_attach_trigger(
@@ -49,20 +66,32 @@ async def async_attach_trigger(
automation_info: AutomationTriggerInfo,
) -> CALLBACK_TYPE:
"""Trigger based on incoming webhooks."""
trigger_data = automation_info["trigger_data"]
webhook_id: str = config[CONF_WEBHOOK_ID]
job = HassJob(action)
async_register(
hass,
automation_info["domain"],
automation_info["name"],
webhook_id,
partial(_handle_webhook, job, trigger_data),
triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault(
WEBHOOK_TRIGGERS, {}
)
if webhook_id not in triggers:
async_register(
hass,
automation_info["domain"],
automation_info["name"],
webhook_id,
_handle_webhook,
)
triggers[webhook_id] = []
trigger_instance = TriggerInstance(automation_info, job)
triggers[webhook_id].append(trigger_instance)
@callback
def unregister():
"""Unregister webhook."""
async_unregister(hass, webhook_id)
triggers[webhook_id].remove(trigger_instance)
if not triggers[webhook_id]:
async_unregister(hass, webhook_id)
triggers.pop(webhook_id)
return unregister

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import dataclasses
from typing import Any
@@ -12,7 +13,7 @@ from xiaomi_ble.parser import EncryptionScheme
from homeassistant.components import onboarding
from homeassistant.components.bluetooth import (
BluetoothScanningMode,
BluetoothServiceInfoBleak,
BluetoothServiceInfo,
async_discovered_service_info,
async_process_advertisements,
)
@@ -31,11 +32,11 @@ class Discovery:
"""A discovered bluetooth device."""
title: str
discovery_info: BluetoothServiceInfoBleak
discovery_info: BluetoothServiceInfo
device: DeviceData
def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str:
def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str:
return device.title or device.get_device_name() or discovery_info.name
@@ -46,19 +47,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovery_info: BluetoothServiceInfo | None = None
self._discovered_device: DeviceData | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def _async_wait_for_full_advertisement(
self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData
) -> BluetoothServiceInfoBleak:
self, discovery_info: BluetoothServiceInfo, device: DeviceData
) -> BluetoothServiceInfo:
"""Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one."""
if not device.pending:
return discovery_info
def _process_more_advertisements(
service_info: BluetoothServiceInfoBleak,
service_info: BluetoothServiceInfo,
) -> bool:
device.update(service_info)
return not device.pending
@@ -72,7 +73,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
)
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
self, discovery_info: BluetoothServiceInfo
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
@@ -81,20 +82,21 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
if not device.supported(discovery_info):
return self.async_abort(reason="not_supported")
title = _title(discovery_info, device)
self.context["title_placeholders"] = {"name": title}
self._discovered_device = device
# Wait until we have received enough information about this device to detect its encryption type
try:
discovery_info = await self._async_wait_for_full_advertisement(
self._discovery_info = await self._async_wait_for_full_advertisement(
discovery_info, device
)
except asyncio.TimeoutError:
# If we don't see a valid packet within the timeout then this device is not supported.
return self.async_abort(reason="not_supported")
self._discovery_info = discovery_info
self._discovered_device = device
title = _title(discovery_info, device)
self.context["title_placeholders"] = {"name": title}
# This device might have a really long advertising interval
# So create a config entry for it, and if we discover it has encryption later
# We can do a reauth
return await self.async_step_confirm_slow()
if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY:
return await self.async_step_get_encryption_key_legacy()
@@ -107,6 +109,8 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Enter a legacy bindkey for a v2/v3 MiBeacon device."""
assert self._discovery_info
assert self._discovered_device
errors = {}
if user_input is not None:
@@ -115,18 +119,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
if len(bindkey) != 24:
errors["bindkey"] = "expected_24_characters"
else:
device = DeviceData(bindkey=bytes.fromhex(bindkey))
self._discovered_device.bindkey = bytes.fromhex(bindkey)
# If we got this far we already know supported will
# return true so we don't bother checking that again
# We just want to retry the decryption
device.supported(self._discovery_info)
self._discovered_device.supported(self._discovery_info)
if device.bindkey_verified:
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={"bindkey": bindkey},
)
if self._discovered_device.bindkey_verified:
return self._async_get_or_create_entry(bindkey)
errors["bindkey"] = "decryption_failed"
@@ -142,6 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Enter a bindkey for a v4/v5 MiBeacon device."""
assert self._discovery_info
assert self._discovered_device
errors = {}
@@ -151,18 +153,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
if len(bindkey) != 32:
errors["bindkey"] = "expected_32_characters"
else:
device = DeviceData(bindkey=bytes.fromhex(bindkey))
self._discovered_device.bindkey = bytes.fromhex(bindkey)
# If we got this far we already know supported will
# return true so we don't bother checking that again
# We just want to retry the decryption
device.supported(self._discovery_info)
self._discovered_device.supported(self._discovery_info)
if device.bindkey_verified:
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={"bindkey": bindkey},
)
if self._discovered_device.bindkey_verified:
return self._async_get_or_create_entry(bindkey)
errors["bindkey"] = "decryption_failed"
@@ -178,10 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Confirm discovery."""
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={},
)
return self._async_get_or_create_entry()
self._set_confirm_only()
return self.async_show_form(
@@ -189,6 +185,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders=self.context["title_placeholders"],
)
async def async_step_confirm_slow(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Ack that device is slow."""
if user_input is not None:
return self._async_get_or_create_entry()
self._set_confirm_only()
return self.async_show_form(
step_id="confirm_slow",
description_placeholders=self.context["title_placeholders"],
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -198,24 +207,28 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(address, raise_on_progress=False)
discovery = self._discovered_devices[address]
self.context["title_placeholders"] = {"name": discovery.title}
# Wait until we have received enough information about this device to detect its encryption type
try:
self._discovery_info = await self._async_wait_for_full_advertisement(
discovery.discovery_info, discovery.device
)
except asyncio.TimeoutError:
# If we don't see a valid packet within the timeout then this device is not supported.
return self.async_abort(reason="not_supported")
# This device might have a really long advertising interval
# So create a config entry for it, and if we discover it has encryption later
# We can do a reauth
return await self.async_step_confirm_slow()
self._discovered_device = discovery.device
if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY:
self.context["title_placeholders"] = {"name": discovery.title}
return await self.async_step_get_encryption_key_legacy()
if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5:
self.context["title_placeholders"] = {"name": discovery.title}
return await self.async_step_get_encryption_key_4_5()
return self.async_create_entry(title=discovery.title, data={})
return self._async_get_or_create_entry()
current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass):
@@ -241,3 +254,46 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}),
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle a flow initialized by a reauth event."""
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry is not None
device: DeviceData = entry_data["device"]
self._discovered_device = device
self._discovery_info = device.last_service_info
if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY:
return await self.async_step_get_encryption_key_legacy()
if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5:
return await self.async_step_get_encryption_key_4_5()
# Otherwise there wasn't actually encryption so abort
return self.async_abort(reason="reauth_successful")
def _async_get_or_create_entry(self, bindkey=None):
data = {}
if bindkey:
data["bindkey"] = bindkey
if entry_id := self.context.get("entry_id"):
entry = self.hass.config_entries.async_get_entry(entry_id)
assert entry is not None
self.hass.config_entries.async_update_entry(entry, data=data)
# Reload the config entry to notify of updated config
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data=data,
)

View File

@@ -8,7 +8,7 @@
"service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb"
}
],
"requirements": ["xiaomi-ble==0.6.2"],
"requirements": ["xiaomi-ble==0.6.4"],
"dependencies": ["bluetooth"],
"codeowners": ["@Jc2k", "@Ernst79"],
"iot_class": "local_push"

View File

@@ -11,8 +11,10 @@ from xiaomi_ble import (
Units,
XiaomiBluetoothDeviceData,
)
from xiaomi_ble.parser import EncryptionScheme
from homeassistant import config_entries
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
@@ -163,6 +165,27 @@ def sensor_update_to_bluetooth_data_update(
)
def process_service_info(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
data: XiaomiBluetoothDeviceData,
service_info: BluetoothServiceInfoBleak,
) -> PassiveBluetoothDataUpdate:
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
update = data.update(service_info)
# If device isn't pending we know it has seen at least one broadcast with a payload
# If that payload was encrypted and the bindkey was not verified then we need to reauth
if (
not data.pending
and data.encryption_scheme != EncryptionScheme.NONE
and not data.bindkey_verified
):
entry.async_start_reauth(hass, data={"device": data})
return sensor_update_to_bluetooth_data_update(update)
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
@@ -177,9 +200,7 @@ async def async_setup_entry(
kwargs["bindkey"] = bytes.fromhex(bindkey)
data = XiaomiBluetoothDeviceData(**kwargs)
processor = PassiveBluetoothDataProcessor(
lambda service_info: sensor_update_to_bluetooth_data_update(
data.update(service_info)
)
lambda service_info: process_service_info(hass, entry, data, service_info)
)
entry.async_on_unload(
processor.async_add_entities_listener(

View File

@@ -11,6 +11,9 @@
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"confirm_slow": {
"description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed."
},
"get_encryption_key_legacy": {
"description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.",
"data": {
@@ -24,13 +27,16 @@
}
}
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"error": {
"decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.",
"expected_24_characters": "Expected a 24 character hexadecimal bindkey.",
"expected_32_characters": "Expected a 32 character hexadecimal bindkey."
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -3,16 +3,22 @@
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"no_devices_found": "No devices found on the network",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.",
"expected_24_characters": "Expected a 24 character hexadecimal bindkey.",
"expected_32_characters": "Expected a 32 character hexadecimal bindkey.",
"no_devices_found": "No devices found on the network"
"expected_32_characters": "Expected a 32 character hexadecimal bindkey."
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "Do you want to setup {name}?"
},
"confirm_slow": {
"description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed."
},
"get_encryption_key_4_5": {
"data": {
"bindkey": "Bindkey"

View File

@@ -35,6 +35,7 @@ from .core.const import (
DOMAIN,
PLATFORMS,
SIGNAL_ADD_ENTITIES,
ZHA_DEVICES_LOADED_EVENT,
RadioType,
)
from .core.discovery import GROUP_PROBE
@@ -75,7 +76,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up ZHA from config."""
hass.data[DATA_ZHA] = {}
hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()}
if DOMAIN in config:
conf = config[DOMAIN]
@@ -109,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
zha_gateway = ZHAGateway(hass, config, config_entry)
await zha_gateway.async_initialize()
hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
@@ -141,6 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
"""Unload ZHA config entry."""
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
await zha_gateway.shutdown()
hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear()
GROUP_PROBE.cleanup()
api.async_unload_api(hass)

View File

@@ -1,7 +1,7 @@
"""Lighting channels module for Zigbee Home Automation."""
from __future__ import annotations
from contextlib import suppress
from functools import cached_property
from zigpy.zcl.clusters import lighting
@@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel):
"color_loop_active": False,
}
@property
def color_capabilities(self) -> int:
"""Return color capabilities of the light."""
with suppress(KeyError):
return self.cluster["color_capabilities"]
if self.cluster.get("color_temperature") is not None:
return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
return self.CAPABILITIES_COLOR_XY
@property
def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities:
@cached_property
def color_capabilities(self) -> lighting.Color.ColorCapabilities:
"""Return ZCL color capabilities of the light."""
color_capabilities = self.cluster.get("color_capabilities")
if color_capabilities is None:
@@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel):
def hs_supported(self) -> bool:
"""Return True if the channel supports hue and saturation."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Hue_and_saturation
in self.zcl_color_capabilities
in self.color_capabilities
)
@property
def enhanced_hue_supported(self) -> bool:
"""Return True if the channel supports enhanced hue and saturation."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue
in self.zcl_color_capabilities
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities
)
@property
def xy_supported(self) -> bool:
"""Return True if the channel supports xy."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.XY_attributes
in self.zcl_color_capabilities
in self.color_capabilities
)
@property
def color_temp_supported(self) -> bool:
"""Return True if the channel supports color temperature."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_temperature
in self.zcl_color_capabilities
)
in self.color_capabilities
) or self.color_temperature is not None
@property
def color_loop_supported(self) -> bool:
"""Return True if the channel supports color loop."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop
in self.zcl_color_capabilities
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
)

View File

@@ -394,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed"
ZHA_GW_MSG_LOG_ENTRY = "log_entry"
ZHA_GW_MSG_LOG_OUTPUT = "log_output"
ZHA_GW_MSG_RAW_INIT = "raw_device_initialized"
ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event"
EFFECT_BLINK = 0x00
EFFECT_BREATHE = 0x01

View File

@@ -142,6 +142,7 @@ class ZHAGateway:
self._log_relay_handler = LogRelayHandler(hass, self)
self.config_entry = config_entry
self._unsubs: list[Callable[[], None]] = []
self.initialized: bool = False
async def async_initialize(self) -> None:
"""Initialize controller and connect radio."""
@@ -183,6 +184,7 @@ class ZHAGateway:
self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee)
self.async_load_devices()
self.async_load_groups()
self.initialized = True
@callback
def async_load_devices(self) -> None:
@@ -217,7 +219,7 @@ class ZHAGateway:
async def async_initialize_devices_and_entities(self) -> None:
"""Initialize devices and load entities."""
_LOGGER.debug("Loading all devices")
_LOGGER.debug("Initializing all devices from Zigpy cache")
await asyncio.gather(
*(dev.async_initialize(from_cache=True) for dev in self.devices.values())
)

View File

@@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import IntegrationError
from homeassistant.helpers import device_registry as dr
from .const import (
@@ -42,6 +43,7 @@ if TYPE_CHECKING:
from .gateway import ZHAGateway
_T = TypeVar("_T")
_LOGGER = logging.getLogger(__name__)
@dataclass
@@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice:
device_registry = dr.async_get(hass)
registry_device = device_registry.async_get(device_id)
if not registry_device:
_LOGGER.error("Device id `%s` not found in registry", device_id)
raise KeyError(f"Device id `{device_id}` not found in registry.")
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee_address = list(list(registry_device.identifiers)[0])[1]
ieee = zigpy.types.EUI64.convert(ieee_address)
if not zha_gateway.initialized:
_LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized")
raise IntegrationError("ZHA is not initialized yet")
try:
ieee_address = list(list(registry_device.identifiers)[0])[1]
ieee = zigpy.types.EUI64.convert(ieee_address)
except (IndexError, ValueError) as ex:
_LOGGER.error(
"Unable to determine device IEEE for device with device id `%s`", device_id
)
raise KeyError(
f"Unable to determine device IEEE for device with device id `{device_id}`."
) from ex
return zha_gateway.devices[ieee]

View File

@@ -13,11 +13,11 @@ from homeassistant.components.device_automation.exceptions import (
from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, IntegrationError
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
from .core.const import ZHA_EVENT
from . import DOMAIN as ZHA_DOMAIN
from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT
from .core.helpers import async_get_zha_device
CONF_SUBTYPE = "subtype"
@@ -35,11 +35,12 @@ async def async_validate_trigger_config(
"""Validate config."""
config = TRIGGER_SCHEMA(config)
if "zha" in hass.config.components:
if ZHA_DOMAIN in hass.config.components:
await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait()
trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
try:
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
except (KeyError, AttributeError) as err:
except (KeyError, AttributeError, IntegrationError) as err:
raise InvalidDeviceAutomationConfig from err
if (
zha_device.device_automation_triggers is None
@@ -100,7 +101,7 @@ async def async_get_triggers(
triggers.append(
{
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_DOMAIN: ZHA_DOMAIN,
CONF_PLATFORM: DEVICE,
CONF_TYPE: trigger,
CONF_SUBTYPE: subtype,

View File

@@ -4,14 +4,14 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.31.2",
"bellows==0.31.3",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.78",
"zigpy-deconz==0.18.0",
"zigpy==0.48.0",
"zigpy==0.49.0",
"zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.0",
"zigpy-zigate==0.9.1",
"zigpy-znp==0.8.1"
],
"usb": [

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