forked from home-assistant/core
Compare commits
122 Commits
2024.2.0b7
...
2024.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aa14e20d1 | ||
|
|
b55b2c8da3 | ||
|
|
8c05ebd031 | ||
|
|
34a3e88e0d | ||
|
|
bf002ac0b0 | ||
|
|
6f529a2c77 | ||
|
|
e5db7278e1 | ||
|
|
cdf67e9bb5 | ||
|
|
393359a546 | ||
|
|
9309e38302 | ||
|
|
479ecc8b94 | ||
|
|
ec7950aeda | ||
|
|
c763483049 | ||
|
|
fe84e7a576 | ||
|
|
5ba31290b8 | ||
|
|
de619e4ddc | ||
|
|
56ceadaeeb | ||
|
|
da61564f82 | ||
|
|
003673cd29 | ||
|
|
da6c571e65 | ||
|
|
159fab7025 | ||
|
|
96a10e76b8 | ||
|
|
e7068ae134 | ||
|
|
6a0c3f1b4f | ||
|
|
a0ae18a1b6 | ||
|
|
ad761bb2de | ||
|
|
edb69fb095 | ||
|
|
58b28e6df1 | ||
|
|
973a13abfa | ||
|
|
2a51377cef | ||
|
|
87bd67656b | ||
|
|
c79bc17d17 | ||
|
|
54270df217 | ||
|
|
5a87cde71e | ||
|
|
e825bcc282 | ||
|
|
b54a3170f0 | ||
|
|
349d8f5c28 | ||
|
|
cfd1f7809f | ||
|
|
5f9cc2fec1 | ||
|
|
58d46f6dec | ||
|
|
74ea9e24df | ||
|
|
437a2a829f | ||
|
|
f5884c6279 | ||
|
|
e4382a494c | ||
|
|
56ff767969 | ||
|
|
4a18f592c6 | ||
|
|
7ff2f376d4 | ||
|
|
a18918bb73 | ||
|
|
49e5709826 | ||
|
|
c665903f9d | ||
|
|
de44af2948 | ||
|
|
95a800b6bc | ||
|
|
a9e9ec2c3d | ||
|
|
7309c3c290 | ||
|
|
f48d70654b | ||
|
|
a9b3c2e2b5 | ||
|
|
19349e1779 | ||
|
|
e320d715c7 | ||
|
|
44c9ea68eb | ||
|
|
dbfee24eb7 | ||
|
|
3b7271d597 | ||
|
|
9dbf84228e | ||
|
|
9e47d03086 | ||
|
|
f63aaf8b5a | ||
|
|
8375fc235d | ||
|
|
3030870de0 | ||
|
|
f61c70b686 | ||
|
|
e720b398d2 | ||
|
|
bd21490a57 | ||
|
|
75b308c1aa | ||
|
|
881707e1fe | ||
|
|
2ca3bbaea5 | ||
|
|
ea4bdbb3a0 | ||
|
|
40cfc31dcb | ||
|
|
031aadff00 | ||
|
|
27691b7d48 | ||
|
|
fe94107af7 | ||
|
|
d784a76d32 | ||
|
|
ebb1912617 | ||
|
|
8c605c29c3 | ||
|
|
74a75e709f | ||
|
|
2103875ff7 | ||
|
|
5c83b774bb | ||
|
|
2c870f9da9 | ||
|
|
40adb3809f | ||
|
|
8aa1242221 | ||
|
|
8569ddc5f9 | ||
|
|
7032415528 | ||
|
|
d099fb2a26 | ||
|
|
35fad52913 | ||
|
|
c170132827 | ||
|
|
439f82a4ec | ||
|
|
2481d14632 | ||
|
|
3cf826dc93 | ||
|
|
e25ddf9650 | ||
|
|
8d79ac67f5 | ||
|
|
5025c15165 | ||
|
|
ffd5e04a29 | ||
|
|
9fcdfd1b16 | ||
|
|
c1e5b2e6cc | ||
|
|
31c0d21204 | ||
|
|
3ba63fc78f | ||
|
|
0395315267 | ||
|
|
6b354457c2 | ||
|
|
df88335370 | ||
|
|
4c6c5ee63d | ||
|
|
65476914ed | ||
|
|
d30a2e3611 | ||
|
|
eb510e3630 | ||
|
|
532df5b5f1 | ||
|
|
1534f99c80 | ||
|
|
a19aa9595a | ||
|
|
e3191d098f | ||
|
|
cc36071612 | ||
|
|
2d90ee8237 | ||
|
|
16266703df | ||
|
|
dd2cc52119 | ||
|
|
c48c8c25fa | ||
|
|
83a5659d57 | ||
|
|
3183cd346d | ||
|
|
5930c841d7 | ||
|
|
f05ba22b5c |
@@ -485,6 +485,7 @@ omit =
|
||||
homeassistant/components/gpsd/sensor.py
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/growatt_server/const.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/sensor_types/*
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
|
||||
@@ -786,8 +786,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/media_source/ @hunterjm
|
||||
/tests/components/media_source/ @hunterjm
|
||||
/homeassistant/components/mediaroom/ @dgomes
|
||||
/homeassistant/components/melcloud/ @vilppuvuorinen
|
||||
/tests/components/melcloud/ @vilppuvuorinen
|
||||
/homeassistant/components/melissa/ @kennedyshead
|
||||
/tests/components/melissa/ @kennedyshead
|
||||
/homeassistant/components/melnor/ @vanstinator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "tplink",
|
||||
"name": "TP-Link",
|
||||
"integrations": ["tplink", "tplink_omada", "tplink_lte"],
|
||||
"integrations": ["tplink", "tplink_omada", "tplink_lte", "tplink_tapo"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
||||
@@ -3,18 +3,46 @@ from __future__ import annotations
|
||||
|
||||
import amberelectric
|
||||
from amberelectric.api import amber_api
|
||||
from amberelectric.model.site import Site
|
||||
from amberelectric.model.site import Site, SiteStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
|
||||
def generate_site_selector_name(site: Site) -> str:
|
||||
"""Generate the name to show in the site drop down in the configuration flow."""
|
||||
if site.status == SiteStatus.CLOSED:
|
||||
return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return]
|
||||
if site.status == SiteStatus.PENDING:
|
||||
return site.nmi + " (Pending)" # type: ignore[no-any-return]
|
||||
return site.nmi # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def filter_sites(sites: list[Site]) -> list[Site]:
|
||||
"""Deduplicates the list of sites."""
|
||||
filtered: list[Site] = []
|
||||
filtered_nmi: set[str] = set()
|
||||
|
||||
for site in sorted(sites, key=lambda site: site.status.value):
|
||||
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
|
||||
filtered.append(site)
|
||||
filtered_nmi.add(site.nmi)
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
@@ -31,7 +59,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
api: amber_api.AmberApi = amber_api.AmberApi.create(configuration)
|
||||
|
||||
try:
|
||||
sites: list[Site] = api.get_sites()
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
if len(sites) == 0:
|
||||
self._errors[CONF_API_TOKEN] = "no_site"
|
||||
return None
|
||||
@@ -86,38 +114,31 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert self._sites is not None
|
||||
assert self._api_token is not None
|
||||
|
||||
api_token = self._api_token
|
||||
if user_input is not None:
|
||||
site_nmi = user_input[CONF_SITE_NMI]
|
||||
sites = [site for site in self._sites if site.nmi == site_nmi]
|
||||
site = sites[0]
|
||||
site_id = site.id
|
||||
site_id = user_input[CONF_SITE_ID]
|
||||
name = user_input.get(CONF_SITE_NAME, site_id)
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
CONF_SITE_ID: site_id,
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: site.nmi,
|
||||
},
|
||||
data={CONF_SITE_ID: site_id, CONF_API_TOKEN: self._api_token},
|
||||
)
|
||||
|
||||
user_input = {
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: "",
|
||||
CONF_SITE_NAME: "",
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="site",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
|
||||
): vol.In([site.nmi for site in self._sites]),
|
||||
vol.Optional(
|
||||
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
|
||||
): str,
|
||||
vol.Required(CONF_SITE_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
value=site.id,
|
||||
label=generate_site_selector_name(site),
|
||||
)
|
||||
for site in self._sites
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_SITE_NAME): str,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.const import Platform
|
||||
DOMAIN = "amberelectric"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
CONF_SITE_NMI = "site_nmi"
|
||||
|
||||
ATTRIBUTION = "Data provided by Amber Electric"
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["amberelectric"],
|
||||
"requirements": ["amberelectric==1.0.4"]
|
||||
"requirements": ["amberelectric==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_homeassistant_analytics import HomeassistantAnalyticsClient
|
||||
from python_homeassistant_analytics import (
|
||||
HomeassistantAnalyticsClient,
|
||||
HomeassistantAnalyticsConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
|
||||
@@ -28,7 +32,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Homeassistant Analytics from a config entry."""
|
||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||
|
||||
integrations = await client.get_integrations()
|
||||
try:
|
||||
integrations = await client.get_integrations()
|
||||
except HomeassistantAnalyticsConnectionError as ex:
|
||||
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
||||
|
||||
names = {}
|
||||
for integration in entry.options[CONF_TRACKED_INTEGRATIONS]:
|
||||
|
||||
@@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
self._async_abort_entries_match()
|
||||
if user_input:
|
||||
return self.async_create_entry(
|
||||
title="Home Assistant Analytics Insights", data={}, options=user_input
|
||||
)
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="Home Assistant Analytics Insights",
|
||||
data={},
|
||||
options={
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
client = HomeassistantAnalyticsClient(
|
||||
session=async_get_clientsession(self.hass)
|
||||
@@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(custom_integrations),
|
||||
multiple=True,
|
||||
@@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
client = HomeassistantAnalyticsClient(
|
||||
session=async_get_clientsession(self.hass)
|
||||
@@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
errors=errors,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(custom_integrations),
|
||||
multiple=True,
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -93,6 +94,7 @@ class HomeassistantAnalyticsSensor(
|
||||
"""Home Assistant Analytics Sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
entity_description: AnalyticsSensorEntityDescription
|
||||
|
||||
|
||||
@@ -5,24 +5,39 @@
|
||||
"data": {
|
||||
"tracked_integrations": "Integrations",
|
||||
"tracked_custom_integrations": "Custom integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_integrations": "Select the integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "You must select at least one integration to track"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]"
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.6"]
|
||||
"requirements": ["py-aosmith==1.0.8"]
|
||||
}
|
||||
|
||||
@@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin):
|
||||
device = self.get_device_detail(device_id)
|
||||
activities = activities_from_pubnub_message(device, date_time, message)
|
||||
activity_stream = self.activity_stream
|
||||
if activities:
|
||||
activity_stream.async_process_newer_device_activities(activities)
|
||||
if activities and activity_stream.async_process_newer_device_activities(
|
||||
activities
|
||||
):
|
||||
self.async_signal_device_id_update(device.device_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
activity_stream.async_schedule_house_id_refresh(device.house_id)
|
||||
|
||||
@callback
|
||||
def async_stop(self) -> None:
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.1"]
|
||||
"requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BringDataUpdateCoordinator
|
||||
@@ -29,14 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
bring = Bring(email, password)
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
session = async_get_clientsession(hass)
|
||||
bring = Bring(email, password, sessionAsync=session)
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(login_and_load_lists)
|
||||
await bring.loginAsync()
|
||||
await bring.loadListsAsync()
|
||||
except BringRequestException as e:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting for email '{email}'"
|
||||
|
||||
@@ -11,6 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
@@ -48,14 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
|
||||
|
||||
def login_and_load_lists() -> None:
|
||||
bring.login()
|
||||
bring.loadLists()
|
||||
session = async_get_clientsession(self.hass)
|
||||
bring = Bring(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD], sessionAsync=session
|
||||
)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(login_and_load_lists)
|
||||
await bring.loginAsync()
|
||||
await bring.loadListsAsync()
|
||||
except BringRequestException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except BringAuthException:
|
||||
|
||||
@@ -40,9 +40,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, BringData]:
|
||||
try:
|
||||
lists_response = await self.hass.async_add_executor_job(
|
||||
self.bring.loadLists
|
||||
)
|
||||
lists_response = await self.bring.loadListsAsync()
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
|
||||
except BringParseException as e:
|
||||
@@ -51,9 +49,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
list_dict = {}
|
||||
for lst in lists_response["lists"]:
|
||||
try:
|
||||
items = await self.hass.async_add_executor_job(
|
||||
self.bring.getItems, lst["listUuid"]
|
||||
)
|
||||
items = await self.bring.getItemsAsync(lst["listUuid"])
|
||||
except BringRequestException as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from bring"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-bring-api==2.0.0"]
|
||||
"requirements": ["python-bring-api==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -91,11 +91,8 @@ class BringTodoListEntity(
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
self.bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
await self.coordinator.bring.saveItemAsync(
|
||||
self.bring_list["listUuid"], item.summary, item.description or ""
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to save todo item for bring") from e
|
||||
@@ -126,16 +123,14 @@ class BringTodoListEntity(
|
||||
assert item.uid
|
||||
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
|
||||
elif item.summary == item.uid:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.updateItem,
|
||||
await self.coordinator.bring.updateItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
item.description or "",
|
||||
@@ -144,13 +139,11 @@ class BringTodoListEntity(
|
||||
raise HomeAssistantError("Unable to update todo item for bring") from e
|
||||
else:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem,
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.saveItem,
|
||||
await self.coordinator.bring.saveItemAsync(
|
||||
bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
@@ -164,8 +157,8 @@ class BringTodoListEntity(
|
||||
"""Delete an item from the To-do list."""
|
||||
for uid in uids:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid
|
||||
await self.coordinator.bring.removeItemAsync(
|
||||
self.bring_list["listUuid"], uid
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to delete todo item for bring") from e
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Intents for the client integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -36,24 +37,34 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
if not entities:
|
||||
raise intent.IntentHandleError("No climate entities")
|
||||
|
||||
if "area" in slots:
|
||||
# Filter by area
|
||||
area_name = slots["area"]["value"]
|
||||
name_slot = slots.get("name", {})
|
||||
entity_name: str | None = name_slot.get("value")
|
||||
entity_text: str | None = name_slot.get("text")
|
||||
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
|
||||
if area_id:
|
||||
# Filter by area and optionally name
|
||||
area_name = area_slot.get("text")
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, area_name=area_name, domains=[DOMAIN]
|
||||
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
elif "name" in slots:
|
||||
elif entity_name:
|
||||
# Filter by name
|
||||
entity_name = slots["name"]["value"]
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, domains=[DOMAIN]
|
||||
):
|
||||
@@ -61,7 +72,12 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No climate entity named {entity_name}")
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
else:
|
||||
|
||||
@@ -8,6 +8,7 @@ from aioelectricitymaps import (
|
||||
ElectricityMaps,
|
||||
ElectricityMapsError,
|
||||
ElectricityMapsInvalidTokenError,
|
||||
ElectricityMapsNoDataError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -151,6 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await fetch_latest_carbon_intensity(self.hass, em, data)
|
||||
except ElectricityMapsInvalidTokenError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ElectricityMapsNoDataError:
|
||||
errors["base"] = "no_data"
|
||||
except ElectricityMapsError:
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioelectricitymaps"],
|
||||
"requirements": ["aioelectricitymaps==0.3.0"]
|
||||
"requirements": ["aioelectricitymaps==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -28,12 +28,9 @@
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "API Ratelimit exceeded"
|
||||
"no_data": "No data is available for the location you have selected."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Support for climates."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE, SLEEP_BETWEEN_CALLS
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
@@ -191,7 +190,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.MANUAL
|
||||
)
|
||||
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.SET, target_temp
|
||||
)
|
||||
@@ -203,7 +201,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaAction.ON
|
||||
)
|
||||
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, MODE_TO_ACTION[hvac_mode]
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.8.2"]
|
||||
"requirements": ["aiocomelit==0.8.3"]
|
||||
}
|
||||
|
||||
@@ -223,22 +223,22 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Check if a trigger matched
|
||||
if isinstance(result, SentenceTriggerResult):
|
||||
# Gather callback responses in parallel
|
||||
trigger_responses = await asyncio.gather(
|
||||
*(
|
||||
self._trigger_sentences[trigger_id].callback(
|
||||
result.sentence, trigger_result
|
||||
)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
trigger_callbacks = [
|
||||
self._trigger_sentences[trigger_id].callback(
|
||||
result.sentence, trigger_result
|
||||
)
|
||||
)
|
||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||
]
|
||||
|
||||
# Use last non-empty result as response.
|
||||
#
|
||||
# There may be multiple copies of a trigger running when editing in
|
||||
# the UI, so it's critical that we filter out empty responses here.
|
||||
response_text: str | None = None
|
||||
for trigger_response in trigger_responses:
|
||||
response_text = response_text or trigger_response
|
||||
for trigger_future in asyncio.as_completed(trigger_callbacks):
|
||||
if trigger_response := await trigger_future:
|
||||
response_text = trigger_response
|
||||
break
|
||||
|
||||
# Convert to conversation result
|
||||
response = intent.IntentResponse(language=language)
|
||||
@@ -316,6 +316,20 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.DuplicateNamesMatchedError as duplicate_names_error:
|
||||
# Intent was valid, but two or more entities with the same name matched.
|
||||
(
|
||||
error_response_type,
|
||||
error_response_args,
|
||||
) = _get_duplicate_names_matched_response(duplicate_names_error)
|
||||
return _make_error_result(
|
||||
language,
|
||||
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||
self._get_error_text(
|
||||
error_response_type, lang_intents, **error_response_args
|
||||
),
|
||||
conversation_id,
|
||||
)
|
||||
except intent.IntentHandleError:
|
||||
# Intent was valid and entities matched constraints, but an error
|
||||
# occurred during handling.
|
||||
@@ -724,7 +738,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if async_should_expose(self.hass, DOMAIN, state.entity_id)
|
||||
]
|
||||
|
||||
# Gather exposed entity names
|
||||
# Gather exposed entity names.
|
||||
#
|
||||
# NOTE: We do not pass entity ids in here because multiple entities may
|
||||
# have the same name. The intent matcher doesn't gather all matching
|
||||
# values for a list, just the first. So we will need to match by name no
|
||||
# matter what.
|
||||
entity_names = []
|
||||
for state in states:
|
||||
# Checked against "requires_context" and "excludes_context" in hassil
|
||||
@@ -740,7 +759,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
if not entity:
|
||||
# Default name
|
||||
entity_names.append((state.name, state.entity_id, context))
|
||||
entity_names.append((state.name, state.name, context))
|
||||
continue
|
||||
|
||||
if entity.aliases:
|
||||
@@ -748,12 +767,15 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if not alias.strip():
|
||||
continue
|
||||
|
||||
entity_names.append((alias, state.entity_id, context))
|
||||
entity_names.append((alias, alias, context))
|
||||
|
||||
# Default name
|
||||
entity_names.append((state.name, state.entity_id, context))
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
# Expose all areas
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
# share the same name or alias.
|
||||
areas = ar.async_get(self.hass)
|
||||
area_names = []
|
||||
for area in areas.async_list_areas():
|
||||
@@ -984,6 +1006,20 @@ def _get_no_states_matched_response(
|
||||
return ErrorKey.NO_INTENT, {}
|
||||
|
||||
|
||||
def _get_duplicate_names_matched_response(
|
||||
duplicate_names_error: intent.DuplicateNamesMatchedError,
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when intent returns duplicate matches."""
|
||||
|
||||
if duplicate_names_error.area:
|
||||
return ErrorKey.DUPLICATE_ENTITIES_IN_AREA, {
|
||||
"entity": duplicate_names_error.name,
|
||||
"area": duplicate_names_error.area,
|
||||
}
|
||||
|
||||
return ErrorKey.DUPLICATE_ENTITIES, {"entity": duplicate_names_error.name}
|
||||
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
"""Collect list reference names recursively."""
|
||||
if isinstance(expression, Sequence):
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["async-upnp-client==0.38.1"],
|
||||
"requirements": ["async-upnp-client==0.38.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"]
|
||||
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"]
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ class EcowittEntity(Entity):
|
||||
"""Update the state on callback."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.ecowitt.update_cb.append(_update_state) # type: ignore[arg-type] # upstream bug
|
||||
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) # type: ignore[arg-type] # upstream bug
|
||||
self.ecowitt.update_cb.append(_update_state)
|
||||
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aioecowitt==2023.5.0"]
|
||||
"requirements": ["aioecowitt==2024.2.1"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -398,22 +398,30 @@ async def async_wait_for_elk_to_sync(
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
def _getelk(service: ServiceCall) -> Elk:
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_word(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.speak_phrase(service.data["number"])
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_getelk(service).panel.set_time(dt_util.now())
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
|
||||
@@ -88,9 +88,9 @@ class EpionSensor(CoordinatorEntity[EpionCoordinator], SensorEntity):
|
||||
super().__init__(coordinator)
|
||||
self._epion_device_id = epion_device_id
|
||||
self.entity_description = description
|
||||
self.unique_id = f"{epion_device_id}_{description.key}"
|
||||
self._attr_unique_id = f"{epion_device_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._epion_device_id)},
|
||||
identifiers={(DOMAIN, epion_device_id)},
|
||||
manufacturer="Epion",
|
||||
name=self.device.get("deviceName"),
|
||||
sw_version=self.device.get("fwVersion"),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==21.0.1",
|
||||
"aioesphomeapi==21.0.2",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==0.4.1"
|
||||
],
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"requirements": ["evohome-async==0.4.17"]
|
||||
"requirements": ["evohome-async==0.4.19"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from aioflo.api import API
|
||||
from aioflo.errors import RequestError
|
||||
from orjson import JSONDecodeError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -46,7 +47,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=
|
||||
await self._update_device()
|
||||
await self._update_consumption_data()
|
||||
self._failure_count = 0
|
||||
except (RequestError, TimeoutError) as error:
|
||||
except (RequestError, TimeoutError, JSONDecodeError) as error:
|
||||
self._failure_count += 1
|
||||
if self._failure_count > 3:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
from .router import get_api
|
||||
from .router import get_api, get_hosts_list_if_supported
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,7 +69,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check permissions
|
||||
await fbx.system.get_config()
|
||||
await fbx.lan.get_hosts_list()
|
||||
await get_hosts_list_if_supported(fbx)
|
||||
|
||||
# Close connection
|
||||
await fbx.close()
|
||||
|
||||
@@ -64,6 +64,33 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
|
||||
return Freepybox(APP_DESC, token_file, API_VERSION)
|
||||
|
||||
|
||||
async def get_hosts_list_if_supported(
|
||||
fbx_api: Freepybox,
|
||||
) -> tuple[bool, list[dict[str, Any]]]:
|
||||
"""Hosts list is not supported when freebox is configured in bridge mode."""
|
||||
supports_hosts: bool = True
|
||||
fbx_devices: list[dict[str, Any]] = []
|
||||
try:
|
||||
fbx_devices = await fbx_api.lan.get_hosts_list() or []
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(matcher := re.search(r"Request failed \(APIResponse: (.+)\)", str(err)))
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
|
||||
return supports_hosts, fbx_devices
|
||||
|
||||
|
||||
class FreeboxRouter:
|
||||
"""Representation of a Freebox router."""
|
||||
|
||||
@@ -111,27 +138,9 @@ class FreeboxRouter:
|
||||
|
||||
# Access to Host list not available in bridge mode, API return error_code 'nodev'
|
||||
if self.supports_hosts:
|
||||
try:
|
||||
fbx_devices = await self._api.lan.get_hosts_list()
|
||||
except HttpRequestError as err:
|
||||
if (
|
||||
(
|
||||
matcher := re.search(
|
||||
r"Request failed \(APIResponse: (.+)\)", str(err)
|
||||
)
|
||||
)
|
||||
and is_json(json_str := matcher.group(1))
|
||||
and (json_resp := json.loads(json_str)).get("error_code") == "nodev"
|
||||
):
|
||||
# No need to retry, Host list not available
|
||||
self.supports_hosts = False
|
||||
_LOGGER.debug(
|
||||
"Host list is not available using bridge mode (%s)",
|
||||
json_resp.get("msg"),
|
||||
)
|
||||
|
||||
else:
|
||||
raise err
|
||||
self.supports_hosts, fbx_devices = await get_hosts_list_if_supported(
|
||||
self._api
|
||||
)
|
||||
|
||||
# Adds the Freebox itself
|
||||
fbx_devices.append(
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240202.0"]
|
||||
"requirements": ["home-assistant-frontend==20240207.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aio_georss_gdacs", "aio_georss_client"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aio-georss-gdacs==0.8"]
|
||||
"requirements": ["aio-georss-gdacs==0.9"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
@@ -140,6 +141,12 @@ class GenericCamera(Camera):
|
||||
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
vol.Schema(vol.Url())(url)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err)
|
||||
return self._last_image
|
||||
|
||||
if url == self._last_url and self._limit_refetch:
|
||||
return self._last_image
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aio_geojson_geonetnz_quakes"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aio-geojson-geonetnz-quakes==0.15"]
|
||||
"requirements": ["aio-geojson-geonetnz-quakes==0.16"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aio_geojson_geonetnz_volcano"],
|
||||
"requirements": ["aio-geojson-geonetnz-volcano==0.8"]
|
||||
"requirements": ["aio-geojson-geonetnz-volcano==0.9"]
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source_entities": ", ".join(self._entity_ids),
|
||||
"state_classes:": ", ".join(state_classes),
|
||||
"state_classes": ", ".join(state_classes),
|
||||
},
|
||||
)
|
||||
return None
|
||||
@@ -519,7 +519,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source_entities": ", ".join(self._entity_ids),
|
||||
"device_classes:": ", ".join(device_classes),
|
||||
"device_classes": ", ".join(device_classes),
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
},
|
||||
"state_classes_not_matching": {
|
||||
"title": "State classes is not correct",
|
||||
"description": "Device classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue."
|
||||
"description": "State classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,16 @@ DEFAULT_PLANT_ID = "0"
|
||||
DEFAULT_NAME = "Growatt"
|
||||
|
||||
SERVER_URLS = [
|
||||
"https://server-api.growatt.com/",
|
||||
"https://server-us.growatt.com/",
|
||||
"http://server.smten.com/",
|
||||
"https://openapi.growatt.com/", # Other regional server
|
||||
"https://openapi-cn.growatt.com/", # Chinese server
|
||||
"https://openapi-us.growatt.com/", # North American server
|
||||
"http://server.smten.com/", # smten server
|
||||
]
|
||||
|
||||
DEPRECATED_URLS = [
|
||||
"https://server.growatt.com/",
|
||||
"https://server-api.growatt.com/",
|
||||
"https://server-us.growatt.com/",
|
||||
]
|
||||
|
||||
DEFAULT_URL = SERVER_URLS[0]
|
||||
|
||||
@@ -1001,12 +1001,18 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has
|
||||
raise_on_entry_error: bool = False,
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled:
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force refreshing updates for non-scheduled updates
|
||||
# If `raise_on_auth_failed` is set, it means this is
|
||||
# the first refresh and we do not want to delay
|
||||
# startup or cause a timeout so we only refresh the
|
||||
# updates if this is not a scheduled refresh and
|
||||
# we are not doing the first refresh.
|
||||
try:
|
||||
await self.hassio.refresh_updates()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
await super()._async_refresh(
|
||||
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
|
||||
)
|
||||
|
||||
@@ -459,7 +459,7 @@ class HassIO:
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command("/refresh_updates", timeout=None)
|
||||
return self.send_command("/refresh_updates", timeout=300)
|
||||
|
||||
@api_data
|
||||
def retrieve_discovery_messages(self) -> Coroutine:
|
||||
@@ -506,7 +506,6 @@ class HassIO:
|
||||
options = {
|
||||
"ssl": CONF_SSL_CERTIFICATE in http_config,
|
||||
"port": port,
|
||||
"watchdog": True,
|
||||
"refresh_token": refresh_token.token,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.41", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.42", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from aiosomecomfort import (
|
||||
APIRateLimited,
|
||||
AuthError,
|
||||
ConnectionError as AscConnectionError,
|
||||
SomeComfortError,
|
||||
@@ -505,10 +506,11 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
await self._device.refresh()
|
||||
|
||||
except (
|
||||
asyncio.TimeoutError,
|
||||
AscConnectionError,
|
||||
APIRateLimited,
|
||||
AuthError,
|
||||
ClientConnectionError,
|
||||
AscConnectionError,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
self._retry += 1
|
||||
self._attr_available = self._retry <= RETRY
|
||||
@@ -524,7 +526,12 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
await _login()
|
||||
return
|
||||
|
||||
except (AscConnectionError, ClientConnectionError, asyncio.TimeoutError):
|
||||
except (
|
||||
asyncio.TimeoutError,
|
||||
AscConnectionError,
|
||||
APIRateLimited,
|
||||
ClientConnectionError,
|
||||
):
|
||||
self._retry += 1
|
||||
self._attr_available = self._retry <= RETRY
|
||||
return
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""The Intent integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -155,16 +156,18 @@ class GetStateIntentHandler(intent.IntentHandler):
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
|
||||
# Entity name to match
|
||||
name: str | None = slots.get("name", {}).get("value")
|
||||
name_slot = slots.get("name", {})
|
||||
entity_name: str | None = name_slot.get("value")
|
||||
entity_text: str | None = name_slot.get("text")
|
||||
|
||||
# Look up area first to fail early
|
||||
area_name = slots.get("area", {}).get("value")
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
area_name = area_slot.get("text")
|
||||
area: ar.AreaEntry | None = None
|
||||
if area_name is not None:
|
||||
if area_id is not None:
|
||||
areas = ar.async_get(hass)
|
||||
area = areas.async_get_area(area_name) or areas.async_get_area_by_name(
|
||||
area_name
|
||||
)
|
||||
area = areas.async_get_area(area_id)
|
||||
if area is None:
|
||||
raise intent.IntentHandleError(f"No area named {area_name}")
|
||||
|
||||
@@ -186,7 +189,7 @@ class GetStateIntentHandler(intent.IntentHandler):
|
||||
states = list(
|
||||
intent.async_match_states(
|
||||
hass,
|
||||
name=name,
|
||||
name=entity_name,
|
||||
area=area,
|
||||
domains=domains,
|
||||
device_classes=device_classes,
|
||||
@@ -197,13 +200,20 @@ class GetStateIntentHandler(intent.IntentHandler):
|
||||
_LOGGER.debug(
|
||||
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
|
||||
len(states),
|
||||
name,
|
||||
entity_name,
|
||||
area,
|
||||
domains,
|
||||
device_classes,
|
||||
intent_obj.assistant,
|
||||
)
|
||||
|
||||
if entity_name and (len(states) > 1):
|
||||
# Multiple entities matched for the same name
|
||||
raise intent.DuplicateNamesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
)
|
||||
|
||||
# Create response
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
||||
|
||||
@@ -138,6 +138,8 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self._client.connect(init=True)
|
||||
return self.async_show_form(step_id="link")
|
||||
|
||||
if not await self._client.is_connected():
|
||||
await self._client.connect(init=False)
|
||||
if not await self._client.is_connected():
|
||||
errors["base"] = "linking"
|
||||
else:
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/keymitt_ble",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "assumed_state",
|
||||
"loggers": ["keymitt_ble"],
|
||||
"requirements": ["PyMicroBot==0.0.9"]
|
||||
"requirements": ["PyMicroBot==0.0.12"]
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.11.2",
|
||||
"xknxproject==3.4.0",
|
||||
"xknx==2.12.0",
|
||||
"xknxproject==3.6.0",
|
||||
"knx-frontend==2024.1.20.105944"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from linear_garage_door import Linear
|
||||
from linear_garage_door.errors import InvalidLoginError, ResponseError
|
||||
from linear_garage_door.errors import InvalidLoginError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -55,6 +56,7 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
email=self._email,
|
||||
password=self._password,
|
||||
device_id=self._device_id,
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
except InvalidLoginError as err:
|
||||
if (
|
||||
@@ -63,8 +65,6 @@ class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except ResponseError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if not self._devices:
|
||||
self._devices = await linear.get_devices(self._site_id)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/linear_garage_door",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["linear-garage-door==0.2.7"]
|
||||
"requirements": ["linear-garage-door==0.2.9"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Support for Lupusec Home Security system."""
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
|
||||
import lupupy
|
||||
@@ -111,16 +112,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
lupusec_system = await hass.async_add_executor_job(
|
||||
lupupy.Lupusec, username, password, host
|
||||
)
|
||||
|
||||
except LupusecException:
|
||||
_LOGGER.error("Failed to connect to Lupusec device at %s", host)
|
||||
return False
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
_LOGGER.error(
|
||||
"Unknown error while trying to connect to Lupusec device at %s: %s",
|
||||
host,
|
||||
ex,
|
||||
)
|
||||
except JSONDecodeError:
|
||||
_LOGGER.error("Failed to connect to Lupusec device at %s", host)
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lupusec_system
|
||||
|
||||
@@ -29,14 +29,14 @@ SCAN_INTERVAL = timedelta(seconds=2)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an alarm control panel for a Lupusec device."""
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
alarm_devices = [LupusecAlarm(data, data.get_alarm(), config_entry.entry_id)]
|
||||
alarm = await hass.async_add_executor_job(data.get_alarm)
|
||||
|
||||
async_add_devices(alarm_devices)
|
||||
async_add_entities([LupusecAlarm(data, alarm, config_entry.entry_id)])
|
||||
|
||||
|
||||
class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import lupupy.constants as CONST
|
||||
@@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a binary sensors for a Lupusec device."""
|
||||
|
||||
@@ -34,10 +35,12 @@ async def async_setup_entry(
|
||||
device_types = CONST.TYPE_OPENING + CONST.TYPE_SENSOR
|
||||
|
||||
sensors = []
|
||||
for device in data.get_devices(generic_type=device_types):
|
||||
partial_func = partial(data.get_devices, generic_type=device_types)
|
||||
devices = await hass.async_add_executor_job(partial_func)
|
||||
for device in devices:
|
||||
sensors.append(LupusecBinarySensor(device, config_entry.entry_id))
|
||||
|
||||
async_add_devices(sensors)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class LupusecBinarySensor(LupusecBaseSensor, BinarySensorEntity):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
""""Config flow for Lupusec integration."""
|
||||
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -50,6 +51,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await test_host_connection(self.hass, host, username, password)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except JSONDecodeError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -80,6 +83,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await test_host_connection(self.hass, host, username, password)
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except JSONDecodeError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The Lupus Electronics LUPUSEC YAML configuration import failed",
|
||||
"description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
"description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"title": "The Lupus Electronics LUPUSEC YAML configuration import failed",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import lupupy.constants as CONST
|
||||
@@ -20,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=2)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Lupusec switch devices."""
|
||||
|
||||
@@ -29,10 +30,12 @@ async def async_setup_entry(
|
||||
device_types = CONST.TYPE_SWITCH
|
||||
|
||||
switches = []
|
||||
for device in data.get_devices(generic_type=device_types):
|
||||
partial_func = partial(data.get_devices, generic_type=device_types)
|
||||
devices = await hass.async_add_executor_job(partial_func)
|
||||
for device in devices:
|
||||
switches.append(LupusecSwitch(device, config_entry.entry_id))
|
||||
|
||||
async_add_devices(switches)
|
||||
async_add_entities(switches)
|
||||
|
||||
|
||||
class LupusecSwitch(LupusecBaseSensor, SwitchEntity):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lutron",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pylutron"],
|
||||
"requirements": ["pylutron==0.2.8"]
|
||||
"requirements": ["pylutron==0.2.12"]
|
||||
}
|
||||
|
||||
@@ -52,11 +52,27 @@ class MatterAdapter:
|
||||
|
||||
async def setup_nodes(self) -> None:
|
||||
"""Set up all existing nodes and subscribe to new nodes."""
|
||||
initialized_nodes: set[int] = set()
|
||||
for node in self.matter_client.get_nodes():
|
||||
if not node.available:
|
||||
# ignore un-initialized nodes at startup
|
||||
# catch them later when they become available.
|
||||
continue
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_added_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node added event."""
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def node_updated_callback(event: EventType, node: MatterNode) -> None:
|
||||
"""Handle node updated event."""
|
||||
if node.node_id in initialized_nodes:
|
||||
return
|
||||
if not node.available:
|
||||
return
|
||||
initialized_nodes.add(node.node_id)
|
||||
self._setup_node(node)
|
||||
|
||||
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
|
||||
@@ -116,6 +132,11 @@ class MatterAdapter:
|
||||
callback=node_added_callback, event_filter=EventType.NODE_ADDED
|
||||
)
|
||||
)
|
||||
self.config_entry.async_on_unload(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=node_updated_callback, event_filter=EventType.NODE_UPDATED
|
||||
)
|
||||
)
|
||||
|
||||
def _setup_node(self, node: MatterNode) -> None:
|
||||
"""Set up an node."""
|
||||
|
||||
@@ -129,6 +129,9 @@ class MatterEntity(Entity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Call when the entity needs to be updated."""
|
||||
if not self._endpoint.node.available:
|
||||
# skip poll when the node is not (yet) available
|
||||
return
|
||||
# manually poll/refresh the primary value
|
||||
await self.matter_client.refresh_attribute(
|
||||
self._endpoint.node.node_id,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==5.4.0"]
|
||||
"requirements": ["python-matter-server==5.5.0"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
@@ -123,11 +123,6 @@ class MelCloudDevice:
|
||||
via_device=(DOMAIN, f"{dev.mac}-{dev.serial}"),
|
||||
)
|
||||
|
||||
@property
|
||||
def daily_energy_consumed(self) -> float | None:
|
||||
"""Return energy consumed during the current day in kWh."""
|
||||
return self.device.daily_energy_consumed
|
||||
|
||||
|
||||
async def mel_devices_setup(
|
||||
hass: HomeAssistant, token: str
|
||||
@@ -138,8 +133,8 @@ async def mel_devices_setup(
|
||||
all_devices = await get_devices(
|
||||
token,
|
||||
session,
|
||||
conf_update_interval=timedelta(minutes=5),
|
||||
device_set_debounce=timedelta(seconds=1),
|
||||
conf_update_interval=timedelta(minutes=30),
|
||||
device_set_debounce=timedelta(seconds=2),
|
||||
)
|
||||
wrapped_devices: dict[str, list[MelCloudDevice]] = {}
|
||||
for device_type, devices in all_devices.items():
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"domain": "melcloud",
|
||||
"name": "MELCloud",
|
||||
"codeowners": ["@vilppuvuorinen"],
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pymelcloud"],
|
||||
"requirements": ["pymelcloud==2.5.8"]
|
||||
"requirements": ["pymelcloud==2.5.9"]
|
||||
}
|
||||
|
||||
@@ -58,16 +58,6 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
value_fn=lambda x: x.device.total_energy_consumed,
|
||||
enabled=lambda x: x.device.has_energy_consumed_meter,
|
||||
),
|
||||
MelcloudSensorEntityDescription(
|
||||
key="daily_energy",
|
||||
translation_key="daily_energy",
|
||||
icon="mdi:factory",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda x: x.device.daily_energy_consumed,
|
||||
enabled=lambda x: True,
|
||||
),
|
||||
)
|
||||
ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
MelcloudSensorEntityDescription(
|
||||
@@ -90,16 +80,6 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
value_fn=lambda x: x.device.tank_temperature,
|
||||
enabled=lambda x: True,
|
||||
),
|
||||
MelcloudSensorEntityDescription(
|
||||
key="daily_energy",
|
||||
translation_key="daily_energy",
|
||||
icon="mdi:factory",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda x: x.device.daily_energy_consumed,
|
||||
enabled=lambda x: True,
|
||||
),
|
||||
)
|
||||
ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
MelcloudSensorEntityDescription(
|
||||
|
||||
@@ -65,9 +65,6 @@
|
||||
"room_temperature": {
|
||||
"name": "Room temperature"
|
||||
},
|
||||
"daily_energy": {
|
||||
"name": "Daily energy consumed"
|
||||
},
|
||||
"outside_temperature": {
|
||||
"name": "Outside temperature"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import datapoint
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
@@ -16,7 +17,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
@@ -34,9 +35,6 @@ from .const import (
|
||||
from .data import MetOfficeData
|
||||
from .helpers import fetch_data, fetch_site
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
import datapoint
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
@@ -44,10 +42,6 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Met Office entry."""
|
||||
if sys.version_info >= (3, 12):
|
||||
raise HomeAssistantError(
|
||||
"Met Office is not supported on Python 3.12. Please use Python 3.11."
|
||||
)
|
||||
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Site import Site
|
||||
from datapoint.Timestep import Timestep
|
||||
from datapoint.Forecast import Forecast
|
||||
from datapoint.Site import Site
|
||||
from datapoint.Timestep import Timestep
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import datapoint
|
||||
from datapoint.Site import Site
|
||||
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
from homeassistant.util.dt import utcnow
|
||||
@@ -10,11 +12,6 @@ from homeassistant.util.dt import utcnow
|
||||
from .const import MODE_3HOURLY
|
||||
from .data import MetOfficeData
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
import datapoint
|
||||
from datapoint.Site import Site
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -34,7 +31,7 @@ def fetch_site(
|
||||
def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData:
|
||||
"""Fetch weather and forecast from Datapoint API."""
|
||||
try:
|
||||
forecast = connection.get_forecast_for_site(site.id, mode)
|
||||
forecast = connection.get_forecast_for_site(site.location_id, mode)
|
||||
except (ValueError, datapoint.exceptions.APIException) as err:
|
||||
_LOGGER.error("Check Met Office connection: %s", err.args)
|
||||
raise UpdateFailed from err
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
"name": "Met Office",
|
||||
"codeowners": ["@MrHarcombe", "@avee87"],
|
||||
"config_flow": true,
|
||||
"disabled": "Integration library not compatible with Python 3.12",
|
||||
"documentation": "https://www.home-assistant.io/integrations/metoffice",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["datapoint"],
|
||||
"requirements": ["datapoint==0.9.8;python_version<'3.12'"]
|
||||
"requirements": ["datapoint==0.9.9"]
|
||||
}
|
||||
|
||||
@@ -251,6 +251,6 @@ class MetOfficeCurrentSensor(
|
||||
return {
|
||||
ATTR_LAST_UPDATE: self.coordinator.data.now.date,
|
||||
ATTR_SENSOR_ID: self.entity_description.key,
|
||||
ATTR_SITE_ID: self.coordinator.data.site.id,
|
||||
ATTR_SITE_ID: self.coordinator.data.site.location_id,
|
||||
ATTR_SITE_NAME: self.coordinator.data.site.name,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Support for mill wifi-enabled home heaters."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import mill
|
||||
@@ -186,9 +187,14 @@ class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntit
|
||||
_attr_max_temp = MAX_TEMP
|
||||
_attr_min_temp = MIN_TEMP
|
||||
_attr_name = None
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(self, coordinator: MillDataUpdateCoordinator) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
@@ -186,7 +186,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_STRUCTURE): cv.string,
|
||||
vol.Optional(CONF_SCALE, default=1): cv.positive_float,
|
||||
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
|
||||
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_PRECISION): cv.positive_int,
|
||||
vol.Optional(
|
||||
@@ -241,8 +241,8 @@ CLIMATE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
||||
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_float,
|
||||
vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_float,
|
||||
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
||||
vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
|
||||
@@ -342,8 +342,8 @@ SENSOR_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
|
||||
vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_VALUE): cv.positive_float,
|
||||
vol.Optional(CONF_MAX_VALUE): cv.positive_float,
|
||||
vol.Optional(CONF_MIN_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAN_VALUE): nan_validator,
|
||||
vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
|
||||
}
|
||||
|
||||
@@ -199,6 +199,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
|
||||
self._precision = config.get(CONF_PRECISION, 2)
|
||||
else:
|
||||
self._precision = config.get(CONF_PRECISION, 0)
|
||||
if self._precision > 0 or self._scale != int(self._scale):
|
||||
self._value_is_int = False
|
||||
|
||||
def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
|
||||
"""Do swap as needed."""
|
||||
|
||||
@@ -364,7 +364,9 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
|
||||
# Translate the value received
|
||||
if fan_mode is not None:
|
||||
self._attr_fan_mode = self._fan_mode_mapping_from_modbus[int(fan_mode)]
|
||||
self._attr_fan_mode = self._fan_mode_mapping_from_modbus.get(
|
||||
int(fan_mode), self._attr_fan_mode
|
||||
)
|
||||
|
||||
# Read the on/off register if defined. If the value in this
|
||||
# register is "OFF", it will take precedence over the value
|
||||
|
||||
@@ -51,6 +51,7 @@ POSITION_DEVICE_MAP = {
|
||||
BlindType.CurtainLeft: CoverDeviceClass.CURTAIN,
|
||||
BlindType.CurtainRight: CoverDeviceClass.CURTAIN,
|
||||
BlindType.SkylightBlind: CoverDeviceClass.SHADE,
|
||||
BlindType.InsectScreen: CoverDeviceClass.SHADE,
|
||||
}
|
||||
|
||||
TILT_DEVICE_MAP = {
|
||||
@@ -69,6 +70,7 @@ TILT_ONLY_DEVICE_MAP = {
|
||||
|
||||
TDBU_DEVICE_MAP = {
|
||||
BlindType.TopDownBottomUp: CoverDeviceClass.SHADE,
|
||||
BlindType.TriangleBlind: CoverDeviceClass.BLIND,
|
||||
}
|
||||
|
||||
|
||||
@@ -398,6 +400,7 @@ class MotionTDBUDevice(MotionPositionDevice):
|
||||
def __init__(self, coordinator, blind, device_class, motor):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator, blind, device_class)
|
||||
delattr(self, "_attr_name")
|
||||
self._motor = motor
|
||||
self._motor_key = motor[0]
|
||||
self._attr_translation_key = motor.lower()
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.19"]
|
||||
"requirements": ["motionblinds==0.6.20"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for Motion Blinds sensors."""
|
||||
from motionblinds import DEVICE_TYPES_WIFI, BlindType
|
||||
from motionblinds import DEVICE_TYPES_WIFI
|
||||
from motionblinds.motion_blinds import DEVICE_TYPE_TDBU
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -29,7 +30,7 @@ async def async_setup_entry(
|
||||
|
||||
for blind in motion_gateway.device_list.values():
|
||||
entities.append(MotionSignalStrengthSensor(coordinator, blind))
|
||||
if blind.type == BlindType.TopDownBottomUp:
|
||||
if blind.device_type == DEVICE_TYPE_TDBU:
|
||||
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom"))
|
||||
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top"))
|
||||
elif blind.battery_voltage is not None and blind.battery_voltage > 0:
|
||||
|
||||
@@ -75,7 +75,7 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity):
|
||||
|
||||
# Internal properties
|
||||
self.point_id = device_point.parameter_id
|
||||
self._attr_name = device_point.parameter_name
|
||||
self._attr_name = device_point.parameter_name.replace("\u002d", "")
|
||||
|
||||
if entity_description is not None:
|
||||
self.entity_description = entity_description
|
||||
|
||||
@@ -212,7 +212,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
host = entry.data[CONF_HOST]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
if not (data := hass.data.get(DOMAIN)) or data.websession.closed:
|
||||
websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
|
||||
|
||||
hass.data[DOMAIN] = LTEData(websession)
|
||||
@@ -258,7 +258,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
hass.data.pop(DOMAIN)
|
||||
hass.data.pop(DOMAIN, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["nibe==2.7.0"]
|
||||
"requirements": ["nibe==2.8.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.5.0"]
|
||||
"requirements": ["python-otbr-api==2.6.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiopegelonline"],
|
||||
"requirements": ["aiopegelonline==0.0.6"]
|
||||
"requirements": ["aiopegelonline==0.0.8"]
|
||||
}
|
||||
|
||||
@@ -63,11 +63,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
self.gateway_data = coordinator.data.devices[gateway_id]
|
||||
# Determine supported features
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if HVACMode.OFF in self.hvac_modes:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
if (
|
||||
self.cdr_gateway["cooling_present"]
|
||||
and self.cdr_gateway["smile_name"] != "Adam"
|
||||
@@ -75,6 +70,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
self._attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
if HVACMode.OFF in self.hvac_modes:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
if presets := self.device.get("preset_modes"):
|
||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||
self._attr_preset_modes = presets
|
||||
|
||||
@@ -21,7 +21,10 @@ from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
HVACAction,
|
||||
)
|
||||
from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
@@ -437,7 +440,7 @@ class PrometheusMetrics:
|
||||
float(cover_state == state.state)
|
||||
)
|
||||
|
||||
position = state.attributes.get(ATTR_POSITION)
|
||||
position = state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if position is not None:
|
||||
position_metric = self._metric(
|
||||
"cover_position",
|
||||
@@ -446,7 +449,7 @@ class PrometheusMetrics:
|
||||
)
|
||||
position_metric.labels(**self._labels(state)).set(float(position))
|
||||
|
||||
tilt_position = state.attributes.get(ATTR_TILT_POSITION)
|
||||
tilt_position = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
|
||||
if tilt_position is not None:
|
||||
tilt_position_metric = self._metric(
|
||||
"cover_tilt_position",
|
||||
|
||||
@@ -69,6 +69,7 @@ class TrackedEntityDescriptor(NamedTuple):
|
||||
|
||||
entity_id: str
|
||||
identifier: str
|
||||
name: str
|
||||
|
||||
|
||||
def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo:
|
||||
@@ -95,13 +96,24 @@ async def async_setup_entry(
|
||||
|
||||
entity_reg = er.async_get(hass)
|
||||
for tracked_entity_id in coordinator.tracked_entities:
|
||||
tracked_entity_object_id = tracked_entity_id.split(".")[-1]
|
||||
if (entity_entry := entity_reg.async_get(tracked_entity_id)) is not None:
|
||||
tracked_entity_descriptors.append(
|
||||
TrackedEntityDescriptor(tracked_entity_id, entity_entry.id)
|
||||
TrackedEntityDescriptor(
|
||||
tracked_entity_id,
|
||||
entity_entry.id,
|
||||
entity_entry.name
|
||||
or entity_entry.original_name
|
||||
or tracked_entity_object_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
tracked_entity_descriptors.append(
|
||||
TrackedEntityDescriptor(tracked_entity_id, tracked_entity_id)
|
||||
TrackedEntityDescriptor(
|
||||
tracked_entity_id,
|
||||
tracked_entity_id,
|
||||
tracked_entity_object_id,
|
||||
)
|
||||
)
|
||||
|
||||
entities += [
|
||||
@@ -165,7 +177,7 @@ class ProximityTrackedEntitySensor(
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor.identifier}_{description.key}"
|
||||
self._attr_device_info = _device_info(coordinator)
|
||||
self._attr_translation_placeholders = {
|
||||
"tracked_entity": self.tracked_entity_id.split(".")[-1]
|
||||
"tracked_entity": tracked_entity_descriptor.name
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -176,18 +188,19 @@ class ProximityTrackedEntitySensor(
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self) -> dict[str, str | int | None] | None:
|
||||
def data(self) -> dict[str, str | int | None]:
|
||||
"""Get data from coordinator."""
|
||||
return self.coordinator.data.entities.get(self.tracked_entity_id)
|
||||
return self.coordinator.data.entities[self.tracked_entity_id]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.data is not None
|
||||
return (
|
||||
super().available
|
||||
and self.tracked_entity_id in self.coordinator.data.entities
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | float | None:
|
||||
"""Return native sensor value."""
|
||||
if self.data is None:
|
||||
return None
|
||||
return self.data.get(self.entity_description.key)
|
||||
|
||||
@@ -96,7 +96,7 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder
|
||||
"""Fetch the data."""
|
||||
root_folders = await self.api_client.async_get_root_folders()
|
||||
if isinstance(root_folders, RootFolder):
|
||||
root_folders = [root_folders]
|
||||
return [root_folders]
|
||||
return root_folders
|
||||
|
||||
|
||||
@@ -105,7 +105,10 @@ class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]):
|
||||
|
||||
async def _fetch_data(self) -> list[Health]:
|
||||
"""Fetch the health data."""
|
||||
return await self.api_client.async_get_failed_health_checks()
|
||||
health = await self.api_client.async_get_failed_health_checks()
|
||||
if isinstance(health, Health):
|
||||
return [health]
|
||||
return health
|
||||
|
||||
|
||||
class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]):
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"requirements": ["ring-doorbell[listen]==0.8.5"]
|
||||
"requirements": ["ring-doorbell[listen]==0.8.7"]
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ async def setup_device(
|
||||
device.name,
|
||||
)
|
||||
_LOGGER.debug(err)
|
||||
await mqtt_client.async_release()
|
||||
raise err
|
||||
coordinator = RoborockDataUpdateCoordinator(
|
||||
hass, device, networking, product_info, mqtt_client
|
||||
@@ -125,6 +126,7 @@ async def setup_device(
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady:
|
||||
await coordinator.release()
|
||||
if isinstance(coordinator.api, RoborockMqttClient):
|
||||
_LOGGER.warning(
|
||||
"Not setting up %s because the we failed to get data for the first time using the online client. "
|
||||
@@ -153,14 +155,10 @@ async def setup_device(
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.release()
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id].values()
|
||||
)
|
||||
)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
release_tasks = set()
|
||||
for coordinator in hass.data[DOMAIN][entry.entry_id].values():
|
||||
release_tasks.add(coordinator.release())
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
await asyncio.gather(*release_tasks)
|
||||
return unload_ok
|
||||
|
||||
@@ -77,7 +77,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
|
||||
async def release(self) -> None:
|
||||
"""Disconnect from API."""
|
||||
await self.api.async_disconnect()
|
||||
await self.api.async_release()
|
||||
await self.cloud_api.async_release()
|
||||
|
||||
async def _update_device_prop(self) -> None:
|
||||
"""Update device properties."""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Support for Roborock device base class."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from roborock.api import AttributeCache, RoborockClient
|
||||
@@ -7,6 +6,7 @@ from roborock.cloud_api import RoborockMqttClient
|
||||
from roborock.command_cache import CacheableAttribute
|
||||
from roborock.containers import Consumable, Status
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_message import RoborockDataProtocol
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -24,7 +24,10 @@ class RoborockEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, unique_id: str, device_info: DeviceInfo, api: RoborockClient
|
||||
self,
|
||||
unique_id: str,
|
||||
device_info: DeviceInfo,
|
||||
api: RoborockClient,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
self._attr_unique_id = unique_id
|
||||
@@ -75,6 +78,9 @@ class RoborockCoordinatedEntity(
|
||||
self,
|
||||
unique_id: str,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
listener_request: list[RoborockDataProtocol]
|
||||
| RoborockDataProtocol
|
||||
| None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinated Roborock Device."""
|
||||
RoborockEntity.__init__(
|
||||
@@ -85,6 +91,23 @@ class RoborockCoordinatedEntity(
|
||||
)
|
||||
CoordinatorEntity.__init__(self, coordinator=coordinator)
|
||||
self._attr_unique_id = unique_id
|
||||
if isinstance(listener_request, RoborockDataProtocol):
|
||||
listener_request = [listener_request]
|
||||
self.listener_requests = listener_request or []
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add listeners when the device is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
for listener_request in self.listener_requests:
|
||||
self.api.add_listener(
|
||||
listener_request, self._update_from_listener, cache=self.api.cache
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove listeners when the device is removed from hass."""
|
||||
for listener_request in self.listener_requests:
|
||||
self.api.remove_listener(listener_request, self._update_from_listener)
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
@property
|
||||
def _device_status(self) -> Status:
|
||||
@@ -107,7 +130,7 @@ class RoborockCoordinatedEntity(
|
||||
await self.coordinator.async_refresh()
|
||||
return res
|
||||
|
||||
def _update_from_listener(self, value: Status | Consumable):
|
||||
def _update_from_listener(self, value: Status | Consumable) -> None:
|
||||
"""Update the status or consumable data from a listener and then write the new entity state."""
|
||||
if isinstance(value, Status):
|
||||
self.coordinator.roborock_device_info.props.status = value
|
||||
|
||||
@@ -107,10 +107,8 @@ class RoborockSelectEntity(RoborockCoordinatedEntity, SelectEntity):
|
||||
) -> None:
|
||||
"""Create a select entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(unique_id, coordinator)
|
||||
super().__init__(unique_id, coordinator, entity_description.protocol_listener)
|
||||
self._attr_options = options
|
||||
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the option."""
|
||||
|
||||
@@ -232,10 +232,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntity, SensorEntity):
|
||||
description: RoborockSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(unique_id, coordinator)
|
||||
self.entity_description = description
|
||||
if (protocol := self.entity_description.protocol_listener) is not None:
|
||||
self.api.add_listener(protocol, self._update_from_listener, self.api.cache)
|
||||
super().__init__(unique_id, coordinator, description.protocol_listener)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime.datetime:
|
||||
|
||||
@@ -92,14 +92,16 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity):
|
||||
) -> None:
|
||||
"""Initialize a vacuum."""
|
||||
StateVacuumEntity.__init__(self)
|
||||
RoborockCoordinatedEntity.__init__(self, unique_id, coordinator)
|
||||
RoborockCoordinatedEntity.__init__(
|
||||
self,
|
||||
unique_id,
|
||||
coordinator,
|
||||
listener_request=[
|
||||
RoborockDataProtocol.FAN_POWER,
|
||||
RoborockDataProtocol.STATE,
|
||||
],
|
||||
)
|
||||
self._attr_fan_speed_list = self._device_status.fan_power_options
|
||||
self.api.add_listener(
|
||||
RoborockDataProtocol.FAN_POWER, self._update_from_listener, self.api.cache
|
||||
)
|
||||
self.api.add_listener(
|
||||
RoborockDataProtocol.STATE, self._update_from_listener, self.api.cache
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
|
||||
22
homeassistant/components/ruuvitag_ble/strings.json
Normal file
22
homeassistant/components/ruuvitag_ble/strings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "[%key:component::bluetooth::config::flow_title%]",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
},
|
||||
"bluetooth_confirm": {
|
||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"not_supported": "Device not supported",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.6.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.38.1"
|
||||
"async-upnp-client==0.38.2"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -173,9 +173,9 @@ async def async_setup_entry(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ENABLE_CLIMATE_REACT,
|
||||
{
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float,
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict,
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float,
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict,
|
||||
vol.Required(ATTR_SMART_TYPE): vol.In(
|
||||
["temperature", "feelsLike", "humidity"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user