forked from home-assistant/core
Compare commits
102 Commits
2022.8.0b6
...
2022.8.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c894ddeb95 | ||
|
|
d5b6ccab07 | ||
|
|
b9964c73ed | ||
|
|
341bf8eff4 | ||
|
|
34cb79408a | ||
|
|
c0b4eb35fe | ||
|
|
b4cb9a521a | ||
|
|
c1d02388d1 | ||
|
|
8e9313840e | ||
|
|
1f13e332ac | ||
|
|
795ed9fbf6 | ||
|
|
ec4ff824ad | ||
|
|
f4a09455c0 | ||
|
|
bf88448ffd | ||
|
|
bae01f188a | ||
|
|
d142618297 | ||
|
|
d91e9f7f98 | ||
|
|
70541eac61 | ||
|
|
30a5e396d3 | ||
|
|
ae99b53757 | ||
|
|
71a6128c65 | ||
|
|
69ea07f29d | ||
|
|
41faf9092a | ||
|
|
ef6285800f | ||
|
|
bd40d6f332 | ||
|
|
d10e2336e2 | ||
|
|
875de80b41 | ||
|
|
666e938b14 | ||
|
|
972aad0e99 | ||
|
|
872b2f56ac | ||
|
|
e2fae855e7 | ||
|
|
fdde4d540d | ||
|
|
5606b4026f | ||
|
|
84b8029c6a | ||
|
|
294cc3ac6e | ||
|
|
99c20223e5 | ||
|
|
451ab47caa | ||
|
|
738423056e | ||
|
|
a3ea881a01 | ||
|
|
2dcc886b2f | ||
|
|
6e688b2b7f | ||
|
|
0fd38ef9f8 | ||
|
|
56c80cd31a | ||
|
|
303d8b05d1 | ||
|
|
972c05eac8 | ||
|
|
e5088d7e84 | ||
|
|
6c1597ff98 | ||
|
|
edac82487d | ||
|
|
5213148fa8 | ||
|
|
8e3f5ec470 | ||
|
|
af90159e7c | ||
|
|
f01b0a1a62 | ||
|
|
ee2acabcbe | ||
|
|
79b371229d | ||
|
|
0f6b059e3e | ||
|
|
bfb2867e8d | ||
|
|
c9581f6a2e | ||
|
|
e96903fddf | ||
|
|
5026bff426 | ||
|
|
4b63aa7f15 | ||
|
|
1c2dd78e4c | ||
|
|
9cf11cf6ed | ||
|
|
8971a2073e | ||
|
|
bfa64d2e01 | ||
|
|
9c21d56539 | ||
|
|
8bfc352524 | ||
|
|
0e7bf35e4a | ||
|
|
1dd701a89a | ||
|
|
d266b1ced6 | ||
|
|
6727dab330 | ||
|
|
42509056bd | ||
|
|
a370e4f4b0 | ||
|
|
a17e99f714 | ||
|
|
db227a888d | ||
|
|
1808dd3d84 | ||
|
|
31fed328ce | ||
|
|
1a030f118a | ||
|
|
a4049e93d8 | ||
|
|
854ca853dc | ||
|
|
2710e4b5ec | ||
|
|
450af52bac | ||
|
|
60da54558e | ||
|
|
11319defae | ||
|
|
6340da72a5 | ||
|
|
5c9d557b10 | ||
|
|
d2955a48b0 | ||
|
|
d2b98fa285 | ||
|
|
8ef3ca2daf | ||
|
|
80a053a4cd | ||
|
|
81ee24738b | ||
|
|
29f6d7818a | ||
|
|
bc1e371cae | ||
|
|
42a1f6ca20 | ||
|
|
d85129c527 | ||
|
|
ad14b5f3d7 | ||
|
|
51a6899a60 | ||
|
|
d2dc83c4c7 | ||
|
|
d7a418a219 | ||
|
|
a78da6a000 | ||
|
|
690f051a87 | ||
|
|
c22cb13bd0 | ||
|
|
213812f087 |
@@ -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/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .base import AcmedaBase
|
||||
from .const import ACMEDA_HUB_UPDATE, DOMAIN
|
||||
from .helpers import async_add_acmeda_entities
|
||||
from .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -24,7 +25,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Acmeda Rollers from a config entry."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
current: set[int] = set()
|
||||
|
||||
@@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity):
|
||||
"""Stop the roller."""
|
||||
await self.roller.move_stop()
|
||||
|
||||
async def async_set_cover_tilt(self, **kwargs):
|
||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Tilt the roller shutter to a specific position."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.39"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.41"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -36,6 +36,7 @@ from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
split_entity_id,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
ConditionError,
|
||||
@@ -361,7 +362,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
referenced |= condition.async_extract_devices(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_trigger_extract_device(conf))
|
||||
referenced |= set(_trigger_extract_devices(conf))
|
||||
|
||||
self._referenced_devices = referenced
|
||||
return referenced
|
||||
@@ -763,7 +764,7 @@ async def _async_process_if(hass, name, config, p_config):
|
||||
|
||||
|
||||
@callback
|
||||
def _trigger_extract_device(trigger_conf: dict) -> list[str]:
|
||||
def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
|
||||
"""Extract devices from a trigger config."""
|
||||
if trigger_conf[CONF_PLATFORM] == "device":
|
||||
return [trigger_conf[CONF_DEVICE_ID]]
|
||||
@@ -772,6 +773,7 @@ def _trigger_extract_device(trigger_conf: dict) -> list[str]:
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str)
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]]
|
||||
|
||||
@@ -803,6 +805,8 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str)
|
||||
and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID])
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,6 +42,13 @@ 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__)
|
||||
|
||||
|
||||
@@ -50,6 +57,10 @@ 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._async_cancel_scanner_callback()
|
||||
_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._async_cancel_scanner_callback()
|
||||
_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._async_cancel_scanner_callback()
|
||||
_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()
|
||||
self._async_cancel_scanner_callback()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timed out starting Bluetooth after {START_TIMEOUT} seconds"
|
||||
) from ex
|
||||
except (FileNotFoundError, BleakError) as ex:
|
||||
self._cancel_device_detected()
|
||||
except BleakError as ex:
|
||||
self._async_cancel_scanner_callback()
|
||||
_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,31 @@ class BluetoothManager:
|
||||
for device_adv in self.scanner.history.values()
|
||||
]
|
||||
|
||||
async def async_stop(self, event: Event | None = None) -> None:
|
||||
"""Stop bluetooth discovery."""
|
||||
async def _async_hass_stopping(self, event: Event) -> None:
|
||||
"""Stop the Bluetooth integration at shutdown."""
|
||||
self._cancel_stop = None
|
||||
await self.async_stop()
|
||||
|
||||
@hass_callback
|
||||
def _async_cancel_scanner_callback(self) -> None:
|
||||
"""Cancel the scanner callback."""
|
||||
if self._cancel_device_detected:
|
||||
self._cancel_device_detected()
|
||||
self._cancel_device_detected = None
|
||||
|
||||
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
|
||||
self._async_cancel_scanner_callback()
|
||||
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]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.3"],
|
||||
"requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.10.1"],
|
||||
"requirements": ["bimmer_connected==0.10.2"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -56,7 +56,7 @@ class BMWNotificationService(BaseNotificationService):
|
||||
"""Set up the notification service."""
|
||||
self.targets: dict[str, MyBMWVehicle] = targets
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message or POI to the car."""
|
||||
for vehicle in kwargs[ATTR_TARGET]:
|
||||
vehicle = cast(MyBMWVehicle, vehicle)
|
||||
@@ -81,6 +81,6 @@ class BMWNotificationService(BaseNotificationService):
|
||||
}
|
||||
)
|
||||
|
||||
vehicle.remote_services.trigger_send_poi(location_dict)
|
||||
await vehicle.remote_services.trigger_send_poi(location_dict)
|
||||
else:
|
||||
raise ValueError(f"'data.{ATTR_LOCATION}' is required.")
|
||||
|
||||
@@ -15,13 +15,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_MILES,
|
||||
PERCENTAGE,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
from homeassistant.const import LENGTH, PERCENTAGE, VOLUME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes BMW sensor entity."""
|
||||
|
||||
key_class: str | None = None
|
||||
unit_metric: str | None = None
|
||||
unit_imperial: str | None = None
|
||||
unit_type: str | None = None
|
||||
value: Callable = lambda x, y: x
|
||||
|
||||
|
||||
@@ -86,56 +79,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
|
||||
"remaining_battery_percent": BMWSensorEntityDescription(
|
||||
key="remaining_battery_percent",
|
||||
key_class="fuel_and_battery",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
unit_type=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
),
|
||||
# --- Specific ---
|
||||
"mileage": BMWSensorEntityDescription(
|
||||
key="mileage",
|
||||
icon="mdi:speedometer",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_total": BMWSensorEntityDescription(
|
||||
key="remaining_range_total",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_electric": BMWSensorEntityDescription(
|
||||
key="remaining_range_electric",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_range_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:gas-station",
|
||||
unit_metric=VOLUME_LITERS,
|
||||
unit_imperial=VOLUME_GALLONS,
|
||||
unit_type=VOLUME,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
|
||||
),
|
||||
"remaining_fuel_percent": BMWSensorEntityDescription(
|
||||
key="remaining_fuel_percent",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:gas-station",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
unit_type=PERCENTAGE,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -182,8 +168,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
||||
self._attr_name = f"{vehicle.name} {description.key}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||
|
||||
# Force metric system as BMW API apparently only returns metric values now
|
||||
self._attr_native_unit_of_measurement = description.unit_metric
|
||||
# Set the correct unit of measurement based on the unit_type
|
||||
if description.unit_type:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
coordinator.hass.config.units.as_dict().get(description.unit_type)
|
||||
or description.unit_type
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -7,12 +7,20 @@ from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DATA_HASS_CONFIG, DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.NOTIFY]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Discord component."""
|
||||
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Discord from a config entry."""
|
||||
nextcord.VoiceClient.warn_nacl = False
|
||||
@@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
hass.data[DOMAIN],
|
||||
hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord"
|
||||
DOMAIN: Final = "discord"
|
||||
|
||||
URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"}
|
||||
|
||||
DATA_HASS_CONFIG = "discord_hass_config"
|
||||
|
||||
@@ -128,26 +128,17 @@ class EvoClimateEntity(EvoDevice, ClimateEntity):
|
||||
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Climate device."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._preset_modes = None
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[str]:
|
||||
"""Return a list of available hvac operation modes."""
|
||||
return list(HA_HVAC_TO_TCS)
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes."""
|
||||
return self._preset_modes
|
||||
|
||||
|
||||
class EvoZone(EvoChild, EvoClimateEntity):
|
||||
"""Base for a Honeywell TCC Zone."""
|
||||
|
||||
_attr_preset_modes = list(HA_PRESET_TO_EVO)
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Honeywell TCC Zone."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
@@ -233,7 +224,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
"""
|
||||
return self._evo_device.setpointCapabilities["maxHeatSetpoint"]
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature."""
|
||||
temperature = kwargs["temperature"]
|
||||
|
||||
@@ -249,7 +240,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
self._evo_device.set_temperature(temperature, until=until)
|
||||
)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set a Zone to one of its native EVO_* operating modes.
|
||||
|
||||
Zones inherit their _effective_ operating mode from their Controller.
|
||||
@@ -387,7 +378,7 @@ class EvoController(EvoClimateEntity):
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Raise exception as Controllers don't have a target temperature."""
|
||||
raise NotImplementedError("Evohome Controllers don't have target temperatures.")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
42
homeassistant/components/flunearyou/repairs.py
Normal file
42
homeassistant/components/flunearyou/repairs.py
Normal 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()
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."""
|
||||
|
||||
@@ -192,7 +192,6 @@ async def async_setup_entry(
|
||||
calendar_id,
|
||||
data.get(CONF_SEARCH),
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entities.append(
|
||||
GoogleCalendarEntity(
|
||||
coordinator,
|
||||
@@ -342,6 +341,9 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
# We do not ask for an update with async_add_entities()
|
||||
# because it will update disabled entities
|
||||
await self.coordinator.async_request_refresh()
|
||||
self._apply_coordinator_update()
|
||||
|
||||
async def async_get_events(
|
||||
|
||||
@@ -67,6 +67,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_devices[address], data={}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
"manufacturer_id": 18994,
|
||||
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 818,
|
||||
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 59970,
|
||||
"service_uuid": "00008151-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 14474,
|
||||
"service_uuid": "00008151-0000-1000-8000-00805f9b34fb"
|
||||
@@ -24,7 +32,7 @@
|
||||
"service_uuid": "00008251-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"requirements": ["govee-ble==0.12.6"],
|
||||
"requirements": ["govee-ble==0.14.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -138,11 +138,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Execute calls to services taking a program."""
|
||||
program = call.data[ATTR_PROGRAM]
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
options = {
|
||||
ATTR_KEY: call.data.get(ATTR_KEY),
|
||||
ATTR_VALUE: call.data.get(ATTR_VALUE),
|
||||
ATTR_UNIT: call.data.get(ATTR_UNIT),
|
||||
}
|
||||
|
||||
options = []
|
||||
|
||||
option_key = call.data.get(ATTR_KEY)
|
||||
if option_key is not None:
|
||||
option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]}
|
||||
|
||||
option_unit = call.data.get(ATTR_UNIT)
|
||||
if option_unit is not None:
|
||||
option[ATTR_UNIT] = option_unit
|
||||
|
||||
options.append(option)
|
||||
|
||||
appliance = _get_appliance_by_device_id(hass, device_id)
|
||||
await hass.async_add_executor_job(getattr(appliance, method), program, options)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"dependencies": ["application_credentials"],
|
||||
"codeowners": ["@DavidMStraub"],
|
||||
"requirements": ["homeconnect==0.7.1"],
|
||||
"requirements": ["homeconnect==0.7.2"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homeconnect"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.11"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -279,11 +279,12 @@ class Router:
|
||||
self._get_data(
|
||||
KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH,
|
||||
lambda: next(
|
||||
filter(
|
||||
lambda ssid: ssid.get("wifiisguestnetwork") == "1",
|
||||
self.client.wlan.multi_basic_settings()
|
||||
(
|
||||
ssid
|
||||
for ssid in self.client.wlan.multi_basic_settings()
|
||||
.get("Ssids", {})
|
||||
.get("Ssid", []),
|
||||
.get("Ssid", [])
|
||||
if isinstance(ssid, dict) and ssid.get("wifiisguestnetwork") == "1"
|
||||
),
|
||||
{},
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ async def async_setup_entry(
|
||||
if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH):
|
||||
switches.append(HuaweiLteMobileDataSwitch(router))
|
||||
|
||||
if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"):
|
||||
if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, {}).get("WifiEnable"):
|
||||
switches.append(HuaweiLteWifiGuestNetworkSwitch(router))
|
||||
|
||||
async_add_entities(switches, True)
|
||||
@@ -151,6 +151,6 @@ class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch):
|
||||
return "mdi:wifi" if self.is_on else "mdi:wifi-off"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
def extra_state_attributes(self) -> dict[str, str | None]:
|
||||
"""Return the state attributes."""
|
||||
return {"ssid": self.router.data[self.key].get("WifiSsid")}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Philips Hue",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||
"requirements": ["aiohue==4.4.2"],
|
||||
"requirements": ["aiohue==4.5.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics",
|
||||
|
||||
@@ -17,7 +17,7 @@ from pyicloud.services.findmyiphone import AppleDevice
|
||||
from homeassistant.components.zone import async_active_zone
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
@@ -104,6 +104,8 @@ class IcloudAccount:
|
||||
self._retried_fetch = False
|
||||
self._config_entry = config_entry
|
||||
|
||||
self.listeners: list[CALLBACK_TYPE] = []
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up an iCloud account."""
|
||||
try:
|
||||
|
||||
@@ -35,7 +35,7 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up device tracker for iCloud component."""
|
||||
account = hass.data[DOMAIN][entry.unique_id]
|
||||
account: IcloudAccount = hass.data[DOMAIN][entry.unique_id]
|
||||
tracked = set[str]()
|
||||
|
||||
@callback
|
||||
|
||||
@@ -20,7 +20,7 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up device tracker for iCloud component."""
|
||||
account = hass.data[DOMAIN][entry.unique_id]
|
||||
account: IcloudAccount = hass.data[DOMAIN][entry.unique_id]
|
||||
tracked = set[str]()
|
||||
|
||||
@callback
|
||||
|
||||
@@ -67,6 +67,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_devices[address], data={}
|
||||
)
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
{ "local_name": "sps" },
|
||||
{ "local_name": "Inkbird*" },
|
||||
{ "local_name": "iBBQ*" },
|
||||
{ "local_name": "xBBQ*" },
|
||||
{ "local_name": "tps" }
|
||||
],
|
||||
"requirements": ["inkbird-ble==0.5.1"],
|
||||
"requirements": ["inkbird-ble==0.5.5"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "KNX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||
"requirements": ["xknx==0.22.1"],
|
||||
"requirements": ["xknx==1.0.0"],
|
||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -119,18 +119,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
) -> FlowResult:
|
||||
"""Confirm discovery."""
|
||||
assert self._discovered_device is not None
|
||||
discovered = self._discovered_device
|
||||
_LOGGER.debug(
|
||||
"Confirming discovery: %s with serial %s",
|
||||
self._discovered_device.label,
|
||||
discovered.label,
|
||||
self.unique_id,
|
||||
)
|
||||
if user_input is not None or self._async_discovered_pending_migration():
|
||||
return self._async_create_entry_from_device(self._discovered_device)
|
||||
return self._async_create_entry_from_device(discovered)
|
||||
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr})
|
||||
self._set_confirm_only()
|
||||
placeholders = {
|
||||
"label": self._discovered_device.label,
|
||||
"host": self._discovered_device.ip_addr,
|
||||
"label": discovered.label,
|
||||
"host": discovered.ip_addr,
|
||||
"serial": self.unique_id,
|
||||
}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -67,6 +67,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_devices[address], data={}
|
||||
)
|
||||
|
||||
@@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._config[ATTR_SENSOR_STATE]
|
||||
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
async def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state."""
|
||||
|
||||
super().async_restore_last_state(last_state)
|
||||
await super().async_restore_last_state(last_state)
|
||||
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
|
||||
|
||||
@@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity):
|
||||
if (state := await self.async_get_last_state()) is None:
|
||||
return
|
||||
|
||||
self.async_restore_last_state(state)
|
||||
await self.async_restore_last_state(state)
|
||||
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
async def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state."""
|
||||
self._config[ATTR_SENSOR_STATE] = last_state.state
|
||||
self._config[ATTR_SENSOR_ATTRIBUTES] = {
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -27,6 +27,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import MobileAppEntity
|
||||
from .webhook import _extract_sensor_unique_id
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -73,9 +74,30 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class MobileAppSensor(MobileAppEntity, SensorEntity):
|
||||
class MobileAppSensor(MobileAppEntity, RestoreSensor):
|
||||
"""Representation of an mobile app sensor."""
|
||||
|
||||
async def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state."""
|
||||
|
||||
await super().async_restore_last_state(last_state)
|
||||
|
||||
if not (last_sensor_data := await self.async_get_last_sensor_data()):
|
||||
# Workaround to handle migration to RestoreSensor, can be removed
|
||||
# in HA Core 2023.4
|
||||
self._config[ATTR_SENSOR_STATE] = None
|
||||
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
|
||||
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
|
||||
if (
|
||||
self.device_class == SensorDeviceClass.TEMPERATURE
|
||||
and sensor_unique_id == "battery_temperature"
|
||||
):
|
||||
self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS
|
||||
return
|
||||
|
||||
self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
|
||||
self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -121,8 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
multicast_interface = entry.data.get(CONF_INTERFACE, DEFAULT_INTERFACE)
|
||||
wait_for_push = entry.options.get(CONF_WAIT_FOR_PUSH, DEFAULT_WAIT_FOR_PUSH)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
# Create multicast Listener
|
||||
async with setup_lock:
|
||||
if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]:
|
||||
@@ -213,6 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"requirements": ["pynetgear==0.10.6"],
|
||||
"requirements": ["pynetgear==0.10.7"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Provides functionality to notify people."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.persistent_notification as pn
|
||||
@@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema(
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the notify services."""
|
||||
|
||||
platform_setups = async_setup_legacy(hass, config)
|
||||
|
||||
# We need to add the component here break the deadlock
|
||||
# when setting up integrations from config entries as
|
||||
# they would otherwise wait for notify to be
|
||||
# setup and thus the config entries would not be able to
|
||||
# setup their platforms.
|
||||
# setup their platforms, but we need to do it after
|
||||
# the dispatcher is connected so we don't miss integrations
|
||||
# that are registered before the dispatcher is connected
|
||||
hass.config.components.add(DOMAIN)
|
||||
await async_setup_legacy(hass, config)
|
||||
|
||||
if platform_setups:
|
||||
await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups])
|
||||
|
||||
async def persistent_notification(service: ServiceCall) -> None:
|
||||
"""Send notification via the built-in persistsent_notify integration."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from functools import partial
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services"
|
||||
NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher"
|
||||
|
||||
|
||||
async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
@callback
|
||||
def async_setup_legacy(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> list[Coroutine[Any, Any, None]]:
|
||||
"""Set up legacy notify services."""
|
||||
hass.data.setdefault(NOTIFY_SERVICES, {})
|
||||
hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None)
|
||||
@@ -101,15 +105,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
)
|
||||
hass.config.components.add(f"{DOMAIN}.{integration_name}")
|
||||
|
||||
setup_tasks = [
|
||||
asyncio.create_task(async_setup_platform(integration_name, p_config))
|
||||
for integration_name, p_config in config_per_platform(config, DOMAIN)
|
||||
if integration_name is not None
|
||||
]
|
||||
|
||||
if setup_tasks:
|
||||
await asyncio.wait(setup_tasks)
|
||||
|
||||
async def async_platform_discovered(
|
||||
platform: str, info: DiscoveryInfoType | None
|
||||
) -> None:
|
||||
@@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
hass, DOMAIN, async_platform_discovered
|
||||
)
|
||||
|
||||
return [
|
||||
async_setup_platform(integration_name, p_config)
|
||||
for integration_name, p_config in config_per_platform(config, DOMAIN)
|
||||
if integration_name is not None
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None:
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
from aionotion import async_get_client
|
||||
@@ -31,7 +33,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
ATTR_SYSTEM_MODE = "system_mode"
|
||||
ATTR_SYSTEM_NAME = "system_name"
|
||||
|
||||
DEFAULT_ATTRIBUTION = "Data provided by Notion"
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
@@ -75,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"There was a Notion error while updating {attr}: {result}"
|
||||
) from result
|
||||
if isinstance(result, Exception):
|
||||
if LOGGER.isEnabledFor(logging.DEBUG):
|
||||
LOGGER.debug("".join(traceback.format_tb(result.__traceback__)))
|
||||
raise UpdateFailed(
|
||||
f"There was an unknown error while updating {attr}: {result}"
|
||||
) from result
|
||||
|
||||
@@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class):
|
||||
platforms[platform].append(device)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
for gateway in setup.gateways:
|
||||
@@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
configuration_url=server.configuration_url,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -437,7 +437,7 @@ class RainMachineEntity(CoordinatorEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.update_from_latest_data()
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Hourly restrictions",
|
||||
icon="mdi:cancel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="hourly",
|
||||
),
|
||||
@@ -86,7 +85,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Month restrictions",
|
||||
icon="mdi:cancel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="month",
|
||||
),
|
||||
@@ -95,7 +93,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Rain delay restrictions",
|
||||
icon="mdi:cancel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="rainDelay",
|
||||
),
|
||||
@@ -113,7 +110,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Weekday restrictions",
|
||||
icon="mdi:cancel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="weekDay",
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,6 +67,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_devices[address], data={}
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"local_name": "SensorPush*"
|
||||
}
|
||||
],
|
||||
"requirements": ["sensorpush-ble==1.5.1"],
|
||||
"requirements": ["sensorpush-ble==1.5.2"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_CLIENT, DOMAIN
|
||||
from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Slack component."""
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
|
||||
# Iterate all entries for notify to only get Slack
|
||||
if Platform.NOTIFY in config:
|
||||
for entry in config[Platform.NOTIFY]:
|
||||
@@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
hass.data[DOMAIN],
|
||||
hass.data[DATA_HASS_CONFIG],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -14,3 +14,5 @@ CONF_DEFAULT_CHANNEL = "default_channel"
|
||||
DATA_CLIENT = "client"
|
||||
DEFAULT_TIMEOUT_SECONDS = 15
|
||||
DOMAIN: Final = "slack"
|
||||
|
||||
DATA_HASS_CONFIG = "slack_hass_config"
|
||||
|
||||
@@ -20,31 +20,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the SMS notification service."""
|
||||
|
||||
if SMS_GATEWAY not in hass.data[DOMAIN]:
|
||||
_LOGGER.error("SMS gateway not found, cannot initialize service")
|
||||
return
|
||||
|
||||
gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY]
|
||||
|
||||
if discovery_info is None:
|
||||
number = config[CONF_RECIPIENT]
|
||||
else:
|
||||
number = discovery_info[CONF_RECIPIENT]
|
||||
|
||||
return SMSNotificationService(gateway, number)
|
||||
return SMSNotificationService(hass, number)
|
||||
|
||||
|
||||
class SMSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for SMS."""
|
||||
|
||||
def __init__(self, gateway, number):
|
||||
def __init__(self, hass, number):
|
||||
"""Initialize the service."""
|
||||
self.gateway = gateway
|
||||
|
||||
self.hass = hass
|
||||
self.number = number
|
||||
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
"""Send SMS message."""
|
||||
|
||||
if SMS_GATEWAY not in self.hass.data[DOMAIN]:
|
||||
_LOGGER.error("SMS gateway not found, cannot send message")
|
||||
return
|
||||
|
||||
gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY]
|
||||
|
||||
targets = kwargs.get(CONF_TARGET, [self.number])
|
||||
smsinfo = {
|
||||
"Class": -1,
|
||||
@@ -67,6 +68,6 @@ class SMSNotificationService(BaseNotificationService):
|
||||
encoded_message["Number"] = target
|
||||
try:
|
||||
# Actually send the message
|
||||
await self.gateway.send_sms_async(encoded_message)
|
||||
await gateway.send_sms_async(encoded_message)
|
||||
except gammu.GSMError as exc:
|
||||
_LOGGER.error("Sending to %s failed: %s", target, exc)
|
||||
|
||||
@@ -180,7 +180,10 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of current playing media in seconds."""
|
||||
if not self._currently_playing:
|
||||
if (
|
||||
not self._currently_playing
|
||||
or self._currently_playing.get("progress_ms") is None
|
||||
):
|
||||
return None
|
||||
return self._currently_playing["progress_ms"] / 1000
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -79,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner):
|
||||
def get_swisscom_data(self):
|
||||
"""Retrieve data from Swisscom and return parsed result."""
|
||||
url = f"http://{self.host}/ws"
|
||||
headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"}
|
||||
headers = {"Content-Type": "application/x-sah-ws-4-call+json"}
|
||||
data = """
|
||||
{"service":"Devices", "method":"get",
|
||||
"parameters":{"expression":"lan and not self"}}"""
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.16.0"],
|
||||
"requirements": ["PySwitchbot==0.18.10"],
|
||||
"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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ from systembridgeconnector.exceptions import (
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
from systembridgeconnector.models.keyboard_key import KeyboardKey
|
||||
from systembridgeconnector.models.keyboard_text import KeyboardText
|
||||
from systembridgeconnector.models.open_path import OpenPath
|
||||
from systembridgeconnector.models.open_url import OpenUrl
|
||||
from systembridgeconnector.version import SUPPORTED_VERSION, Version
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -149,7 +153,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
await coordinator.websocket_client.open_path(call.data[CONF_PATH])
|
||||
await coordinator.websocket_client.open_path(
|
||||
OpenPath(path=call.data[CONF_PATH])
|
||||
)
|
||||
|
||||
async def handle_open_url(call: ServiceCall) -> None:
|
||||
"""Handle the open url service call."""
|
||||
@@ -157,21 +163,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
await coordinator.websocket_client.open_url(call.data[CONF_URL])
|
||||
await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL]))
|
||||
|
||||
async def handle_send_keypress(call: ServiceCall) -> None:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY])
|
||||
await coordinator.websocket_client.keyboard_keypress(
|
||||
KeyboardKey(key=call.data[CONF_KEY])
|
||||
)
|
||||
|
||||
async def handle_send_text(call: ServiceCall) -> None:
|
||||
"""Handle the send_keypress service call."""
|
||||
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
call.data[CONF_BRIDGE]
|
||||
]
|
||||
await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT])
|
||||
await coordinator.websocket_client.keyboard_text(
|
||||
KeyboardText(text=call.data[CONF_TEXT])
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
|
||||
@@ -7,12 +7,13 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE
|
||||
from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
)
|
||||
from systembridgeconnector.models.get_data import GetData
|
||||
from systembridgeconnector.models.system import System
|
||||
from systembridgeconnector.websocket_client import WebSocketClient
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -38,7 +39,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(
|
||||
async def _validate_input(
|
||||
hass: HomeAssistant,
|
||||
data: dict[str, Any],
|
||||
) -> dict[str, str]:
|
||||
@@ -56,15 +57,12 @@ async def validate_input(
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
await websocket_client.connect(session=async_get_clientsession(hass))
|
||||
await websocket_client.get_data(["system"])
|
||||
while True:
|
||||
message = await websocket_client.receive_message()
|
||||
_LOGGER.debug("Message: %s", message)
|
||||
if (
|
||||
message[EVENT_TYPE] == TYPE_DATA_UPDATE
|
||||
and message[EVENT_MODULE] == "system"
|
||||
):
|
||||
break
|
||||
hass.async_create_task(websocket_client.listen())
|
||||
response = await websocket_client.get_data(GetData(modules=["system"]))
|
||||
_LOGGER.debug("Got response: %s", response.json())
|
||||
if response.data is None or not isinstance(response.data, System):
|
||||
raise CannotConnect("No data received")
|
||||
system: System = response.data
|
||||
except AuthenticationException as exception:
|
||||
_LOGGER.warning(
|
||||
"Authentication error when connecting to %s: %s", data[CONF_HOST], exception
|
||||
@@ -81,14 +79,12 @@ async def validate_input(
|
||||
except asyncio.TimeoutError as exception:
|
||||
_LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception)
|
||||
raise CannotConnect from exception
|
||||
except ValueError as exception:
|
||||
raise CannotConnect from exception
|
||||
|
||||
_LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message)
|
||||
_LOGGER.debug("Got System data: %s", system.json())
|
||||
|
||||
if "uuid" not in message["data"]:
|
||||
error = "No UUID in result!"
|
||||
raise CannotConnect(error)
|
||||
|
||||
return {"hostname": host, "uuid": message["data"]["uuid"]}
|
||||
return {"hostname": host, "uuid": system.uuid}
|
||||
|
||||
|
||||
async def _async_get_info(
|
||||
@@ -98,7 +94,7 @@ async def _async_get_info(
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await validate_input(hass, user_input)
|
||||
info = await _validate_input(hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
from pydantic import BaseModel # pylint: disable=no-name-in-module
|
||||
@@ -17,8 +18,10 @@ from systembridgeconnector.models.battery import Battery
|
||||
from systembridgeconnector.models.cpu import Cpu
|
||||
from systembridgeconnector.models.disk import Disk
|
||||
from systembridgeconnector.models.display import Display
|
||||
from systembridgeconnector.models.get_data import GetData
|
||||
from systembridgeconnector.models.gpu import Gpu
|
||||
from systembridgeconnector.models.memory import Memory
|
||||
from systembridgeconnector.models.register_data_listener import RegisterDataListener
|
||||
from systembridgeconnector.models.system import System
|
||||
from systembridgeconnector.websocket_client import WebSocketClient
|
||||
|
||||
@@ -93,12 +96,14 @@ class SystemBridgeDataUpdateCoordinator(
|
||||
if not self.websocket_client.connected:
|
||||
await self._setup_websocket()
|
||||
|
||||
self.hass.async_create_task(self.websocket_client.get_data(modules))
|
||||
self.hass.async_create_task(
|
||||
self.websocket_client.get_data(GetData(modules=modules))
|
||||
)
|
||||
|
||||
async def async_handle_module(
|
||||
self,
|
||||
module_name: str,
|
||||
module,
|
||||
module: Any,
|
||||
) -> None:
|
||||
"""Handle data from the WebSocket client."""
|
||||
self.logger.debug("Set new data for: %s", module_name)
|
||||
@@ -174,7 +179,9 @@ class SystemBridgeDataUpdateCoordinator(
|
||||
|
||||
self.hass.async_create_task(self._listen_for_data())
|
||||
|
||||
await self.websocket_client.register_data_listener(MODULES)
|
||||
await self.websocket_client.register_data_listener(
|
||||
RegisterDataListener(modules=MODULES)
|
||||
)
|
||||
|
||||
self.last_update_success = True
|
||||
self.async_update_listeners()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "System Bridge",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/system_bridge",
|
||||
"requirements": ["systembridgeconnector==3.3.2"],
|
||||
"requirements": ["systembridgeconnector==3.4.4"],
|
||||
"codeowners": ["@timmo001"],
|
||||
"zeroconf": ["_system-bridge._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "UniFi Protect",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
|
||||
"requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"],
|
||||
"requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
|
||||
"quality_scale": "platinum",
|
||||
|
||||
@@ -189,7 +189,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Ack that device is slow."""
|
||||
if user_input is not None or not onboarding.async_is_onboarded(self.hass):
|
||||
if user_input is not None:
|
||||
return self._async_get_or_create_entry()
|
||||
|
||||
self._set_confirm_only()
|
||||
@@ -205,6 +205,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
discovery = self._discovered_devices[address]
|
||||
|
||||
self.context["title_placeholders"] = {"name": discovery.title}
|
||||
@@ -260,7 +261,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
assert entry is not None
|
||||
|
||||
device: DeviceData = self.context["device"]
|
||||
device: DeviceData = entry_data["device"]
|
||||
self._discovered_device = device
|
||||
|
||||
self._discovery_info = device.last_service_info
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user