forked from home-assistant/core
Compare commits
115 Commits
2022.8.0b0
...
2022.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
19b0961084 | ||
|
|
e073f6b439 | ||
|
|
c4906414ea | ||
|
|
cc9a130f58 | ||
|
|
c90a223cb6 | ||
|
|
2eddbf2381 | ||
|
|
654e26052b | ||
|
|
676664022d | ||
|
|
ed57951571 | ||
|
|
b9ee81dfc3 | ||
|
|
da00f5ba1e | ||
|
|
30cd087f6f | ||
|
|
66afd1e696 | ||
|
|
23488f392b | ||
|
|
7140a9d025 | ||
|
|
4f671bccbc | ||
|
|
6b588d41ff | ||
|
|
b962a6e767 | ||
|
|
a332eb154c | ||
|
|
75747ce319 | ||
|
|
c6038380d6 | ||
|
|
990975e908 | ||
|
|
2a58bf06c1 | ||
|
|
5ab549653b | ||
|
|
ffd2813150 | ||
|
|
ebf91fe46b | ||
|
|
e330147751 | ||
|
|
26a3621bb3 | ||
|
|
58265664d1 | ||
|
|
d205fb5064 | ||
|
|
38ae2f4e9e | ||
|
|
d84bc20a58 | ||
|
|
a3276e00b9 | ||
|
|
bdb627539e | ||
|
|
240890e496 | ||
|
|
e2a9ab1831 | ||
|
|
8f8bccd982 | ||
|
|
26c475d3dc | ||
|
|
0f0b51bee7 | ||
|
|
241ffe07b9 | ||
|
|
c3c5442467 | ||
|
|
d7827d9902 | ||
|
|
176d44190e | ||
|
|
48b97a1f2d | ||
|
|
f4defb660b | ||
|
|
dfd503cc1a | ||
|
|
97c6c949e7 | ||
|
|
c469bdea75 | ||
|
|
2b1fbbfae3 | ||
|
|
e4e36b51b6 | ||
|
|
53870dd0bc | ||
|
|
38909855bf | ||
|
|
f98d95c76f | ||
|
|
2bf10799ed | ||
|
|
4be623a492 | ||
|
|
a000687eb5 | ||
|
|
c10ed6edba | ||
|
|
2dc318be54 | ||
|
|
b4d2c25f8e | ||
|
|
e7ff97bac0 | ||
|
|
add9ff5736 | ||
|
|
937fd490f2 | ||
|
|
96587c1227 | ||
|
|
15e6fcca41 | ||
|
|
7811518d7c | ||
|
|
70731c0bc7 | ||
|
|
3b8650d053 | ||
|
|
15f87ca0a1 |
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@ PARALLEL_UPDATES = 4
|
||||
class AdGuardHomeEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]]
|
||||
value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,56 +42,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
|
||||
name="DNS queries",
|
||||
icon="mdi:magnify",
|
||||
native_unit_of_measurement="queries",
|
||||
value_fn=lambda adguard: adguard.stats.dns_queries,
|
||||
value_fn=lambda adguard: adguard.stats.dns_queries(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="blocked_filtering",
|
||||
name="DNS queries blocked",
|
||||
icon="mdi:magnify-close",
|
||||
native_unit_of_measurement="queries",
|
||||
value_fn=lambda adguard: adguard.stats.blocked_filtering,
|
||||
value_fn=lambda adguard: adguard.stats.blocked_filtering(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="blocked_percentage",
|
||||
name="DNS queries blocked ratio",
|
||||
icon="mdi:magnify-close",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda adguard: adguard.stats.blocked_percentage,
|
||||
value_fn=lambda adguard: adguard.stats.blocked_percentage(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="blocked_parental",
|
||||
name="Parental control blocked",
|
||||
icon="mdi:human-male-girl",
|
||||
native_unit_of_measurement="requests",
|
||||
value_fn=lambda adguard: adguard.stats.replaced_parental,
|
||||
value_fn=lambda adguard: adguard.stats.replaced_parental(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="blocked_safebrowsing",
|
||||
name="Safe browsing blocked",
|
||||
icon="mdi:shield-half-full",
|
||||
native_unit_of_measurement="requests",
|
||||
value_fn=lambda adguard: adguard.stats.replaced_safebrowsing,
|
||||
value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="enforced_safesearch",
|
||||
name="Safe searches enforced",
|
||||
icon="mdi:shield-search",
|
||||
native_unit_of_measurement="requests",
|
||||
value_fn=lambda adguard: adguard.stats.replaced_safesearch,
|
||||
value_fn=lambda adguard: adguard.stats.replaced_safesearch(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="average_speed",
|
||||
name="Average processing speed",
|
||||
icon="mdi:speedometer",
|
||||
native_unit_of_measurement=TIME_MILLISECONDS,
|
||||
value_fn=lambda adguard: adguard.stats.avg_processing_time,
|
||||
value_fn=lambda adguard: adguard.stats.avg_processing_time(),
|
||||
),
|
||||
AdGuardHomeEntityDescription(
|
||||
key="rules_count",
|
||||
name="Rules count",
|
||||
icon="mdi:counter",
|
||||
native_unit_of_measurement="rules",
|
||||
value_fn=lambda adguard: adguard.stats.avg_processing_time,
|
||||
value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
@@ -144,7 +144,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
value = await self.entity_description.value_fn(self.adguard)()
|
||||
value = await self.entity_description.value_fn(self.adguard)
|
||||
self._attr_native_value = value
|
||||
if isinstance(value, float):
|
||||
self._attr_native_value = f"{value:.2f}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.31"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.41"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,5 +24,11 @@
|
||||
"description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"pending_removal": {
|
||||
"description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.",
|
||||
"title": "Die Ambee-Integration wird entfernt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,11 @@
|
||||
"description": "Configura Ambee per l'integrazione con Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"pending_removal": {
|
||||
"description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.",
|
||||
"title": "L'integrazione Ambee verr\u00e0 rimossa"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,11 @@
|
||||
"description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"pending_removal": {
|
||||
"description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.",
|
||||
"title": "Integracja Ambee zostanie usuni\u0119ta"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,11 @@
|
||||
"description": "Configure o Ambee para integrar com o Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"pending_removal": {
|
||||
"description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.",
|
||||
"title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,5 +24,11 @@
|
||||
"description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"pending_removal": {
|
||||
"description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
|
||||
"title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.",
|
||||
"title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.",
|
||||
"title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.",
|
||||
"title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
|
||||
"title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -4,28 +4,36 @@ import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_registry import async_migrate_entries
|
||||
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
from .device import AxisNetworkDevice
|
||||
from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS
|
||||
from .device import AxisNetworkDevice, get_axis_device
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up the Axis component."""
|
||||
"""Set up the Axis integration."""
|
||||
hass.data.setdefault(AXIS_DOMAIN, {})
|
||||
|
||||
device = AxisNetworkDevice(hass, config_entry)
|
||||
|
||||
if not await device.async_setup():
|
||||
return False
|
||||
|
||||
hass.data[AXIS_DOMAIN][config_entry.unique_id] = device
|
||||
try:
|
||||
api = await get_axis_device(hass, config_entry.data)
|
||||
except CannotConnect as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except AuthenticationRequired as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice(
|
||||
hass, config_entry, api
|
||||
)
|
||||
await device.async_update_device_registry()
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
device.async_setup_events()
|
||||
|
||||
config_entry.add_update_listener(device.async_new_address_callback)
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import ip_address
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@@ -32,7 +33,7 @@ from .const import (
|
||||
DEFAULT_VIDEO_SOURCE,
|
||||
DOMAIN as AXIS_DOMAIN,
|
||||
)
|
||||
from .device import AxisNetworkDevice, get_device
|
||||
from .device import AxisNetworkDevice, get_axis_device
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
|
||||
@@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
device = await get_device(
|
||||
self.hass,
|
||||
host=user_input[CONF_HOST],
|
||||
port=user_input[CONF_PORT],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
device = await get_axis_device(self.hass, MappingProxyType(user_input))
|
||||
|
||||
serial = device.vapix.serial_number
|
||||
await self.async_set_unique_id(format_mac(serial))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Axis network device abstraction."""
|
||||
|
||||
import asyncio
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
import axis
|
||||
@@ -24,7 +26,6 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect
|
||||
class AxisNetworkDevice:
|
||||
"""Manages a Axis device."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
def __init__(self, hass, config_entry, api):
|
||||
"""Initialize the device."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.available = True
|
||||
self.api = api
|
||||
|
||||
self.api = None
|
||||
self.fw_version = None
|
||||
self.product_type = None
|
||||
self.available = True
|
||||
self.fw_version = api.vapix.firmware_version
|
||||
self.product_type = api.vapix.product_type
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
@@ -184,7 +185,7 @@ class AxisNetworkDevice:
|
||||
sw_version=self.fw_version,
|
||||
)
|
||||
|
||||
async def use_mqtt(self, hass: HomeAssistant, component: str) -> None:
|
||||
async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None:
|
||||
"""Set up to use MQTT."""
|
||||
try:
|
||||
status = await self.api.vapix.mqtt.get_client_status()
|
||||
@@ -209,50 +210,18 @@ class AxisNetworkDevice:
|
||||
|
||||
# Setup and teardown methods
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up the device."""
|
||||
try:
|
||||
self.api = await get_device(
|
||||
self.hass,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
def async_setup_events(self):
|
||||
"""Set up the device events."""
|
||||
|
||||
if self.option_events:
|
||||
self.api.stream.connection_status_callback.append(
|
||||
self.async_connection_status_callback
|
||||
)
|
||||
self.api.enable_events(event_callback=self.async_event_callback)
|
||||
self.api.stream.start()
|
||||
|
||||
except CannotConnect as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
except AuthenticationRequired as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
self.fw_version = self.api.vapix.firmware_version
|
||||
self.product_type = self.api.vapix.product_type
|
||||
|
||||
async def start_platforms():
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, platform
|
||||
)
|
||||
for platform in PLATFORMS
|
||||
)
|
||||
)
|
||||
if self.option_events:
|
||||
self.api.stream.connection_status_callback.append(
|
||||
self.async_connection_status_callback
|
||||
)
|
||||
self.api.enable_events(event_callback=self.async_event_callback)
|
||||
self.api.stream.start()
|
||||
|
||||
if self.api.vapix.mqtt:
|
||||
async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt)
|
||||
|
||||
self.hass.async_create_task(start_platforms())
|
||||
|
||||
self.config_entry.add_update_listener(self.async_new_address_callback)
|
||||
|
||||
return True
|
||||
if self.api.vapix.mqtt:
|
||||
async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt)
|
||||
|
||||
@callback
|
||||
def disconnect_from_stream(self):
|
||||
@@ -274,14 +243,21 @@ class AxisNetworkDevice:
|
||||
)
|
||||
|
||||
|
||||
async def get_device(
|
||||
hass: HomeAssistant, host: str, port: int, username: str, password: str
|
||||
async def get_axis_device(
|
||||
hass: HomeAssistant,
|
||||
config: MappingProxyType[str, Any],
|
||||
) -> axis.AxisDevice:
|
||||
"""Create a Axis device."""
|
||||
session = get_async_client(hass, verify_ssl=False)
|
||||
|
||||
device = axis.AxisDevice(
|
||||
Configuration(session, host, port=port, username=username, password=password)
|
||||
Configuration(
|
||||
session,
|
||||
config[CONF_HOST],
|
||||
port=config[CONF_PORT],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -291,11 +267,13 @@ async def get_device(
|
||||
return device
|
||||
|
||||
except axis.Unauthorized as err:
|
||||
LOGGER.warning("Connected to device at %s but not registered", host)
|
||||
LOGGER.warning(
|
||||
"Connected to device at %s but not registered", config[CONF_HOST]
|
||||
)
|
||||
raise AuthenticationRequired from err
|
||||
|
||||
except (asyncio.TimeoutError, axis.RequestError) as err:
|
||||
LOGGER.error("Error connecting to the Axis device at %s", host)
|
||||
LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST])
|
||||
raise CannotConnect from err
|
||||
|
||||
except axis.AxisException as err:
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
"""The bluetooth integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import Future
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import Final, Union
|
||||
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
|
||||
@@ -24,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
|
||||
@@ -39,13 +42,25 @@ from .models import HaBleakScanner, HaBleakScannerWrapper
|
||||
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
||||
from .util import async_get_bluetooth_adapters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5
|
||||
START_TIMEOUT = 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):
|
||||
@@ -92,9 +107,8 @@ SCANNING_MODE_TO_BLEAK = {
|
||||
|
||||
|
||||
BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT")
|
||||
BluetoothCallback = Callable[
|
||||
[Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None
|
||||
]
|
||||
BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None]
|
||||
ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool]
|
||||
|
||||
|
||||
@hass_callback
|
||||
@@ -150,15 +164,47 @@ def async_register_callback(
|
||||
hass: HomeAssistant,
|
||||
callback: BluetoothCallback,
|
||||
match_dict: BluetoothCallbackMatcher | None,
|
||||
mode: BluetoothScanningMode,
|
||||
) -> Callable[[], None]:
|
||||
"""Register to receive a callback on bluetooth change.
|
||||
|
||||
mode is currently not used as we only support active scanning.
|
||||
Passive scanning will be available in the future. The flag
|
||||
is required to be present to avoid a future breaking change
|
||||
when we support passive scanning.
|
||||
|
||||
Returns a callback that can be used to cancel the registration.
|
||||
"""
|
||||
manager: BluetoothManager = hass.data[DOMAIN]
|
||||
return manager.async_register_callback(callback, match_dict)
|
||||
|
||||
|
||||
async def async_process_advertisements(
|
||||
hass: HomeAssistant,
|
||||
callback: ProcessAdvertisementCallback,
|
||||
match_dict: BluetoothCallbackMatcher,
|
||||
mode: BluetoothScanningMode,
|
||||
timeout: int,
|
||||
) -> BluetoothServiceInfoBleak:
|
||||
"""Process advertisements until callback returns true or timeout expires."""
|
||||
done: Future[BluetoothServiceInfoBleak] = Future()
|
||||
|
||||
@hass_callback
|
||||
def _async_discovered_device(
|
||||
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
|
||||
) -> None:
|
||||
if not done.done() and callback(service_info):
|
||||
done.set_result(service_info)
|
||||
|
||||
unload = async_register_callback(hass, _async_discovered_device, match_dict, mode)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(timeout):
|
||||
return await done
|
||||
finally:
|
||||
unload()
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_track_unavailable(
|
||||
hass: HomeAssistant,
|
||||
@@ -211,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
|
||||
|
||||
@@ -222,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)
|
||||
|
||||
|
||||
@@ -232,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
|
||||
|
||||
|
||||
@@ -248,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:
|
||||
@@ -276,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
|
||||
@@ -300,12 +355,72 @@ class BluetoothManager:
|
||||
self._device_detected, {}
|
||||
)
|
||||
try:
|
||||
await self.scanner.start()
|
||||
except (FileNotFoundError, BleakError) as ex:
|
||||
async with async_timeout.timeout(START_TIMEOUT):
|
||||
await self.scanner.start() # type: ignore[no-untyped-call]
|
||||
except InvalidMessageError as ex:
|
||||
self._cancel_device_detected()
|
||||
_LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
f"Invalid DBus message received: {ex}; try restarting `dbus`"
|
||||
) from ex
|
||||
except BrokenPipeError as ex:
|
||||
self._cancel_device_detected()
|
||||
_LOGGER.debug("DBus connection broken: %s", ex, exc_info=True)
|
||||
if is_docker_env():
|
||||
raise ConfigEntryNotReady(
|
||||
f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container"
|
||||
) from ex
|
||||
raise ConfigEntryNotReady(
|
||||
f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`"
|
||||
) from ex
|
||||
except FileNotFoundError as ex:
|
||||
self._cancel_device_detected()
|
||||
_LOGGER.debug(
|
||||
"FileNotFoundError while starting bluetooth: %s", ex, exc_info=True
|
||||
)
|
||||
if is_docker_env():
|
||||
raise ConfigEntryNotReady(
|
||||
f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}"
|
||||
) from ex
|
||||
raise ConfigEntryNotReady(
|
||||
f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}"
|
||||
) from ex
|
||||
except asyncio.TimeoutError as ex:
|
||||
self._cancel_device_detected()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timed out starting Bluetooth after {START_TIMEOUT} seconds"
|
||||
) from ex
|
||||
except BleakError as ex:
|
||||
self._cancel_device_detected()
|
||||
_LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True)
|
||||
raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex
|
||||
self.async_setup_unavailable_tracking()
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||
self._async_setup_scanner_watchdog()
|
||||
self._cancel_stop = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
|
||||
)
|
||||
|
||||
@hass_callback
|
||||
def _async_setup_scanner_watchdog(self) -> None:
|
||||
"""If Dbus gets restarted or updated, we need to restart the scanner."""
|
||||
self._last_detection = MONOTONIC_TIME()
|
||||
self._cancel_watchdog = async_track_time_interval(
|
||||
self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL
|
||||
)
|
||||
|
||||
async def _async_scanner_watchdog(self, now: datetime) -> None:
|
||||
"""Check if the scanner is running."""
|
||||
time_since_last_detection = MONOTONIC_TIME() - self._last_detection
|
||||
if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT:
|
||||
return
|
||||
_LOGGER.info(
|
||||
"Bluetooth scanner has gone quiet for %s, restarting",
|
||||
SCANNER_WATCHDOG_INTERVAL,
|
||||
)
|
||||
async with self.start_stop_lock:
|
||||
self.async_start_reload()
|
||||
await self.async_stop()
|
||||
await self.async_start(self._scanning_mode, self._adapter)
|
||||
|
||||
@hass_callback
|
||||
def async_setup_unavailable_tracking(self) -> None:
|
||||
@@ -340,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
|
||||
)
|
||||
@@ -452,17 +568,29 @@ class BluetoothManager:
|
||||
for device_adv in self.scanner.history.values()
|
||||
]
|
||||
|
||||
async def async_stop(self, event: Event | None = None) -> None:
|
||||
async def _async_hass_stopping(self, event: Event) -> None:
|
||||
"""Stop the Bluetooth integration at shutdown."""
|
||||
self._cancel_stop = None
|
||||
await self.async_stop()
|
||||
|
||||
async def async_stop(self) -> None:
|
||||
"""Stop bluetooth discovery."""
|
||||
_LOGGER.debug("Stopping bluetooth discovery")
|
||||
if self._cancel_watchdog:
|
||||
self._cancel_watchdog()
|
||||
self._cancel_watchdog = None
|
||||
if self._cancel_device_detected:
|
||||
self._cancel_device_detected()
|
||||
self._cancel_device_detected = None
|
||||
if self._cancel_unavailable_tracking:
|
||||
self._cancel_unavailable_tracking()
|
||||
self._cancel_unavailable_tracking = None
|
||||
if self._cancel_stop:
|
||||
self._cancel_stop()
|
||||
self._cancel_stop = None
|
||||
if self.scanner:
|
||||
try:
|
||||
await self.scanner.stop()
|
||||
await self.scanner.stop() # type: ignore[no-untyped-call]
|
||||
except BleakError as ex:
|
||||
# This is not fatal, and they may want to reload
|
||||
# the config entry to restart the scanner if they
|
||||
|
||||
@@ -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.14.3", "bluetooth-adapters==0.1.2"],
|
||||
"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, cast
|
||||
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"
|
||||
@@ -32,7 +35,7 @@ def _dispatch_callback(
|
||||
"""Dispatch the callback."""
|
||||
if not callback:
|
||||
# Callback destroyed right before being called, ignore
|
||||
return
|
||||
return # type: ignore[unreachable]
|
||||
|
||||
if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection(
|
||||
advertisement_data.service_uuids
|
||||
@@ -45,7 +48,7 @@ def _dispatch_callback(
|
||||
_LOGGER.exception("Error in callback: %s", callback)
|
||||
|
||||
|
||||
class HaBleakScanner(BleakScanner): # type: ignore[misc]
|
||||
class HaBleakScanner(BleakScanner):
|
||||
"""BleakScanner that cannot be stopped."""
|
||||
|
||||
def __init__( # pylint: disable=super-init-not-called
|
||||
@@ -106,16 +109,29 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc]
|
||||
_dispatch_callback(*callback_filters, device, advertisement_data)
|
||||
|
||||
|
||||
class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc]
|
||||
class HaBleakScannerWrapper(BaseBleakScanner):
|
||||
"""A wrapper that uses the single instance."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
detection_callback: AdvertisementDataCallback | None = None,
|
||||
service_uuids: list[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the BleakScanner."""
|
||||
self._detection_cancel: CALLBACK_TYPE | None = None
|
||||
self._mapped_filters: dict[str, set[str]] = {}
|
||||
self._adv_data_callback: AdvertisementDataCallback | None = None
|
||||
self._map_filters(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
remapped_kwargs = {
|
||||
"detection_callback": detection_callback,
|
||||
"service_uuids": service_uuids or [],
|
||||
**kwargs,
|
||||
}
|
||||
self._map_filters(*args, **remapped_kwargs)
|
||||
super().__init__(
|
||||
detection_callback=detection_callback, service_uuids=service_uuids or []
|
||||
)
|
||||
|
||||
async def stop(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Stop scanning for devices."""
|
||||
@@ -153,9 +169,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc]
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""Return a list of discovered devices."""
|
||||
assert HA_BLEAK_SCANNER is not None
|
||||
return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices)
|
||||
return HA_BLEAK_SCANNER.discovered_devices
|
||||
|
||||
def register_detection_callback(self, callback: AdvertisementDataCallback) -> None:
|
||||
def register_detection_callback(
|
||||
self, callback: AdvertisementDataCallback | None
|
||||
) -> None:
|
||||
"""Register a callback that is called when a device is discovered or has a property changed.
|
||||
|
||||
This method takes the callback and registers it with the long running
|
||||
@@ -171,6 +189,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc]
|
||||
self._cancel_callback()
|
||||
super().register_detection_callback(self._adv_data_callback)
|
||||
assert HA_BLEAK_SCANNER is not None
|
||||
assert self._callback is not None
|
||||
self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback(
|
||||
self._callback, self._mapped_filters
|
||||
)
|
||||
|
||||
@@ -1,17 +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.service_info.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BluetoothChange
|
||||
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.
|
||||
@@ -25,9 +27,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
) -> None:
|
||||
"""Initialize PassiveBluetoothDataUpdateCoordinator."""
|
||||
super().__init__(hass, logger, address)
|
||||
super().__init__(hass, logger, address, mode)
|
||||
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
|
||||
|
||||
@callback
|
||||
@@ -65,7 +68,7 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
|
||||
@@ -1,22 +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 home_assistant_bluetooth import BluetoothServiceInfo
|
||||
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
|
||||
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:
|
||||
@@ -62,9 +64,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(hass, logger, address)
|
||||
super().__init__(hass, logger, address, mode)
|
||||
self._processors: list[PassiveBluetoothDataProcessor] = []
|
||||
|
||||
@callback
|
||||
@@ -92,7 +95,7 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
@@ -122,7 +125,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
||||
The processor will call the update_method every time the bluetooth device
|
||||
receives a new advertisement data from the coordinator with the following signature:
|
||||
|
||||
update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate
|
||||
update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate
|
||||
|
||||
As the size of each advertisement is limited, the update_method should
|
||||
return a PassiveBluetoothDataUpdate object that contains only data that
|
||||
@@ -135,7 +138,9 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]],
|
||||
update_method: Callable[
|
||||
[BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T]
|
||||
],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.coordinator: PassiveBluetoothProcessorCoordinator
|
||||
@@ -241,7 +246,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
||||
@callback
|
||||
def async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
|
||||
@@ -4,13 +4,13 @@ from __future__ import annotations
|
||||
import logging
|
||||
import time
|
||||
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
|
||||
from . import (
|
||||
BluetoothCallbackMatcher,
|
||||
BluetoothChange,
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_register_callback,
|
||||
async_track_unavailable,
|
||||
)
|
||||
@@ -27,6 +27,7 @@ class BasePassiveBluetoothCoordinator:
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.hass = hass
|
||||
@@ -36,6 +37,7 @@ class BasePassiveBluetoothCoordinator:
|
||||
self._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
||||
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
||||
self._present = False
|
||||
self.mode = mode
|
||||
self.last_seen = 0.0
|
||||
|
||||
@callback
|
||||
@@ -61,6 +63,7 @@ class BasePassiveBluetoothCoordinator:
|
||||
self.hass,
|
||||
self._async_handle_bluetooth_event,
|
||||
BluetoothCallbackMatcher(address=self.address),
|
||||
self.mode,
|
||||
)
|
||||
self._cancel_track_unavailable = async_track_unavailable(
|
||||
self.hass,
|
||||
@@ -86,7 +89,7 @@ class BasePassiveBluetoothCoordinator:
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""bluetooth usage utility to handle multiple instances."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bleak
|
||||
@@ -10,9 +11,9 @@ ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner
|
||||
|
||||
def install_multiple_bleak_catcher() -> None:
|
||||
"""Wrap the bleak classes to return the shared instance if multiple instances are detected."""
|
||||
bleak.BleakScanner = HaBleakScannerWrapper
|
||||
bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment]
|
||||
|
||||
|
||||
def uninstall_multiple_bleak_catcher() -> None:
|
||||
"""Unwrap the bleak classes."""
|
||||
bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER
|
||||
bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc]
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from uuid import UUID
|
||||
|
||||
from bleak import BleakClient, BleakError
|
||||
from bleak.backends.device import BLEDevice
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
@@ -136,15 +135,12 @@ async def async_setup_scanner( # noqa: C901
|
||||
async def _async_see_update_ble_battery(
|
||||
mac: str,
|
||||
now: datetime,
|
||||
service_info: bluetooth.BluetoothServiceInfo,
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
) -> None:
|
||||
"""Lookup Bluetooth LE devices and update status."""
|
||||
battery = None
|
||||
ble_device: BLEDevice | str = (
|
||||
bluetooth.async_ble_device_from_address(hass, mac) or mac
|
||||
)
|
||||
try:
|
||||
async with BleakClient(ble_device) as client:
|
||||
async with BleakClient(service_info.device) as client:
|
||||
bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID)
|
||||
battery = ord(bat_char)
|
||||
except asyncio.TimeoutError:
|
||||
@@ -165,7 +161,8 @@ async def async_setup_scanner( # noqa: C901
|
||||
|
||||
@callback
|
||||
def _async_update_ble(
|
||||
service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Update from a ble callback."""
|
||||
mac = service_info.address
|
||||
@@ -199,7 +196,9 @@ async def async_setup_scanner( # noqa: C901
|
||||
_async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT)
|
||||
|
||||
cancels = [
|
||||
bluetooth.async_register_callback(hass, _async_update_ble, None),
|
||||
bluetooth.async_register_callback(
|
||||
hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE
|
||||
),
|
||||
async_track_time_interval(hass, _async_refresh_ble, interval),
|
||||
]
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import os
|
||||
from random import SystemRandom
|
||||
from typing import Final, Optional, cast, final
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import hdrs, web
|
||||
import async_timeout
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
@@ -715,7 +715,12 @@ class CameraView(HomeAssistantView):
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
raise web.HTTPUnauthorized()
|
||||
# Attempt with invalid bearer token, raise unauthorized
|
||||
# so ban middleware can handle it.
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized()
|
||||
# Invalid sigAuth or camera access token
|
||||
raise web.HTTPForbidden()
|
||||
|
||||
if not camera.is_on:
|
||||
_LOGGER.debug("Camera is off")
|
||||
|
||||
@@ -103,8 +103,8 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_of_measurement:
|
||||
return self.entity_description.unit_of_measurement
|
||||
if self.entity_description.native_unit_of_measurement:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
return cast(
|
||||
str, self.coordinator.data["units"].get(self.entity_description.key)
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -85,7 +85,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
key="electricity_delivery",
|
||||
key="current_electricity_delivery",
|
||||
name="Power production",
|
||||
obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
|
||||
@@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
name="Low tariff usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity2",
|
||||
name="High tariff usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity1_returned",
|
||||
name="Low tariff return",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity2_returned",
|
||||
name="High tariff return",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity_merged",
|
||||
name="Power usage total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity_returned_merged",
|
||||
name="Power return total",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/day-consumption/electricity1_cost",
|
||||
|
||||
@@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = (
|
||||
sensor.SensorDeviceClass.GAS,
|
||||
)
|
||||
GAS_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR),
|
||||
sensor.SensorDeviceClass.ENERGY: (
|
||||
ENERGY_WATT_HOUR,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
ENERGY_MEGA_WATT_HOUR,
|
||||
),
|
||||
sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
|
||||
}
|
||||
GAS_PRICE_UNITS = tuple(
|
||||
|
||||
@@ -5,12 +5,14 @@ from enocean.utils import combine_hex
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||
from homeassistant.const import CONF_ID, CONF_NAME
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .device import EnOceanEntity
|
||||
|
||||
CONF_CHANNEL = "channel"
|
||||
@@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
def generate_unique_id(dev_id: list[int], channel: int) -> str:
|
||||
"""Generate a valid unique id."""
|
||||
return f"{combine_hex(dev_id)}-{channel}"
|
||||
|
||||
|
||||
def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None:
|
||||
"""Migrate old unique ids to new unique ids."""
|
||||
old_unique_id = f"{combine_hex(dev_id)}"
|
||||
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id)
|
||||
|
||||
if entity_id is not None:
|
||||
new_unique_id = generate_unique_id(dev_id, channel)
|
||||
try:
|
||||
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
except ValueError:
|
||||
LOGGER.warning(
|
||||
"Skip migration of id [%s] to [%s] because it already exists",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id from [%s] to [%s]",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the EnOcean switch platform."""
|
||||
@@ -36,7 +68,8 @@ def setup_platform(
|
||||
dev_id = config.get(CONF_ID)
|
||||
dev_name = config.get(CONF_NAME)
|
||||
|
||||
add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
|
||||
_migrate_to_new_unique_id(hass, dev_id, channel)
|
||||
async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
|
||||
|
||||
|
||||
class EnOceanSwitch(EnOceanEntity, SwitchEntity):
|
||||
@@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity):
|
||||
self._on_state = False
|
||||
self._on_state2 = False
|
||||
self.channel = channel
|
||||
self._attr_unique_id = f"{combine_hex(dev_id)}"
|
||||
self._attr_unique_id = generate_unique_id(dev_id, channel)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "entur_public_transport",
|
||||
"name": "Entur",
|
||||
"documentation": "https://www.home-assistant.io/integrations/entur_public_transport",
|
||||
"requirements": ["enturclient==0.2.3"],
|
||||
"requirements": ["enturclient==0.2.4"],
|
||||
"codeowners": ["@hfurubotten"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["enturclient"]
|
||||
|
||||
@@ -126,6 +126,8 @@ async def async_setup_platform(
|
||||
class EvoClimateEntity(EvoDevice, ClimateEntity):
|
||||
"""Base for an evohome Climate device."""
|
||||
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Climate device."""
|
||||
super().__init__(evo_broker, evo_device)
|
||||
@@ -316,7 +318,6 @@ class EvoController(EvoClimateEntity):
|
||||
|
||||
_attr_icon = "mdi:thermostat"
|
||||
_attr_precision = PRECISION_TENTHS
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize a Honeywell TCC Controller/Location."""
|
||||
|
||||
@@ -6,14 +6,19 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakScanner
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from fjaraskupan import Device, State, device_filter
|
||||
from fjaraskupan import Device, State
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothCallbackMatcher,
|
||||
BluetoothChange,
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_address_present,
|
||||
async_register_callback,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
@@ -66,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]):
|
||||
async def _async_update_data(self) -> State:
|
||||
"""Handle an explicit update request."""
|
||||
if self._refresh_was_scheduled:
|
||||
raise UpdateFailed("No data received within schedule.")
|
||||
if async_address_present(self.hass, self.device.address):
|
||||
return self.device.state
|
||||
raise UpdateFailed(
|
||||
"No data received within schedule, and device is no longer present"
|
||||
)
|
||||
|
||||
await self.device.update()
|
||||
return self.device.state
|
||||
|
||||
def detection_callback(
|
||||
self, ble_device: BLEDevice, advertisement_data: AdvertisementData
|
||||
) -> None:
|
||||
def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||
"""Handle a new announcement of data."""
|
||||
self.device.detection_callback(ble_device, advertisement_data)
|
||||
self.device.detection_callback(service_info.device, service_info.advertisement)
|
||||
self.async_set_updated_data(self.device.state)
|
||||
|
||||
|
||||
@@ -83,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]):
|
||||
class EntryState:
|
||||
"""Store state of config entry."""
|
||||
|
||||
scanner: BleakScanner
|
||||
coordinators: dict[str, Coordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Fjäråskupan from a config entry."""
|
||||
|
||||
scanner = BleakScanner(filters={"DuplicateData": True})
|
||||
|
||||
state = EntryState(scanner, {})
|
||||
state = EntryState({})
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = state
|
||||
|
||||
async def detection_callback(
|
||||
ble_device: BLEDevice, advertisement_data: AdvertisementData
|
||||
def detection_callback(
|
||||
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
|
||||
) -> None:
|
||||
if data := state.coordinators.get(ble_device.address):
|
||||
_LOGGER.debug(
|
||||
"Update: %s %s - %s", ble_device.name, ble_device, advertisement_data
|
||||
)
|
||||
|
||||
data.detection_callback(ble_device, advertisement_data)
|
||||
if change != BluetoothChange.ADVERTISEMENT:
|
||||
return
|
||||
if data := state.coordinators.get(service_info.address):
|
||||
_LOGGER.debug("Update: %s", service_info)
|
||||
data.detection_callback(service_info)
|
||||
else:
|
||||
if not device_filter(ble_device, advertisement_data):
|
||||
return
|
||||
_LOGGER.debug("Detected: %s", service_info)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data
|
||||
)
|
||||
|
||||
device = Device(ble_device)
|
||||
device = Device(service_info.device)
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, ble_device.address)},
|
||||
identifiers={(DOMAIN, service_info.address)},
|
||||
manufacturer="Fjäråskupan",
|
||||
name="Fjäråskupan",
|
||||
)
|
||||
|
||||
coordinator: Coordinator = Coordinator(hass, device, device_info)
|
||||
coordinator.detection_callback(ble_device, advertisement_data)
|
||||
coordinator.detection_callback(service_info)
|
||||
|
||||
state.coordinators[ble_device.address] = coordinator
|
||||
state.coordinators[service_info.address] = coordinator
|
||||
async_dispatcher_send(
|
||||
hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator
|
||||
)
|
||||
|
||||
scanner.register_detection_callback(detection_callback)
|
||||
await scanner.start()
|
||||
|
||||
async def on_hass_stop(event: Event) -> None:
|
||||
await scanner.stop()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||
async_register_callback(
|
||||
hass,
|
||||
detection_callback,
|
||||
BluetoothCallbackMatcher(
|
||||
manufacturer_id=20296,
|
||||
manufacturer_data_start=[79, 68, 70, 74, 65, 82],
|
||||
),
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
)
|
||||
)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
@@ -173,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
await entry_state.scanner.stop()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
"""Config flow for Fjäråskupan integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import async_timeout
|
||||
from bleak import BleakScanner
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from fjaraskupan import device_filter
|
||||
|
||||
from homeassistant.components.bluetooth import async_discovered_service_info
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_flow import register_discovery_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONST_WAIT_TIME = 5.0
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
"""Return if there are devices that can be discovered."""
|
||||
|
||||
event = asyncio.Event()
|
||||
service_infos = async_discovered_service_info(hass)
|
||||
|
||||
def detection(device: BLEDevice, advertisement_data: AdvertisementData):
|
||||
if device_filter(device, advertisement_data):
|
||||
event.set()
|
||||
for service_info in service_infos:
|
||||
if device_filter(service_info.device, service_info.advertisement):
|
||||
return True
|
||||
|
||||
async with BleakScanner(
|
||||
detection_callback=detection,
|
||||
filters={"DuplicateData": True},
|
||||
):
|
||||
try:
|
||||
async with async_timeout.timeout(CONST_WAIT_TIME):
|
||||
await event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
return False
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices)
|
||||
|
||||
@@ -6,5 +6,12 @@
|
||||
"requirements": ["fjaraskupan==1.0.2"],
|
||||
"codeowners": ["@elupus"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "fjaraskupan"]
|
||||
"loggers": ["bleak", "fjaraskupan"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"bluetooth": [
|
||||
{
|
||||
"manufacturer_id": 20296,
|
||||
"manufacturer_data_start": [79, 68, 70, 74, 65, 82]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220727.0"],
|
||||
"requirements": ["home-assistant-frontend==20220802.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.",
|
||||
"title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa"
|
||||
"title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa"
|
||||
},
|
||||
"removed_track_new_yaml": {
|
||||
"description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
|
||||
"title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664"
|
||||
"title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664"
|
||||
},
|
||||
"removed_track_new_yaml": {
|
||||
"description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
@@ -27,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -7,7 +7,7 @@ from govee_ble import GoveeBluetoothDeviceData as DeviceData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfo,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
@@ -24,12 +24,12 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: BluetoothServiceInfo | None = None
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_device: DeviceData | None = None
|
||||
self._discovered_devices: dict[str, str] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfo
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
{
|
||||
"manufacturer_id": 14474,
|
||||
"service_uuid": "00008151-0000-1000-8000-00805f9b34fb"
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 10032,
|
||||
"service_uuid": "00008251-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"requirements": ["govee-ble==0.12.3"],
|
||||
"requirements": ["govee-ble==0.12.6"],
|
||||
"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"]
|
||||
|
||||
@@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# so we use a lock to ensure that only one API request is reaching it at a time:
|
||||
api_lock = asyncio.Lock()
|
||||
|
||||
async def async_init_coordinator(
|
||||
coordinator: GuardianDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a GuardianDataUpdateCoordinator."""
|
||||
await coordinator.async_initialize()
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Set up GuardianDataUpdateCoordinators for the valve controller:
|
||||
valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {}
|
||||
init_valve_controller_tasks = []
|
||||
@@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api
|
||||
] = GuardianDataUpdateCoordinator(
|
||||
hass,
|
||||
entry=entry,
|
||||
client=client,
|
||||
api_name=api,
|
||||
api_coro=api_coro,
|
||||
api_lock=api_lock,
|
||||
valve_controller_uid=entry.data[CONF_UID],
|
||||
)
|
||||
init_valve_controller_tasks.append(coordinator.async_refresh())
|
||||
init_valve_controller_tasks.append(async_init_coordinator(coordinator))
|
||||
|
||||
await asyncio.gather(*init_valve_controller_tasks)
|
||||
|
||||
@@ -352,6 +360,7 @@ class PairedSensorManager:
|
||||
|
||||
coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator(
|
||||
self._hass,
|
||||
entry=self._entry,
|
||||
client=self._client,
|
||||
api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}",
|
||||
api_coro=lambda: cast(
|
||||
@@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]):
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity.
|
||||
"""Update the entity's underlying data.
|
||||
|
||||
This should be extended by Guardian platforms.
|
||||
"""
|
||||
|
||||
@@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity):
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity."""
|
||||
"""Update the entity's underlying data."""
|
||||
if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED:
|
||||
self._attr_is_on = self.coordinator.data["wet"]
|
||||
elif self.entity_description.key == SENSOR_KIND_MOVED:
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.button import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity):
|
||||
raise HomeAssistantError(
|
||||
f'Error while pressing button "{self.entity_id}": {err}'
|
||||
) from err
|
||||
|
||||
async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested)
|
||||
|
||||
@@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity):
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity."""
|
||||
"""Update the entity's underlying data."""
|
||||
if self.entity_description.key == SENSOR_KIND_BATTERY:
|
||||
self._attr_native_value = self.coordinator.data["battery"]
|
||||
elif self.entity_description.key == SENSOR_KIND_TEMPERATURE:
|
||||
@@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity):
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity."""
|
||||
"""Update the entity's underlying data."""
|
||||
if self.entity_description.key == SENSOR_KIND_TEMPERATURE:
|
||||
self._attr_native_value = self.coordinator.data["temperature"]
|
||||
elif self.entity_description.key == SENSOR_KIND_UPTIME:
|
||||
|
||||
@@ -9,21 +9,28 @@ from typing import Any, cast
|
||||
from aioguardian import Client
|
||||
from aioguardian.errors import GuardianError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}"
|
||||
|
||||
|
||||
class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
|
||||
"""Define an extended DataUpdateCoordinator with some Guardian goodies."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
api_name: str,
|
||||
api_coro: Callable[..., Awaitable],
|
||||
@@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
|
||||
self._api_coro = api_coro
|
||||
self._api_lock = api_lock
|
||||
self._client = client
|
||||
self._signal_handler_unsubs: list[Callable[..., None]] = []
|
||||
|
||||
self.config_entry = entry
|
||||
self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format(
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Execute a "locked" API request against the valve controller."""
|
||||
@@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
|
||||
except GuardianError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
return cast(dict[str, Any], resp["data"])
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
|
||||
@callback
|
||||
def async_reboot_requested() -> None:
|
||||
"""Respond to a reboot request."""
|
||||
self.last_update_success = False
|
||||
self.async_update_listeners()
|
||||
|
||||
self._signal_handler_unsubs.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass, self.signal_reboot_requested, async_reboot_requested
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_teardown() -> None:
|
||||
"""Tear the coordinator down appropriately."""
|
||||
for unsub in self._signal_handler_unsubs:
|
||||
unsub()
|
||||
|
||||
self.config_entry.async_on_unload(async_teardown)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Home Connect component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config[DOMAIN][CONF_CLIENT_ID],
|
||||
config[DOMAIN][CONF_CLIENT_SECRET],
|
||||
),
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"Configuration of Home Connect integration in YAML is deprecated and "
|
||||
"will be removed in a future release; Your existing OAuth "
|
||||
"Application Credentials have been imported into the UI "
|
||||
"automatically and can be safely removed from your "
|
||||
"configuration.yaml file"
|
||||
)
|
||||
if DOMAIN in config:
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config[DOMAIN][CONF_CLIENT_ID],
|
||||
config[DOMAIN][CONF_CLIENT_SECRET],
|
||||
),
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"Configuration of Home Connect integration in YAML is deprecated and "
|
||||
"will be removed in a future release; Your existing OAuth "
|
||||
"Application Credentials have been imported into the UI "
|
||||
"automatically and can be safely removed from your "
|
||||
"configuration.yaml file"
|
||||
)
|
||||
|
||||
async def _async_service_program(call, method):
|
||||
"""Execute calls to services taking a program."""
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util.yaml import parse_yaml
|
||||
@@ -75,7 +76,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
learn_more_url=alert.alert_url,
|
||||
issue_domain=alert.integration,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="alert",
|
||||
translation_placeholders={
|
||||
@@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
coordinator = AlertUpdateCoordinator(hass)
|
||||
coordinator.async_add_listener(async_schedule_update_alerts)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
async def initial_refresh(hass: HomeAssistant) -> None:
|
||||
await coordinator.async_refresh()
|
||||
|
||||
async_at_start(hass, initial_refresh)
|
||||
|
||||
return True
|
||||
|
||||
@@ -112,7 +117,6 @@ class IntegrationAlert:
|
||||
integration: str
|
||||
filename: str
|
||||
date_updated: str | None
|
||||
alert_url: str | None
|
||||
|
||||
@property
|
||||
def issue_id(self) -> str:
|
||||
@@ -147,7 +151,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]])
|
||||
result = {}
|
||||
|
||||
for alert in alerts:
|
||||
if "alert_url" not in alert or "integrations" not in alert:
|
||||
if "integrations" not in alert:
|
||||
continue
|
||||
|
||||
if "homeassistant" in alert:
|
||||
@@ -177,7 +181,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]])
|
||||
integration=integration["package"],
|
||||
filename=alert["filename"],
|
||||
date_updated=alert.get("date_updated"),
|
||||
alert_url=alert["alert_url"],
|
||||
)
|
||||
|
||||
result[integration_alert.issue_id] = integration_alert
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "homeassistant_alerts",
|
||||
"name": "Home Assistant alerts",
|
||||
"name": "Home Assistant Alerts",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"alert": {
|
||||
"description": "{description}",
|
||||
"title": "{title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,16 +20,19 @@ 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
|
||||
from homeassistant.helpers.service_info import bluetooth
|
||||
|
||||
from .connection import HKDevice
|
||||
from .const import DOMAIN, KNOWN_DEVICES
|
||||
from .storage import async_get_entity_storage
|
||||
from .utils import async_get_controller
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components import bluetooth
|
||||
|
||||
|
||||
HOMEKIT_DIR = ".homekit"
|
||||
HOMEKIT_BRIDGE_DOMAIN = "homekit"
|
||||
|
||||
@@ -75,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:
|
||||
@@ -112,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"]
|
||||
@@ -139,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():
|
||||
@@ -164,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)
|
||||
@@ -172,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(
|
||||
@@ -359,7 +373,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self._async_step_pair_show_form()
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: bluetooth.BluetoothServiceInfo
|
||||
self, discovery_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED:
|
||||
@@ -407,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
|
||||
@@ -425,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"]
|
||||
@@ -504,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()
|
||||
@@ -526,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),
|
||||
@@ -566,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.5"],
|
||||
"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:
|
||||
|
||||
@@ -32,7 +32,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller:
|
||||
|
||||
controller = Controller(
|
||||
async_zeroconf_instance=async_zeroconf_instance,
|
||||
bleak_scanner_instance=bleak_scanner_instance,
|
||||
bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
hass.data[CONTROLLER] = controller
|
||||
|
||||
@@ -33,7 +33,7 @@ def _get_file_path(
|
||||
return None
|
||||
if filepath.is_file():
|
||||
return filepath
|
||||
raise HTTPNotFound
|
||||
raise FileNotFoundError
|
||||
|
||||
|
||||
class CachingStaticResource(StaticResource):
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
@@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -7,7 +7,7 @@ from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfo,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
@@ -24,12 +24,12 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: BluetoothServiceInfo | None = None
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_device: DeviceData | None = None
|
||||
self._discovered_devices: dict[str, str] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfo
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
|
||||
@@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Utility methods for the Insteon platform."""
|
||||
from pyinsteon.device_types import (
|
||||
AccessControl_Morningstar,
|
||||
ClimateControl_Thermostat,
|
||||
ClimateControl_WirelessThermostat,
|
||||
DimmableLightingControl,
|
||||
@@ -12,6 +13,7 @@ from pyinsteon.device_types import (
|
||||
DimmableLightingControl_OutletLinc,
|
||||
DimmableLightingControl_SwitchLinc,
|
||||
DimmableLightingControl_ToggleLinc,
|
||||
EnergyManagement_LoadController,
|
||||
GeneralController_ControlLinc,
|
||||
GeneralController_MiniRemote_4,
|
||||
GeneralController_MiniRemote_8,
|
||||
@@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE
|
||||
from homeassistant.components.cover import DOMAIN as COVER
|
||||
from homeassistant.components.fan import DOMAIN as FAN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT
|
||||
from homeassistant.components.lock import DOMAIN as LOCK
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
|
||||
from .const import ON_OFF_EVENTS
|
||||
|
||||
DEVICE_PLATFORM = {
|
||||
AccessControl_Morningstar: {LOCK: [1]},
|
||||
DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]},
|
||||
DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]},
|
||||
DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]},
|
||||
@@ -67,6 +71,7 @@ DEVICE_PLATFORM = {
|
||||
DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
|
||||
DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
|
||||
DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
|
||||
EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]},
|
||||
GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]},
|
||||
GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)},
|
||||
GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)},
|
||||
|
||||
49
homeassistant/components/insteon/lock.py
Normal file
49
homeassistant/components/insteon/lock.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Support for INSTEON locks."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SIGNAL_ADD_ENTITIES
|
||||
from .insteon_entity import InsteonEntity
|
||||
from .utils import async_add_insteon_entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Insteon locks from a config entry."""
|
||||
|
||||
@callback
|
||||
def async_add_insteon_lock_entities(discovery_info=None):
|
||||
"""Add the Insteon entities for the platform."""
|
||||
async_add_insteon_entities(
|
||||
hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info
|
||||
)
|
||||
|
||||
signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}"
|
||||
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
|
||||
async_add_insteon_lock_entities()
|
||||
|
||||
|
||||
class InsteonLockEntity(InsteonEntity, LockEntity):
|
||||
"""A Class for an Insteon lock entity."""
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
"""Return the boolean response if the node is on."""
|
||||
return bool(self._insteon_device_group.value)
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the device."""
|
||||
await self._insteon_device.async_lock()
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the device."""
|
||||
await self._insteon_device.async_unlock()
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/insteon",
|
||||
"dependencies": ["http", "websocket_api"],
|
||||
"requirements": [
|
||||
"pyinsteon==1.1.3",
|
||||
"pyinsteon==1.2.0",
|
||||
"insteon-frontend-home-assistant==0.2.0"
|
||||
],
|
||||
"codeowners": ["@teharris1"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -363,14 +363,18 @@ class JellyfinSource(MediaSource):
|
||||
|
||||
def _media_mime_type(media_item: dict[str, Any]) -> str:
|
||||
"""Return the mime type of a media item."""
|
||||
if not media_item[ITEM_KEY_MEDIA_SOURCES]:
|
||||
if not media_item.get(ITEM_KEY_MEDIA_SOURCES):
|
||||
raise BrowseError("Unable to determine mime type for item without media source")
|
||||
|
||||
media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0]
|
||||
|
||||
if MEDIA_SOURCE_KEY_PATH not in media_source:
|
||||
raise BrowseError("Unable to determine mime type for media source without path")
|
||||
|
||||
path = media_source[MEDIA_SOURCE_KEY_PATH]
|
||||
mime_type, _ = mimetypes.guess_type(path)
|
||||
|
||||
if mime_type is not None:
|
||||
return mime_type
|
||||
if mime_type is None:
|
||||
raise BrowseError(f"Unable to determine mime type for path {path}")
|
||||
|
||||
raise BrowseError(f"Unable to determine mime type for path {path}")
|
||||
return mime_type
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "KNX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||
"requirements": ["xknx==0.22.0"],
|
||||
"requirements": ["xknx==0.22.1"],
|
||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_push",
|
||||
|
||||
20
homeassistant/components/lacrosse_view/translations/de.json
Normal file
20
homeassistant/components/lacrosse_view/translations/de.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
"no_locations": "Keine Standorte gefunden",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Passwort",
|
||||
"username": "Benutzername"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
homeassistant/components/lacrosse_view/translations/en.json
Normal file
20
homeassistant/components/lacrosse_view/translations/en.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"no_locations": "No locations found",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
homeassistant/components/lacrosse_view/translations/it.json
Normal file
20
homeassistant/components/lacrosse_view/translations/it.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"no_locations": "Nessuna localit\u00e0 trovata",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
|
||||
"no_locations": "Nenhum local encontrado",
|
||||
"unknown": "Erro inesperado"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Senha",
|
||||
"username": "Nome de usu\u00e1rio"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
|
||||
"no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19",
|
||||
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u5bc6\u78bc",
|
||||
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity):
|
||||
self._port = port
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
self._name = None
|
||||
self._volume = 0
|
||||
self._volume_min = 0
|
||||
self._volume_max = 0
|
||||
@@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity):
|
||||
elif response["msg"] == "SPK_LIST_VIEW_INFO":
|
||||
if "i_vol" in data:
|
||||
self._volume = data["i_vol"]
|
||||
if "s_user_name" in data:
|
||||
self._name = data["s_user_name"]
|
||||
if "i_vol_min" in data:
|
||||
self._volume_min = data["i_vol_min"]
|
||||
if "i_vol_max" in data:
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"existing_instance_updated": "Updated existing configuration.",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured",
|
||||
"existing_instance_updated": "Updated existing configuration."
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
|
||||
@@ -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__(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user