forked from home-assistant/core
Compare commits
85 Commits
2024.9.0b1
...
2024.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ec1b33fe | ||
|
|
84a0a28be2 | ||
|
|
ac19ee3e2e | ||
|
|
438af042ed | ||
|
|
122f11c790 | ||
|
|
de99dfef4e | ||
|
|
bcdc3563a5 | ||
|
|
9ef0a1f0a2 | ||
|
|
d0629d4e66 | ||
|
|
65e98eab9c | ||
|
|
a0d9764443 | ||
|
|
8293f270df | ||
|
|
116090bff1 | ||
|
|
6082220f7f | ||
|
|
74fd16b953 | ||
|
|
82cffcbc23 | ||
|
|
4e1a77326e | ||
|
|
54cf52069e | ||
|
|
70b811096c | ||
|
|
1efd267ee6 | ||
|
|
31267b4095 | ||
|
|
4982e1cbcf | ||
|
|
be3b16b7fa | ||
|
|
393a0ac0df | ||
|
|
a0bbcb0401 | ||
|
|
3f65bc78e8 | ||
|
|
d005440544 | ||
|
|
94d2da1685 | ||
|
|
4c5ba0617a | ||
|
|
a58bf149fc | ||
|
|
005be4e8ba | ||
|
|
b81d7a0ed8 | ||
|
|
9a690ed421 | ||
|
|
009989d7ae | ||
|
|
c7d1ad27f0 | ||
|
|
3af11fb2b1 | ||
|
|
c839cc1f15 | ||
|
|
a0f2e2ebdd | ||
|
|
d07e62b2f1 | ||
|
|
e7f957def2 | ||
|
|
16ab57c9a6 | ||
|
|
1a67052cbd | ||
|
|
f85a802ebd | ||
|
|
3b5c08ecf8 | ||
|
|
450c63ad28 | ||
|
|
a8f472f44e | ||
|
|
fa3a301e97 | ||
|
|
b1ef1be9a3 | ||
|
|
62ef951ace | ||
|
|
06660f9170 | ||
|
|
7662ca8a96 | ||
|
|
e04fc74fcf | ||
|
|
f9bca7619c | ||
|
|
1b9aa727f8 | ||
|
|
d54c1935f8 | ||
|
|
9cfad05793 | ||
|
|
03ab471d23 | ||
|
|
b2b69e40fd | ||
|
|
c6ff445dd4 | ||
|
|
0948a94409 | ||
|
|
9be20d6130 | ||
|
|
c4e484539d | ||
|
|
234f32265e | ||
|
|
411b014da2 | ||
|
|
3a8aa4200d | ||
|
|
dd8471e786 | ||
|
|
f33b4b0dc0 | ||
|
|
8ab8f7a740 | ||
|
|
ee9e3fe27b | ||
|
|
d4830caac0 | ||
|
|
3b4e3b1370 | ||
|
|
533c8ca31c | ||
|
|
8668af17f6 | ||
|
|
5b866e071c | ||
|
|
37af180edc | ||
|
|
0d5dc01048 | ||
|
|
bd2be0a763 | ||
|
|
98cbd7d8da | ||
|
|
26f3305743 | ||
|
|
3c0480596d | ||
|
|
81d2231e6f | ||
|
|
2d041a1fa9 | ||
|
|
516f3295bf | ||
|
|
94516de724 | ||
|
|
ae4fc9504a |
10
.github/workflows/builder.yml
vendored
10
.github/workflows/builder.yml
vendored
@@ -491,7 +491,7 @@ jobs:
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
needs: ["init", "build_base"]
|
||||
needs: ["init"]
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
env:
|
||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||
@@ -510,8 +510,8 @@ jobs:
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
with:
|
||||
context: ./script/hassfest/docker
|
||||
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
load: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
|
||||
@@ -523,8 +523,8 @@ jobs:
|
||||
id: push
|
||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||
with:
|
||||
context: ./script/hassfest/docker
|
||||
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiopulse"],
|
||||
"requirements": ["aiopulse==0.4.4"]
|
||||
"requirements": ["aiopulse==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["androidtvremote2==0.1.1"],
|
||||
"requirements": ["androidtvremote2==0.1.2"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__)
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
|
||||
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
RECOMMENDED_MAX_TOKENS = 1024
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
|
||||
@@ -6,15 +6,16 @@ from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .data import AugustData
|
||||
@@ -24,7 +25,27 @@ from .util import async_create_august_clientsession
|
||||
type AugustConfigEntry = ConfigEntry[AugustData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@callback
|
||||
def _async_create_yale_brand_migration_issue(
|
||||
hass: HomeAssistant, entry: AugustConfigEntry
|
||||
) -> None:
|
||||
"""Create an issue for a brand migration."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"yale_brand_migration",
|
||||
breaks_in_ha_version="2024.9",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/yale",
|
||||
translation_key="yale_brand_migration",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.CRITICAL,
|
||||
translation_placeholders={
|
||||
"migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||
"""Set up August from a config entry."""
|
||||
session = async_create_august_clientsession(hass)
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
|
||||
@@ -40,6 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> None:
|
||||
"""Remove an August config entry."""
|
||||
ir.async_delete_issue(hass, DOMAIN, "yale_brand_migration")
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -51,6 +77,8 @@ async def async_setup_august(
|
||||
"""Set up the August component."""
|
||||
config = cast(YaleXSConfig, entry.data)
|
||||
await august_gateway.async_setup(config)
|
||||
if august_gateway.api.brand == Brand.YALE_HOME:
|
||||
_async_create_yale_brand_migration_issue(hass, entry)
|
||||
await august_gateway.async_authenticate()
|
||||
await august_gateway.async_refresh_access_token_if_needed()
|
||||
data = entry.runtime_data = AugustData(hass, august_gateway)
|
||||
|
||||
@@ -109,12 +109,11 @@ async def async_setup_entry(
|
||||
for description in SENSOR_TYPES_DOORBELL
|
||||
)
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
entities.extend(
|
||||
AugustDoorbellBinarySensor(data, doorbell, description)
|
||||
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
AugustDoorbellBinarySensor(data, doorbell, description)
|
||||
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
|
||||
for doorbell in data.doorbells
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AugustConfigEntry
|
||||
from .entity import AugustEntityMixin
|
||||
from .entity import AugustEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -18,7 +18,7 @@ async def async_setup_entry(
|
||||
async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks)
|
||||
|
||||
|
||||
class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
|
||||
class AugustWakeLockButton(AugustEntity, ButtonEntity):
|
||||
"""Representation of an August lock wake button."""
|
||||
|
||||
_attr_translation_key = "wake"
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
|
||||
from .entity import AugustEntityMixin
|
||||
from .entity import AugustEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,7 +38,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class AugustCamera(AugustEntityMixin, Camera):
|
||||
class AugustCamera(AugustEntity, Camera):
|
||||
"""An implementation of an August security camera."""
|
||||
|
||||
_attr_translation_key = "camera"
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from yalexs.authenticator_common import ValidationResult
|
||||
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND
|
||||
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -28,6 +28,12 @@ from .const import (
|
||||
from .gateway import AugustGateway
|
||||
from .util import async_create_august_clientsession
|
||||
|
||||
# The Yale Home Brand is not supported by the August integration
|
||||
# anymore and should migrate to the Yale integration
|
||||
AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy()
|
||||
del AVAILABLE_BRANDS[Brand.YALE_HOME]
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -118,7 +124,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(
|
||||
CONF_BRAND,
|
||||
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
||||
): vol.In(BRANDS_WITHOUT_OAUTH),
|
||||
): vol.In(AVAILABLE_BRANDS),
|
||||
vol.Required(
|
||||
CONF_LOGIN_METHOD,
|
||||
default=self._user_auth_details.get(
|
||||
|
||||
@@ -20,7 +20,7 @@ from .const import MANUFACTURER
|
||||
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
|
||||
|
||||
|
||||
class AugustEntityMixin(Entity):
|
||||
class AugustEntity(Entity):
|
||||
"""Base implementation for August device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
@@ -87,7 +87,7 @@ class AugustEntityMixin(Entity):
|
||||
self._update_from_data()
|
||||
|
||||
|
||||
class AugustDescriptionEntity(AugustEntityMixin):
|
||||
class AugustDescriptionEntity(AugustEntity):
|
||||
"""An August entity with a description."""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -63,22 +63,17 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the august event platform."""
|
||||
data = config_entry.runtime_data
|
||||
entities: list[AugustEventEntity] = []
|
||||
|
||||
for lock in data.locks:
|
||||
detail = data.get_device_detail(lock.device_id)
|
||||
if detail.doorbell:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, lock, description)
|
||||
for description in TYPES_DOORBELL
|
||||
)
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, doorbell, description)
|
||||
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
||||
)
|
||||
|
||||
entities: list[AugustEventEntity] = [
|
||||
AugustEventEntity(data, lock, description)
|
||||
for description in TYPES_DOORBELL
|
||||
for lock in data.locks
|
||||
if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell
|
||||
]
|
||||
entities.extend(
|
||||
AugustEventEntity(data, doorbell, description)
|
||||
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
||||
for doorbell in data.doorbells
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -86,7 +81,6 @@ class AugustEventEntity(AugustDescriptionEntity, EventEntity):
|
||||
"""An august event entity."""
|
||||
|
||||
entity_description: AugustEventEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_last_activity: Activity | None = None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .entity import AugustEntityMixin
|
||||
from .entity import AugustEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +36,7 @@ async def async_setup_entry(
|
||||
async_add_entities(AugustLock(data, lock) for lock in data.locks)
|
||||
|
||||
|
||||
class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
||||
class AugustLock(AugustEntity, RestoreEntity, LockEntity):
|
||||
"""Representation of an August lock."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from yalexs.activity import ActivityType, LockOperationActivity
|
||||
from yalexs.doorbell import Doorbell
|
||||
@@ -42,7 +42,7 @@ from .const import (
|
||||
OPERATION_METHOD_REMOTE,
|
||||
OPERATION_METHOD_TAG,
|
||||
)
|
||||
from .entity import AugustDescriptionEntity, AugustEntityMixin
|
||||
from .entity import AugustDescriptionEntity, AugustEntity
|
||||
|
||||
|
||||
def _retrieve_device_battery_state(detail: LockDetail) -> int:
|
||||
@@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
|
||||
return detail.battery_percentage
|
||||
|
||||
|
||||
_T = TypeVar("_T", LockDetail, KeypadDetail)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AugustSensorEntityDescription(SensorEntityDescription, Generic[_T]):
|
||||
class AugustSensorEntityDescription[T: LockDetail | KeypadDetail](
|
||||
SensorEntityDescription
|
||||
):
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[_T], int | None]
|
||||
value_fn: Callable[[T], int | None]
|
||||
|
||||
|
||||
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
||||
@@ -114,7 +113,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
|
||||
class AugustOperatorSensor(AugustEntity, RestoreSensor):
|
||||
"""Representation of an August lock operation sensor."""
|
||||
|
||||
_attr_translation_key = "operator"
|
||||
@@ -198,10 +197,12 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
|
||||
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
|
||||
|
||||
|
||||
class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]):
|
||||
class AugustBatterySensor[T: LockDetail | KeypadDetail](
|
||||
AugustDescriptionEntity, SensorEntity
|
||||
):
|
||||
"""Representation of an August sensor."""
|
||||
|
||||
entity_description: AugustSensorEntityDescription[_T]
|
||||
entity_description: AugustSensorEntityDescription[T]
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"issues": {
|
||||
"yale_brand_migration": {
|
||||
"title": "Yale Home has a new integration",
|
||||
"description": "Add the [Yale integration]({migrate_url}), and remove the August integration as soon as possible to avoid an interruption in service. The Yale Home brand will stop working with the August integration soon and will be removed in a future release."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"error": {
|
||||
"unhandled": "Unhandled error: {error}",
|
||||
|
||||
@@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
if start <= _native_datetime() <= end:
|
||||
if start <= datetime.now() <= end:
|
||||
return latest
|
||||
return None
|
||||
|
||||
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
|
||||
@@ -309,7 +309,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
return True
|
||||
|
||||
async def _start_poll_command(self):
|
||||
async def _poll_loop(self):
|
||||
"""Loop which polls the status of the player."""
|
||||
while True:
|
||||
try:
|
||||
@@ -335,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._polling_task = self.hass.async_create_background_task(
|
||||
self._start_poll_command(),
|
||||
self._poll_loop(),
|
||||
name=f"bluesound.polling_{self.host}:{self.port}",
|
||||
)
|
||||
|
||||
@@ -345,7 +345,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
assert self._polling_task is not None
|
||||
if self._polling_task.cancel():
|
||||
await self._polling_task
|
||||
# the sleeps in _poll_loop will raise CancelledError
|
||||
with suppress(CancelledError):
|
||||
await self._polling_task
|
||||
|
||||
self.hass.data[DATA_BLUESOUND].remove(self)
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-data-tools==1.20.0",
|
||||
"dbus-fast==2.24.0",
|
||||
"habluetooth==3.3.2"
|
||||
"habluetooth==3.4.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
|
||||
|
||||
@@ -33,6 +34,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
entry.data[CONF_PASSWORD],
|
||||
get_region_from_name(entry.data[CONF_REGION]),
|
||||
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
||||
verify=get_default_context(),
|
||||
)
|
||||
self.read_only = entry.options[CONF_READ_ONLY]
|
||||
self._entry = entry
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bimmer-connected[china]==0.16.2"]
|
||||
"requirements": ["bimmer-connected[china]==0.16.3"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
DOMAIN = "buienradar"
|
||||
|
||||
DEFAULT_TIMEOUT = 60
|
||||
DEFAULT_TIMEFRAME = 60
|
||||
|
||||
DEFAULT_DIMENSION = 700
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Shared utilities for different supported platforms."""
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from buienradar.buienradar import parse_data
|
||||
@@ -27,12 +27,12 @@ from buienradar.constants import (
|
||||
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
|
||||
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import SCHEDULE_NOK, SCHEDULE_OK
|
||||
from .const import DEFAULT_TIMEOUT, SCHEDULE_NOK, SCHEDULE_OK
|
||||
|
||||
__all__ = ["BrData"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -59,10 +59,10 @@ class BrData:
|
||||
load_error_count: int = WARN_THRESHOLD
|
||||
rain_error_count: int = WARN_THRESHOLD
|
||||
|
||||
def __init__(self, hass, coordinates, timeframe, devices):
|
||||
def __init__(self, hass: HomeAssistant, coordinates, timeframe, devices) -> None:
|
||||
"""Initialize the data object."""
|
||||
self.devices = devices
|
||||
self.data = {}
|
||||
self.data: dict[str, Any] | None = {}
|
||||
self.hass = hass
|
||||
self.coordinates = coordinates
|
||||
self.timeframe = timeframe
|
||||
@@ -93,9 +93,9 @@ class BrData:
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
async with timeout(10):
|
||||
resp = await websession.get(url)
|
||||
|
||||
async with websession.get(
|
||||
url, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
|
||||
) as resp:
|
||||
result[STATUS_CODE] = resp.status
|
||||
result[CONTENT] = await resp.text()
|
||||
if resp.status == HTTPStatus.OK:
|
||||
|
||||
@@ -130,7 +130,7 @@ class BrWeather(WeatherEntity):
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
|
||||
|
||||
def __init__(self, config, coordinates):
|
||||
def __init__(self, config, coordinates) -> None:
|
||||
"""Initialize the platform with a data instance and station name."""
|
||||
self._stationname = config.get(CONF_NAME, "Buienradar")
|
||||
self._attr_name = self._stationname or f"BR {'(unknown station)'}"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.7.1"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"]
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.9.4"]
|
||||
}
|
||||
|
||||
@@ -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.10", "deebot-client==8.3.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==25.2.1",
|
||||
"aioesphomeapi==25.3.1",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==1.0.0"
|
||||
],
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240829.0"]
|
||||
"requirements": ["home-assistant-frontend==20240904.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["fyta_cli==0.6.3"]
|
||||
"requirements": ["fyta_cli==0.6.6"]
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.4.2"]
|
||||
"requirements": ["gardena-bluetooth==1.4.3"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.55", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.56", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ async def websocket_update_modem_config(
|
||||
"""Get the schema for the modem configuration."""
|
||||
config = msg["config"]
|
||||
config_entry = get_insteon_config_entry(hass)
|
||||
is_connected = devices.modem.connected
|
||||
is_connected = devices.modem is not None and devices.modem.connected
|
||||
|
||||
if not await _async_connect(**config):
|
||||
connection.send_error(
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from intellifire4py import IntellifireControlAsync
|
||||
from intellifire4py.exceptions import LoginException
|
||||
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal
|
||||
import asyncio
|
||||
|
||||
from intellifire4py import UnifiedFireplace
|
||||
from intellifire4py.cloud_interface import IntelliFireCloudInterface
|
||||
from intellifire4py.model import IntelliFireCommonFireplaceData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
@@ -18,7 +20,18 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_USER_ID, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_AUTH_COOKIE,
|
||||
CONF_CONTROL_MODE,
|
||||
CONF_READ_MODE,
|
||||
CONF_SERIAL,
|
||||
CONF_USER_ID,
|
||||
CONF_WEB_CLIENT_ID,
|
||||
DOMAIN,
|
||||
INIT_WAIT_TIME_SECONDS,
|
||||
LOGGER,
|
||||
STARTUP_TIMEOUT,
|
||||
)
|
||||
from .coordinator import IntellifireDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -32,79 +45,114 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData:
|
||||
"""Convert config entry data into IntelliFireCommonFireplaceData."""
|
||||
|
||||
return IntelliFireCommonFireplaceData(
|
||||
auth_cookie=entry.data[CONF_AUTH_COOKIE],
|
||||
user_id=entry.data[CONF_USER_ID],
|
||||
web_client_id=entry.data[CONF_WEB_CLIENT_ID],
|
||||
serial=entry.data[CONF_SERIAL],
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
ip_address=entry.data[CONF_IP_ADDRESS],
|
||||
read_mode=entry.options[CONF_READ_MODE],
|
||||
control_mode=entry.options[CONF_CONTROL_MODE],
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate entries."""
|
||||
LOGGER.debug(
|
||||
"Migrating configuration from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version == 1:
|
||||
new = {**config_entry.data}
|
||||
|
||||
if config_entry.minor_version < 2:
|
||||
username = config_entry.data[CONF_USERNAME]
|
||||
password = config_entry.data[CONF_PASSWORD]
|
||||
|
||||
# Create a Cloud Interface
|
||||
async with IntelliFireCloudInterface() as cloud_interface:
|
||||
await cloud_interface.login_with_credentials(
|
||||
username=username, password=password
|
||||
)
|
||||
|
||||
new_data = cloud_interface.user_data.get_data_for_ip(new[CONF_HOST])
|
||||
|
||||
if not new_data:
|
||||
raise ConfigEntryAuthFailed
|
||||
new[CONF_API_KEY] = new_data.api_key
|
||||
new[CONF_WEB_CLIENT_ID] = new_data.web_client_id
|
||||
new[CONF_AUTH_COOKIE] = new_data.auth_cookie
|
||||
|
||||
new[CONF_IP_ADDRESS] = new_data.ip_address
|
||||
new[CONF_SERIAL] = new_data.serial
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
options={CONF_READ_MODE: "local", CONF_CONTROL_MODE: "local"},
|
||||
unique_id=new[CONF_SERIAL],
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
LOGGER.debug("Pseudo Migration %s successful", config_entry.version)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up IntelliFire from a config entry."""
|
||||
LOGGER.debug("Setting up config entry: %s", entry.unique_id)
|
||||
|
||||
if CONF_USERNAME not in entry.data:
|
||||
LOGGER.debug("Old config entry format detected: %s", entry.unique_id)
|
||||
LOGGER.debug("Config entry without username detected: %s", entry.unique_id)
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
ift_control = IntellifireControlAsync(
|
||||
fireplace_ip=entry.data[CONF_HOST],
|
||||
)
|
||||
try:
|
||||
await ift_control.login(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
fireplace: UnifiedFireplace = (
|
||||
await UnifiedFireplace.build_fireplace_from_common(
|
||||
_construct_common_data(entry)
|
||||
)
|
||||
)
|
||||
except (ConnectionError, ClientConnectionError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except LoginException as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
|
||||
finally:
|
||||
await ift_control.close()
|
||||
|
||||
# Extract API Key and User_ID from ift_control
|
||||
# Eventually this will migrate to using IntellifireAPICloud
|
||||
|
||||
if CONF_USER_ID not in entry.data or CONF_API_KEY not in entry.data:
|
||||
LOGGER.info(
|
||||
"Updating intellifire config entry for %s with api information",
|
||||
entry.unique_id,
|
||||
)
|
||||
cloud_api = IntellifireAPICloud()
|
||||
await cloud_api.login(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
api_key = cloud_api.get_fireplace_api_key()
|
||||
user_id = cloud_api.get_user_id()
|
||||
# Update data entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_USER_ID: user_id,
|
||||
},
|
||||
LOGGER.debug("Waiting for Fireplace to Initialize")
|
||||
await asyncio.wait_for(
|
||||
_async_wait_for_initialization(fireplace), timeout=STARTUP_TIMEOUT
|
||||
)
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
"Initialization of fireplace timed out after 10 minutes"
|
||||
) from err
|
||||
|
||||
else:
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
user_id = entry.data[CONF_USER_ID]
|
||||
|
||||
# Instantiate local control
|
||||
api = IntellifireAPILocal(
|
||||
fireplace_ip=entry.data[CONF_HOST],
|
||||
api_key=api_key,
|
||||
user_id=user_id,
|
||||
# Construct coordinator
|
||||
data_update_coordinator = IntellifireDataUpdateCoordinator(
|
||||
hass=hass, fireplace=fireplace
|
||||
)
|
||||
|
||||
# Define the update coordinator
|
||||
coordinator = IntellifireDataUpdateCoordinator(
|
||||
hass=hass,
|
||||
api=api,
|
||||
)
|
||||
LOGGER.debug("Fireplace to Initialized - Awaiting first refresh")
|
||||
await data_update_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_update_coordinator
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_wait_for_initialization(
|
||||
fireplace: UnifiedFireplace, timeout=STARTUP_TIMEOUT
|
||||
):
|
||||
"""Wait for a fireplace to be initialized."""
|
||||
while (
|
||||
fireplace.data.ipv4_address == "127.0.0.1" and fireplace.data.serial == "unset"
|
||||
):
|
||||
LOGGER.debug(f"Waiting for fireplace to initialize [{fireplace.read_mode}]")
|
||||
await asyncio.sleep(INIT_WAIT_TIME_SECONDS)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from intellifire4py import IntellifirePollData
|
||||
from intellifire4py.model import IntelliFirePollData
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -26,7 +26,7 @@ from .entity import IntellifireEntity
|
||||
class IntellifireBinarySensorRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[IntellifirePollData], bool]
|
||||
value_fn: Callable[[IntelliFirePollData], bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -69,7 +69,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity):
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
if coordinator.data.thermostat_on:
|
||||
self.last_temp = coordinator.data.thermostat_setpoint_c
|
||||
self.last_temp = int(coordinator.data.thermostat_setpoint_c)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
|
||||
@@ -7,16 +7,33 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from intellifire4py import AsyncUDPFireplaceFinder
|
||||
from intellifire4py.exceptions import LoginException
|
||||
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal
|
||||
from intellifire4py.cloud_interface import IntelliFireCloudInterface
|
||||
from intellifire4py.exceptions import LoginError
|
||||
from intellifire4py.local_api import IntelliFireAPILocal
|
||||
from intellifire4py.model import IntelliFireCommonFireplaceData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.dhcp import DhcpServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
|
||||
from .const import CONF_USER_ID, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
API_MODE_LOCAL,
|
||||
CONF_AUTH_COOKIE,
|
||||
CONF_CONTROL_MODE,
|
||||
CONF_READ_MODE,
|
||||
CONF_SERIAL,
|
||||
CONF_USER_ID,
|
||||
CONF_WEB_CLIENT_ID,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
|
||||
@@ -31,17 +48,20 @@ class DiscoveredHostInfo:
|
||||
serial: str | None
|
||||
|
||||
|
||||
async def validate_host_input(host: str, dhcp_mode: bool = False) -> str:
|
||||
async def _async_poll_local_fireplace_for_serial(
|
||||
host: str, dhcp_mode: bool = False
|
||||
) -> str:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host)
|
||||
api = IntellifireAPILocal(fireplace_ip=host)
|
||||
api = IntelliFireAPILocal(fireplace_ip=host)
|
||||
await api.poll(suppress_warnings=dhcp_mode)
|
||||
serial = api.data.serial
|
||||
|
||||
LOGGER.debug("Found a fireplace: %s", serial)
|
||||
|
||||
# Return the serial number which will be used to calculate a unique ID for the device/sensors
|
||||
return serial
|
||||
|
||||
@@ -50,239 +70,206 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for IntelliFire."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Config Flow Handler."""
|
||||
self._host: str = ""
|
||||
self._serial: str = ""
|
||||
self._not_configured_hosts: list[DiscoveredHostInfo] = []
|
||||
|
||||
# DHCP Variables
|
||||
self._dhcp_discovered_serial: str = "" # used only in discovery mode
|
||||
self._discovered_host: DiscoveredHostInfo
|
||||
self._dhcp_mode = False
|
||||
self._is_reauth = False
|
||||
|
||||
self._not_configured_hosts: list[DiscoveredHostInfo] = []
|
||||
self._reauth_needed: DiscoveredHostInfo
|
||||
|
||||
async def _find_fireplaces(self):
|
||||
"""Perform UDP discovery."""
|
||||
fireplace_finder = AsyncUDPFireplaceFinder()
|
||||
discovered_hosts = await fireplace_finder.search_fireplace(timeout=12)
|
||||
configured_hosts = {
|
||||
entry.data[CONF_HOST]
|
||||
for entry in self._async_current_entries(include_ignore=False)
|
||||
if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries
|
||||
}
|
||||
self._configured_serials: list[str] = []
|
||||
|
||||
self._not_configured_hosts = [
|
||||
DiscoveredHostInfo(ip, None)
|
||||
for ip in discovered_hosts
|
||||
if ip not in configured_hosts
|
||||
]
|
||||
LOGGER.debug("Discovered Hosts: %s", discovered_hosts)
|
||||
LOGGER.debug("Configured Hosts: %s", configured_hosts)
|
||||
LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts)
|
||||
|
||||
async def validate_api_access_and_create_or_update(
|
||||
self, *, host: str, username: str, password: str, serial: str
|
||||
):
|
||||
"""Validate username/password against api."""
|
||||
LOGGER.debug("Attempting login to iftapi with: %s", username)
|
||||
|
||||
ift_cloud = IntellifireAPICloud()
|
||||
await ift_cloud.login(username=username, password=password)
|
||||
api_key = ift_cloud.get_fireplace_api_key()
|
||||
user_id = ift_cloud.get_user_id()
|
||||
|
||||
data = {
|
||||
CONF_HOST: host,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_USERNAME: username,
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_USER_ID: user_id,
|
||||
}
|
||||
|
||||
# Update or Create
|
||||
existing_entry = await self.async_set_unique_id(serial)
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=data)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
return self.async_create_entry(title=f"Fireplace {serial}", data=data)
|
||||
|
||||
async def async_step_api_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure API access."""
|
||||
|
||||
errors = {}
|
||||
control_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
control_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
|
||||
): str,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
return await self.validate_api_access_and_create_or_update(
|
||||
host=self._host,
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
serial=self._serial,
|
||||
)
|
||||
|
||||
except (ConnectionError, ClientConnectionError):
|
||||
errors["base"] = "iftapi_connect"
|
||||
LOGGER.error(
|
||||
"Could not connect to iftapi.net over https - verify connectivity"
|
||||
)
|
||||
except LoginException:
|
||||
errors["base"] = "api_error"
|
||||
LOGGER.error("Invalid credentials for iftapi.net")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="api_config", errors=errors, data_schema=control_schema
|
||||
)
|
||||
|
||||
async def _async_validate_ip_and_continue(self, host: str) -> ConfigFlowResult:
|
||||
"""Validate local config and continue."""
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
self._serial = await validate_host_input(host)
|
||||
await self.async_set_unique_id(self._serial, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
# Store current data and jump to next stage
|
||||
self._host = host
|
||||
|
||||
return await self.async_step_api_config()
|
||||
|
||||
async def async_step_manual_device_entry(self, user_input=None):
|
||||
"""Handle manual input of local IP configuration."""
|
||||
LOGGER.debug("STEP: manual_device_entry")
|
||||
errors = {}
|
||||
self._host = user_input.get(CONF_HOST) if user_input else None
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._async_validate_ip_and_continue(self._host)
|
||||
except (ConnectionError, ClientConnectionError):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual_device_entry",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST, default=self._host): str}),
|
||||
)
|
||||
|
||||
async def async_step_pick_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick which device to configure."""
|
||||
errors = {}
|
||||
LOGGER.debug("STEP: pick_device")
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_HOST] == MANUAL_ENTRY_STRING:
|
||||
return await self.async_step_manual_device_entry()
|
||||
|
||||
try:
|
||||
return await self._async_validate_ip_and_continue(user_input[CONF_HOST])
|
||||
except (ConnectionError, ClientConnectionError):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="pick_device",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): vol.In(
|
||||
[host.ip for host in self._not_configured_hosts]
|
||||
+ [MANUAL_ENTRY_STRING]
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
# Define a cloud api interface we can use
|
||||
self.cloud_api_interface = IntelliFireCloudInterface()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start the user flow."""
|
||||
|
||||
# Launch fireplaces discovery
|
||||
await self._find_fireplaces()
|
||||
LOGGER.debug("STEP: user")
|
||||
if self._not_configured_hosts:
|
||||
LOGGER.debug("Running Step: pick_device")
|
||||
return await self.async_step_pick_device()
|
||||
LOGGER.debug("Running Step: manual_device_entry")
|
||||
return await self.async_step_manual_device_entry()
|
||||
current_entries = self._async_current_entries(include_ignore=False)
|
||||
self._configured_serials = [
|
||||
entry.data[CONF_SERIAL] for entry in current_entries
|
||||
]
|
||||
|
||||
return await self.async_step_cloud_api()
|
||||
|
||||
async def async_step_cloud_api(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Authenticate against IFTAPI Cloud in order to see configured devices.
|
||||
|
||||
Local control of IntelliFire devices requires that the user download the correct API KEY which is only available on the cloud. Cloud control of the devices requires the user has at least once authenticated against the cloud and a set of cookie variables have been stored locally.
|
||||
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
LOGGER.debug("STEP: cloud_api")
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
async with self.cloud_api_interface as cloud_interface:
|
||||
await cloud_interface.login_with_credentials(
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
# If login was successful pass username/password to next step
|
||||
return await self.async_step_pick_cloud_device()
|
||||
except LoginError:
|
||||
errors["base"] = "api_error"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="cloud_api",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_pick_cloud_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Step to select a device from the cloud.
|
||||
|
||||
We can only get here if we have logged in. If there is only one device available it will be auto-configured,
|
||||
else the user will be given a choice to pick a device.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
LOGGER.debug(
|
||||
f"STEP: pick_cloud_device: {user_input} - DHCP_MODE[{self._dhcp_mode}"
|
||||
)
|
||||
|
||||
if self._dhcp_mode or user_input is not None:
|
||||
if self._dhcp_mode:
|
||||
serial = self._dhcp_discovered_serial
|
||||
LOGGER.debug(f"DHCP Mode detected for serial [{serial}]")
|
||||
if user_input is not None:
|
||||
serial = user_input[CONF_SERIAL]
|
||||
|
||||
# Run a unique ID Check prior to anything else
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured(updates={CONF_SERIAL: serial})
|
||||
|
||||
# If Serial is Good obtain fireplace and configure
|
||||
fireplace = self.cloud_api_interface.user_data.get_data_for_serial(serial)
|
||||
if fireplace:
|
||||
return await self._async_create_config_entry_from_common_data(
|
||||
fireplace=fireplace
|
||||
)
|
||||
|
||||
# Parse User Data to see if we auto-configure or prompt for selection:
|
||||
user_data = self.cloud_api_interface.user_data
|
||||
|
||||
available_fireplaces: list[IntelliFireCommonFireplaceData] = [
|
||||
fp
|
||||
for fp in user_data.fireplaces
|
||||
if fp.serial not in self._configured_serials
|
||||
]
|
||||
|
||||
# Abort if all devices have been configured
|
||||
if not available_fireplaces:
|
||||
return self.async_abort(reason="no_available_devices")
|
||||
|
||||
# If there is a single fireplace configure it
|
||||
if len(available_fireplaces) == 1:
|
||||
if self._is_reauth:
|
||||
reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self._async_create_config_entry_from_common_data(
|
||||
fireplace=available_fireplaces[0], existing_entry=reauth_entry
|
||||
)
|
||||
|
||||
return await self._async_create_config_entry_from_common_data(
|
||||
fireplace=available_fireplaces[0]
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="pick_cloud_device",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SERIAL): vol.In(
|
||||
[fp.serial for fp in available_fireplaces]
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def _async_create_config_entry_from_common_data(
|
||||
self,
|
||||
fireplace: IntelliFireCommonFireplaceData,
|
||||
existing_entry: ConfigEntry | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Construct a config entry based on an object of IntelliFireCommonFireplaceData."""
|
||||
|
||||
data = {
|
||||
CONF_IP_ADDRESS: fireplace.ip_address,
|
||||
CONF_API_KEY: fireplace.api_key,
|
||||
CONF_SERIAL: fireplace.serial,
|
||||
CONF_AUTH_COOKIE: fireplace.auth_cookie,
|
||||
CONF_WEB_CLIENT_ID: fireplace.web_client_id,
|
||||
CONF_USER_ID: fireplace.user_id,
|
||||
CONF_USERNAME: self.cloud_api_interface.user_data.username,
|
||||
CONF_PASSWORD: self.cloud_api_interface.user_data.password,
|
||||
}
|
||||
|
||||
options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL}
|
||||
|
||||
if existing_entry:
|
||||
return self.async_update_reload_and_abort(
|
||||
existing_entry, data=data, options=options
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=f"Fireplace {fireplace.serial}", data=data, options=options
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
LOGGER.debug("STEP: reauth")
|
||||
self._is_reauth = True
|
||||
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
assert entry
|
||||
assert entry.unique_id
|
||||
|
||||
# populate the expected vars
|
||||
self._serial = entry.unique_id
|
||||
self._host = entry.data[CONF_HOST]
|
||||
self._dhcp_discovered_serial = entry.data[CONF_SERIAL] # type: ignore[union-attr]
|
||||
|
||||
placeholders = {CONF_HOST: self._host, "serial": self._serial}
|
||||
placeholders = {"serial": self._dhcp_discovered_serial}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
return await self.async_step_api_config()
|
||||
|
||||
return await self.async_step_cloud_api()
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle DHCP Discovery."""
|
||||
self._dhcp_mode = True
|
||||
|
||||
# Run validation logic on ip
|
||||
host = discovery_info.ip
|
||||
LOGGER.debug("STEP: dhcp for host %s", host)
|
||||
ip_address = discovery_info.ip
|
||||
LOGGER.debug("STEP: dhcp for ip_address %s", ip_address)
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
|
||||
try:
|
||||
self._serial = await validate_host_input(host, dhcp_mode=True)
|
||||
self._dhcp_discovered_serial = await _async_poll_local_fireplace_for_serial(
|
||||
ip_address, dhcp_mode=True
|
||||
)
|
||||
except (ConnectionError, ClientConnectionError):
|
||||
LOGGER.debug(
|
||||
"DHCP Discovery has determined %s is not an IntelliFire device", host
|
||||
"DHCP Discovery has determined %s is not an IntelliFire device",
|
||||
ip_address,
|
||||
)
|
||||
return self.async_abort(reason="not_intellifire_device")
|
||||
|
||||
await self.async_set_unique_id(self._serial)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
self._discovered_host = DiscoveredHostInfo(ip=host, serial=self._serial)
|
||||
|
||||
placeholders = {CONF_HOST: host, "serial": self._serial}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
self._set_confirm_only()
|
||||
|
||||
return await self.async_step_dhcp_confirm()
|
||||
|
||||
async def async_step_dhcp_confirm(self, user_input=None):
|
||||
"""Attempt to confirm."""
|
||||
|
||||
LOGGER.debug("STEP: dhcp_confirm")
|
||||
# Add the hosts one by one
|
||||
host = self._discovered_host.ip
|
||||
serial = self._discovered_host.serial
|
||||
|
||||
if user_input is None:
|
||||
# Show the confirmation dialog
|
||||
return self.async_show_form(
|
||||
step_id="dhcp_confirm",
|
||||
description_placeholders={CONF_HOST: host, "serial": serial},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Fireplace {serial}",
|
||||
data={CONF_HOST: host},
|
||||
)
|
||||
return await self.async_step_cloud_api()
|
||||
|
||||
@@ -5,11 +5,22 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
DOMAIN = "intellifire"
|
||||
|
||||
CONF_USER_ID = "user_id"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
DEFAULT_THERMOSTAT_TEMP = 21
|
||||
|
||||
CONF_USER_ID = "user_id" # part of the cloud cookie
|
||||
CONF_WEB_CLIENT_ID = "web_client_id" # part of the cloud cookie
|
||||
CONF_AUTH_COOKIE = "auth_cookie" # part of the cloud cookie
|
||||
|
||||
CONF_SERIAL = "serial"
|
||||
CONF_READ_MODE = "cloud_read"
|
||||
CONF_CONTROL_MODE = "cloud_control"
|
||||
|
||||
DEFAULT_THERMOSTAT_TEMP = 21
|
||||
|
||||
API_MODE_LOCAL = "local"
|
||||
API_MODE_CLOUD = "cloud"
|
||||
|
||||
|
||||
STARTUP_TIMEOUT = 600
|
||||
|
||||
INIT_WAIT_TIME_SECONDS = 10
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from intellifire4py import IntellifirePollData
|
||||
from intellifire4py.intellifire import IntellifireAPILocal
|
||||
from intellifire4py import UnifiedFireplace
|
||||
from intellifire4py.control import IntelliFireController
|
||||
from intellifire4py.model import IntelliFirePollData
|
||||
from intellifire4py.read import IntelliFireDataProvider
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]):
|
||||
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData]):
|
||||
"""Class to manage the polling of the fireplace API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api: IntellifireAPILocal,
|
||||
fireplace: UnifiedFireplace,
|
||||
) -> None:
|
||||
"""Initialize the Coordinator."""
|
||||
super().__init__(
|
||||
@@ -31,36 +31,21 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=15),
|
||||
)
|
||||
self._api = api
|
||||
|
||||
async def _async_update_data(self) -> IntellifirePollData:
|
||||
if not self._api.is_polling_in_background:
|
||||
LOGGER.info("Starting Intellifire Background Polling Loop")
|
||||
await self._api.start_background_polling()
|
||||
|
||||
# Don't return uninitialized poll data
|
||||
async with asyncio.timeout(15):
|
||||
try:
|
||||
await self._api.poll()
|
||||
except (ConnectionError, ClientConnectionError) as exception:
|
||||
raise UpdateFailed from exception
|
||||
|
||||
LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts)
|
||||
if self._api.failed_poll_attempts > 10:
|
||||
LOGGER.debug("Too many polling errors - raising exception")
|
||||
raise UpdateFailed
|
||||
|
||||
return self._api.data
|
||||
self.fireplace = fireplace
|
||||
|
||||
@property
|
||||
def read_api(self) -> IntellifireAPILocal:
|
||||
def read_api(self) -> IntelliFireDataProvider:
|
||||
"""Return the Status API pointer."""
|
||||
return self._api
|
||||
return self.fireplace.read_api
|
||||
|
||||
@property
|
||||
def control_api(self) -> IntellifireAPILocal:
|
||||
def control_api(self) -> IntelliFireController:
|
||||
"""Return the control API."""
|
||||
return self._api
|
||||
return self.fireplace.control_api
|
||||
|
||||
async def _async_update_data(self) -> IntelliFirePollData:
|
||||
return self.fireplace.data
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
@@ -69,7 +54,6 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
|
||||
manufacturer="Hearth and Home",
|
||||
model="IFT-WFM",
|
||||
name="IntelliFire",
|
||||
identifiers={("IntelliFire", f"{self.read_api.data.serial}]")},
|
||||
sw_version=self.read_api.data.fw_ver_str,
|
||||
configuration_url=f"http://{self._api.fireplace_ip}/poll",
|
||||
identifiers={("IntelliFire", str(self.fireplace.serial))},
|
||||
configuration_url=f"http://{self.fireplace.ip_address}/poll",
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from . import IntellifireDataUpdateCoordinator
|
||||
|
||||
|
||||
class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
|
||||
"""Define a generic class for Intellifire entities."""
|
||||
"""Define a generic class for IntelliFire entities."""
|
||||
|
||||
_attr_attribution = "Data provided by unpublished Intellifire API"
|
||||
_attr_has_entity_name = True
|
||||
@@ -22,6 +22,8 @@ class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{description.key}_{coordinator.read_api.data.serial}"
|
||||
self._attr_unique_id = f"{description.key}_{coordinator.fireplace.serial}"
|
||||
self.identifiers = ({("IntelliFire", f"{coordinator.fireplace.serial}]")},)
|
||||
|
||||
# Configure the Device Info
|
||||
self._attr_device_info = self.coordinator.device_info
|
||||
|
||||
@@ -7,7 +7,8 @@ from dataclasses import dataclass
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from intellifire4py import IntellifireControlAsync, IntellifirePollData
|
||||
from intellifire4py.control import IntelliFireController
|
||||
from intellifire4py.model import IntelliFirePollData
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity,
|
||||
@@ -31,8 +32,8 @@ from .entity import IntellifireEntity
|
||||
class IntellifireFanRequiredKeysMixin:
|
||||
"""Required keys for fan entity."""
|
||||
|
||||
set_fn: Callable[[IntellifireControlAsync, int], Awaitable]
|
||||
value_fn: Callable[[IntellifirePollData], bool]
|
||||
set_fn: Callable[[IntelliFireController, int], Awaitable]
|
||||
value_fn: Callable[[IntelliFirePollData], int]
|
||||
speed_range: tuple[int, int]
|
||||
|
||||
|
||||
@@ -91,7 +92,8 @@ class IntellifireFan(IntellifireEntity, FanEntity):
|
||||
def percentage(self) -> int | None:
|
||||
"""Return fan percentage."""
|
||||
return ranged_value_to_percentage(
|
||||
self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed
|
||||
self.entity_description.speed_range,
|
||||
self.coordinator.read_api.data.fanspeed,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,7 +6,8 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from intellifire4py import IntellifireControlAsync, IntellifirePollData
|
||||
from intellifire4py.control import IntelliFireController
|
||||
from intellifire4py.model import IntelliFirePollData
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@@ -27,8 +28,8 @@ from .entity import IntellifireEntity
|
||||
class IntellifireLightRequiredKeysMixin:
|
||||
"""Required keys for fan entity."""
|
||||
|
||||
set_fn: Callable[[IntellifireControlAsync, int], Awaitable]
|
||||
value_fn: Callable[[IntellifirePollData], bool]
|
||||
set_fn: Callable[[IntelliFireController, int], Awaitable]
|
||||
value_fn: Callable[[IntelliFirePollData], int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -56,7 +57,7 @@ class IntellifireLight(IntellifireEntity, LightEntity):
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
def brightness(self) -> int:
|
||||
"""Return the current brightness 0-255."""
|
||||
return 85 * self.entity_description.value_fn(self.coordinator.read_api.data)
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/intellifire",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["intellifire4py"],
|
||||
"requirements": ["intellifire4py==2.2.2"]
|
||||
"requirements": ["intellifire4py==4.1.9"]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from intellifire4py import IntellifirePollData
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -29,7 +27,9 @@ from .entity import IntellifireEntity
|
||||
class IntellifireSensorRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[IntellifirePollData], int | str | datetime | None]
|
||||
value_fn: Callable[
|
||||
[IntellifireDataUpdateCoordinator], int | str | datetime | float | None
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -40,16 +40,29 @@ class IntellifireSensorEntityDescription(
|
||||
"""Describes a sensor entity."""
|
||||
|
||||
|
||||
def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None:
|
||||
def _time_remaining_to_timestamp(
|
||||
coordinator: IntellifireDataUpdateCoordinator,
|
||||
) -> datetime | None:
|
||||
"""Define a sensor that takes into account timezone."""
|
||||
if not (seconds_offset := data.timeremaining_s):
|
||||
if not (seconds_offset := coordinator.data.timeremaining_s):
|
||||
return None
|
||||
return utcnow() + timedelta(seconds=seconds_offset)
|
||||
|
||||
|
||||
def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None:
|
||||
def _downtime_to_timestamp(
|
||||
coordinator: IntellifireDataUpdateCoordinator,
|
||||
) -> datetime | None:
|
||||
"""Define a sensor that takes into account a timezone."""
|
||||
if not (seconds_offset := data.downtime):
|
||||
if not (seconds_offset := coordinator.data.downtime):
|
||||
return None
|
||||
return utcnow() - timedelta(seconds=seconds_offset)
|
||||
|
||||
|
||||
def _uptime_to_timestamp(
|
||||
coordinator: IntellifireDataUpdateCoordinator,
|
||||
) -> datetime | None:
|
||||
"""Return a timestamp of how long the sensor has been up."""
|
||||
if not (seconds_offset := coordinator.data.uptime):
|
||||
return None
|
||||
return utcnow() - timedelta(seconds=seconds_offset)
|
||||
|
||||
@@ -60,14 +73,14 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
translation_key="flame_height",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
# UI uses 1-5 for flame height, backing lib uses 0-4
|
||||
value_fn=lambda data: (data.flameheight + 1),
|
||||
value_fn=lambda coordinator: (coordinator.data.flameheight + 1),
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="temperature",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.temperature_c,
|
||||
value_fn=lambda coordinator: coordinator.data.temperature_c,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="target_temp",
|
||||
@@ -75,13 +88,13 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: data.thermostat_setpoint_c,
|
||||
value_fn=lambda coordinator: coordinator.data.thermostat_setpoint_c,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="fan_speed",
|
||||
translation_key="fan_speed",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.fanspeed,
|
||||
value_fn=lambda coordinator: coordinator.data.fanspeed,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="timer_end_timestamp",
|
||||
@@ -102,27 +115,27 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
translation_key="uptime",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime),
|
||||
value_fn=_uptime_to_timestamp,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="connection_quality",
|
||||
translation_key="connection_quality",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.connection_quality,
|
||||
value_fn=lambda coordinator: coordinator.data.connection_quality,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="ecm_latency",
|
||||
translation_key="ecm_latency",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.ecm_latency,
|
||||
value_fn=lambda coordinator: coordinator.data.ecm_latency,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
IntellifireSensorEntityDescription(
|
||||
key="ipv4_address",
|
||||
translation_key="ipv4_address",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: data.ipv4_address,
|
||||
value_fn=lambda coordinator: coordinator.data.ipv4_address,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -134,17 +147,17 @@ async def async_setup_entry(
|
||||
|
||||
coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
IntellifireSensor(coordinator=coordinator, description=description)
|
||||
IntelliFireSensor(coordinator=coordinator, description=description)
|
||||
for description in INTELLIFIRE_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class IntellifireSensor(IntellifireEntity, SensorEntity):
|
||||
"""Extends IntellifireEntity with Sensor specific logic."""
|
||||
class IntelliFireSensor(IntellifireEntity, SensorEntity):
|
||||
"""Extends IntelliFireEntity with Sensor specific logic."""
|
||||
|
||||
entity_description: IntellifireSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | str | datetime | None:
|
||||
def native_value(self) -> int | str | datetime | float | None:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(self.coordinator.read_api.data)
|
||||
return self.entity_description.value_fn(self.coordinator)
|
||||
|
||||
@@ -1,39 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{serial} ({host})",
|
||||
"flow_title": "{serial}",
|
||||
"step": {
|
||||
"manual_device_entry": {
|
||||
"description": "Local Configuration",
|
||||
"data": {
|
||||
"host": "Host (IP Address)"
|
||||
}
|
||||
"pick_cloud_device": {
|
||||
"title": "Configure fireplace",
|
||||
"description": "Select fireplace by serial number:"
|
||||
},
|
||||
"api_config": {
|
||||
"cloud_api": {
|
||||
"description": "Authenticate against IntelliFire Cloud",
|
||||
"data_description": {
|
||||
"username": "Your IntelliFire app username",
|
||||
"password": "Your IntelliFire app password"
|
||||
},
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"dhcp_confirm": {
|
||||
"description": "Do you want to set up {host}\nSerial: {serial}?"
|
||||
},
|
||||
"pick_device": {
|
||||
"title": "Device Selection",
|
||||
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"api_error": "Login failed",
|
||||
"iftapi_connect": "Error conecting to iftapi.net"
|
||||
"api_error": "Login failed"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"not_intellifire_device": "Not an IntelliFire Device."
|
||||
"not_intellifire_device": "Not an IntelliFire device.",
|
||||
"no_available_devices": "All available devices have already been configured."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -6,16 +6,13 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from intellifire4py import IntellifirePollData
|
||||
from intellifire4py.intellifire import IntellifireAPILocal
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import IntellifireDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import IntellifireDataUpdateCoordinator
|
||||
from .entity import IntellifireEntity
|
||||
|
||||
|
||||
@@ -23,9 +20,9 @@ from .entity import IntellifireEntity
|
||||
class IntellifireSwitchRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
on_fn: Callable[[IntellifireAPILocal], Awaitable]
|
||||
off_fn: Callable[[IntellifireAPILocal], Awaitable]
|
||||
value_fn: Callable[[IntellifirePollData], bool]
|
||||
on_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
|
||||
off_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
|
||||
value_fn: Callable[[IntellifireDataUpdateCoordinator], bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -39,16 +36,16 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = (
|
||||
IntellifireSwitchEntityDescription(
|
||||
key="on_off",
|
||||
translation_key="flame",
|
||||
on_fn=lambda control_api: control_api.flame_on(),
|
||||
off_fn=lambda control_api: control_api.flame_off(),
|
||||
value_fn=lambda data: data.is_on,
|
||||
on_fn=lambda coordinator: coordinator.control_api.flame_on(),
|
||||
off_fn=lambda coordinator: coordinator.control_api.flame_off(),
|
||||
value_fn=lambda coordinator: coordinator.read_api.data.is_on,
|
||||
),
|
||||
IntellifireSwitchEntityDescription(
|
||||
key="pilot",
|
||||
translation_key="pilot_light",
|
||||
on_fn=lambda control_api: control_api.pilot_on(),
|
||||
off_fn=lambda control_api: control_api.pilot_off(),
|
||||
value_fn=lambda data: data.pilot_on,
|
||||
on_fn=lambda coordinator: coordinator.control_api.pilot_on(),
|
||||
off_fn=lambda coordinator: coordinator.control_api.pilot_off(),
|
||||
value_fn=lambda coordinator: coordinator.read_api.data.pilot_on,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -74,15 +71,15 @@ class IntellifireSwitch(IntellifireEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.entity_description.on_fn(self.coordinator.control_api)
|
||||
await self.entity_description.on_fn(self.coordinator)
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.entity_description.off_fn(self.coordinator.control_api)
|
||||
await self.entity_description.off_fn(self.coordinator)
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the on state."""
|
||||
return self.entity_description.value_fn(self.coordinator.read_api.data)
|
||||
return self.entity_description.value_fn(self.coordinator)
|
||||
|
||||
@@ -244,8 +244,8 @@ class IndexSensor(IQVIAEntity, SensorEntity):
|
||||
key = self.entity_description.key.split("_")[-1].title()
|
||||
|
||||
try:
|
||||
[period] = [p for p in data["periods"] if p["Type"] == key] # type: ignore[index]
|
||||
except TypeError:
|
||||
period = next(p for p in data["periods"] if p["Type"] == key) # type: ignore[index]
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
data = cast(dict[str, Any], data)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.1.1",
|
||||
"xknxproject==3.7.1",
|
||||
"knx-frontend==2024.8.9.225351"
|
||||
"knx-frontend==2024.9.4.64538"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
client=get_async_client(hass),
|
||||
)
|
||||
|
||||
# initialize local API
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lmcloud"],
|
||||
"requirements": ["lmcloud==1.1.13"]
|
||||
"requirements": ["lmcloud==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"models": [
|
||||
"LIFX A19",
|
||||
"LIFX A21",
|
||||
"LIFX B10",
|
||||
"LIFX Beam",
|
||||
"LIFX BR30",
|
||||
"LIFX Candle",
|
||||
@@ -41,7 +40,6 @@
|
||||
"LIFX Round",
|
||||
"LIFX Square",
|
||||
"LIFX String",
|
||||
"LIFX T10",
|
||||
"LIFX Tile",
|
||||
"LIFX White",
|
||||
"LIFX Z"
|
||||
@@ -50,7 +48,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.0.8",
|
||||
"aiolifx==1.0.9",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.5.0"
|
||||
]
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
"""Support for LinkPlay devices."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from linkplay.bridge import LinkPlayBridge
|
||||
from linkplay.discovery import linkplay_factory_bridge
|
||||
from linkplay.discovery import linkplay_factory_httpapi_bridge
|
||||
from linkplay.exceptions import LinkPlayRequestException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import PLATFORMS
|
||||
from .utils import async_get_client_session
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkPlayData:
|
||||
"""Data for LinkPlay."""
|
||||
|
||||
@@ -24,16 +29,17 @@ type LinkPlayConfigEntry = ConfigEntry[LinkPlayData]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool:
|
||||
"""Async setup hass config entry. Called when an entry has been setup."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
if (
|
||||
bridge := await linkplay_factory_bridge(entry.data[CONF_HOST], session)
|
||||
) is None:
|
||||
session: ClientSession = await async_get_client_session(hass)
|
||||
bridge: LinkPlayBridge | None = None
|
||||
|
||||
try:
|
||||
bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session)
|
||||
except LinkPlayRequestException as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}"
|
||||
)
|
||||
) from exception
|
||||
|
||||
entry.runtime_data = LinkPlayData()
|
||||
entry.runtime_data.bridge = bridge
|
||||
entry.runtime_data = LinkPlayData(bridge=bridge)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
"""Config flow to configure LinkPlay component."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from linkplay.discovery import linkplay_factory_bridge
|
||||
from aiohttp import ClientSession
|
||||
from linkplay.bridge import LinkPlayBridge
|
||||
from linkplay.discovery import linkplay_factory_httpapi_bridge
|
||||
from linkplay.exceptions import LinkPlayRequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import async_get_client_session
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -25,10 +31,15 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle Zeroconf discovery."""
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
bridge = await linkplay_factory_bridge(discovery_info.host, session)
|
||||
session: ClientSession = await async_get_client_session(self.hass)
|
||||
bridge: LinkPlayBridge | None = None
|
||||
|
||||
if bridge is None:
|
||||
try:
|
||||
bridge = await linkplay_factory_httpapi_bridge(discovery_info.host, session)
|
||||
except LinkPlayRequestException:
|
||||
_LOGGER.exception(
|
||||
"Failed to connect to LinkPlay device at %s", discovery_info.host
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
self.data[CONF_HOST] = discovery_info.host
|
||||
@@ -66,14 +77,26 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
bridge = await linkplay_factory_bridge(user_input[CONF_HOST], session)
|
||||
session: ClientSession = await async_get_client_session(self.hass)
|
||||
bridge: LinkPlayBridge | None = None
|
||||
|
||||
try:
|
||||
bridge = await linkplay_factory_httpapi_bridge(
|
||||
user_input[CONF_HOST], session
|
||||
)
|
||||
except LinkPlayRequestException:
|
||||
_LOGGER.exception(
|
||||
"Failed to connect to LinkPlay device at %s", user_input[CONF_HOST]
|
||||
)
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
if bridge is not None:
|
||||
self.data[CONF_HOST] = user_input[CONF_HOST]
|
||||
self.data[CONF_MODEL] = bridge.device.name
|
||||
|
||||
await self.async_set_unique_id(bridge.device.uuid)
|
||||
await self.async_set_unique_id(
|
||||
bridge.device.uuid, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.data[CONF_HOST]}
|
||||
)
|
||||
@@ -83,7 +106,6 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={CONF_HOST: self.data[CONF_HOST]},
|
||||
)
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
|
||||
@@ -4,3 +4,4 @@ from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "linkplay"
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
CONF_SESSION = "session"
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/linkplay",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-linkplay==0.0.8"],
|
||||
"requirements": ["python-linkplay==0.0.9"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -48,6 +48,17 @@ SOURCE_MAP: dict[PlayingMode, str] = {
|
||||
PlayingMode.XLR: "XLR",
|
||||
PlayingMode.HDMI: "HDMI",
|
||||
PlayingMode.OPTICAL_2: "Optical 2",
|
||||
PlayingMode.EXTERN_BLUETOOTH: "External Bluetooth",
|
||||
PlayingMode.PHONO: "Phono",
|
||||
PlayingMode.ARC: "ARC",
|
||||
PlayingMode.COAXIAL_2: "Coaxial 2",
|
||||
PlayingMode.TF_CARD_1: "SD Card 1",
|
||||
PlayingMode.TF_CARD_2: "SD Card 2",
|
||||
PlayingMode.CD: "CD",
|
||||
PlayingMode.DAB: "DAB Radio",
|
||||
PlayingMode.FM: "FM Radio",
|
||||
PlayingMode.RCA: "RCA",
|
||||
PlayingMode.UDISK: "USB",
|
||||
}
|
||||
|
||||
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
from typing import Final
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from linkplay.utils import async_create_unverified_client_session
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
|
||||
from .const import CONF_SESSION, DOMAIN
|
||||
|
||||
MANUFACTURER_ARTSOUND: Final[str] = "ArtSound"
|
||||
MANUFACTURER_ARYLIC: Final[str] = "Arylic"
|
||||
MANUFACTURER_IEAST: Final[str] = "iEAST"
|
||||
@@ -44,3 +52,22 @@ def get_info_from_project(project: str) -> tuple[str, str]:
|
||||
return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5
|
||||
case _:
|
||||
return MANUFACTURER_GENERIC, MODELS_GENERIC
|
||||
|
||||
|
||||
async def async_get_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Get a ClientSession that can be used with LinkPlay devices."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if CONF_SESSION not in hass.data[DOMAIN]:
|
||||
clientsession: ClientSession = await async_create_unverified_client_session()
|
||||
|
||||
@callback
|
||||
def _async_close_websession(event: Event) -> None:
|
||||
"""Close websession."""
|
||||
clientsession.detach()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession)
|
||||
hass.data[DOMAIN][CONF_SESSION] = clientsession
|
||||
return clientsession
|
||||
|
||||
session: ClientSession = hass.data[DOMAIN][CONF_SESSION]
|
||||
return session
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/madvr",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["py-madvr2==1.6.29"]
|
||||
"requirements": ["py-madvr2==1.6.32"]
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ThirdRealityEnergySensorWattAccumulated",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
|
||||
@@ -48,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
|
||||
),
|
||||
)
|
||||
try:
|
||||
await client.define_household_support()
|
||||
about = await client.get_about()
|
||||
version = create_version(about.version)
|
||||
except MealieAuthenticationError as error:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.8.1"]
|
||||
"requirements": ["aiomealie==0.9.2"]
|
||||
}
|
||||
|
||||
36
homeassistant/components/modern_forms/diagnostics.py
Normal file
36
homeassistant/components/modern_forms/diagnostics.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Diagnostics support for Modern Forms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ModernFormsDataUpdateCoordinator
|
||||
|
||||
REDACT_CONFIG = {CONF_MAC}
|
||||
REDACT_DEVICE_INFO = {"mac_address", "owner"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator is not None
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
|
||||
"device": {
|
||||
"info": async_redact_data(
|
||||
asdict(coordinator.modern_forms.info), REDACT_DEVICE_INFO
|
||||
),
|
||||
"status": asdict(coordinator.modern_forms.status),
|
||||
},
|
||||
}
|
||||
@@ -166,38 +166,43 @@ class SignalUpdateCallback:
|
||||
)
|
||||
if not device_entry:
|
||||
return
|
||||
supported_traits = self._supported_traits(device_id)
|
||||
for api_event_type, image_event in events.items():
|
||||
if not (event_type := EVENT_NAME_MAP.get(api_event_type)):
|
||||
continue
|
||||
nest_event_id = image_event.event_token
|
||||
attachment = {
|
||||
"image": EVENT_THUMBNAIL_URL_FORMAT.format(
|
||||
device_id=device_entry.id, event_token=image_event.event_token
|
||||
),
|
||||
}
|
||||
if self._supports_clip(device_id):
|
||||
attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format(
|
||||
device_id=device_entry.id, event_token=image_event.event_token
|
||||
)
|
||||
message = {
|
||||
"device_id": device_entry.id,
|
||||
"type": event_type,
|
||||
"timestamp": event_message.timestamp,
|
||||
"nest_event_id": nest_event_id,
|
||||
"attachment": attachment,
|
||||
}
|
||||
if (
|
||||
TraitType.CAMERA_EVENT_IMAGE in supported_traits
|
||||
or TraitType.CAMERA_CLIP_PREVIEW in supported_traits
|
||||
):
|
||||
attachment = {
|
||||
"image": EVENT_THUMBNAIL_URL_FORMAT.format(
|
||||
device_id=device_entry.id, event_token=image_event.event_token
|
||||
)
|
||||
}
|
||||
if TraitType.CAMERA_CLIP_PREVIEW in supported_traits:
|
||||
attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format(
|
||||
device_id=device_entry.id, event_token=image_event.event_token
|
||||
)
|
||||
message["attachment"] = attachment
|
||||
if image_event.zones:
|
||||
message["zones"] = image_event.zones
|
||||
self._hass.bus.async_fire(NEST_EVENT, message)
|
||||
|
||||
def _supports_clip(self, device_id: str) -> bool:
|
||||
def _supported_traits(self, device_id: str) -> list[TraitType]:
|
||||
if not (
|
||||
device_manager := self._hass.data[DOMAIN]
|
||||
.get(self._config_entry_id, {})
|
||||
.get(DATA_DEVICE_MANAGER)
|
||||
) or not (device := device_manager.devices.get(device_id)):
|
||||
return False
|
||||
return TraitType.CAMERA_CLIP_PREVIEW in device.traits
|
||||
return []
|
||||
return list(device.traits)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nice_go",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["nice-go"],
|
||||
"requirements": ["nice-go==0.3.5"]
|
||||
"requirements": ["nice-go==0.3.8"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import CONF_URL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import (
|
||||
CONF_KEEP_ALIVE,
|
||||
@@ -43,7 +44,7 @@ PLATFORMS = (Platform.CONVERSATION,)
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Ollama from a config entry."""
|
||||
settings = {**entry.data, **entry.options}
|
||||
client = ollama.AsyncClient(host=settings[CONF_URL])
|
||||
client = ollama.AsyncClient(host=settings[CONF_URL], verify=get_default_context())
|
||||
try:
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
await client.list()
|
||||
|
||||
@@ -33,6 +33,7 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import (
|
||||
CONF_KEEP_ALIVE,
|
||||
@@ -91,7 +92,9 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
self.client = ollama.AsyncClient(host=self.url)
|
||||
self.client = ollama.AsyncClient(
|
||||
host=self.url, verify=get_default_context()
|
||||
)
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
response = await self.client.list()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import pyeiscp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN,
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
@@ -28,9 +28,14 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "onkyo"
|
||||
|
||||
DATA_MP_ENTITIES: HassKey[list[dict[str, OnkyoMediaPlayer]]] = HassKey(DOMAIN)
|
||||
|
||||
CONF_SOURCES = "sources"
|
||||
CONF_MAX_VOLUME = "max_volume"
|
||||
CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume"
|
||||
@@ -148,6 +153,33 @@ class ReceiverInfo:
|
||||
identifier: str
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register Onkyo services."""
|
||||
|
||||
async def async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle for services."""
|
||||
entity_ids = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
targets: list[OnkyoMediaPlayer] = []
|
||||
for receiver_entities in hass.data[DATA_MP_ENTITIES]:
|
||||
targets.extend(
|
||||
entity
|
||||
for entity in receiver_entities.values()
|
||||
if entity.entity_id in entity_ids
|
||||
)
|
||||
|
||||
for target in targets:
|
||||
if service.service == SERVICE_SELECT_HDMI_OUTPUT:
|
||||
await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
|
||||
|
||||
hass.services.async_register(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_HDMI_OUTPUT,
|
||||
async_service_handle,
|
||||
schema=ONKYO_SELECT_OUTPUT_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -155,29 +187,10 @@ async def async_setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Onkyo platform."""
|
||||
await async_register_services(hass)
|
||||
|
||||
receivers: dict[str, pyeiscp.Connection] = {} # indexed by host
|
||||
entities: dict[str, dict[str, OnkyoMediaPlayer]] = {} # indexed by host and zone
|
||||
|
||||
async def async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle for services."""
|
||||
entity_ids = service.data[ATTR_ENTITY_ID]
|
||||
targets = [
|
||||
entity
|
||||
for h in entities.values()
|
||||
for entity in h.values()
|
||||
if entity.entity_id in entity_ids
|
||||
]
|
||||
|
||||
for target in targets:
|
||||
if service.service == SERVICE_SELECT_HDMI_OUTPUT:
|
||||
await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_HDMI_OUTPUT,
|
||||
async_service_handle,
|
||||
schema=ONKYO_SELECT_OUTPUT_SCHEMA,
|
||||
)
|
||||
all_entities = hass.data.setdefault(DATA_MP_ENTITIES, [])
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -188,6 +201,9 @@ async def async_setup_platform(
|
||||
async def async_setup_receiver(
|
||||
info: ReceiverInfo, discovered: bool, name: str | None
|
||||
) -> None:
|
||||
entities: dict[str, OnkyoMediaPlayer] = {}
|
||||
all_entities.append(entities)
|
||||
|
||||
@callback
|
||||
def async_onkyo_update_callback(
|
||||
message: tuple[str, str, Any], origin: str
|
||||
@@ -199,7 +215,7 @@ async def async_setup_platform(
|
||||
)
|
||||
|
||||
zone, _, value = message
|
||||
entity = entities[origin].get(zone)
|
||||
entity = entities.get(zone)
|
||||
if entity is not None:
|
||||
if entity.enabled:
|
||||
entity.process_update(message)
|
||||
@@ -210,7 +226,7 @@ async def async_setup_platform(
|
||||
zone_entity = OnkyoMediaPlayer(
|
||||
receiver, sources, zone, max_volume, receiver_max_volume
|
||||
)
|
||||
entities[origin][zone] = zone_entity
|
||||
entities[zone] = zone_entity
|
||||
async_add_entities([zone_entity])
|
||||
|
||||
@callback
|
||||
@@ -221,7 +237,7 @@ async def async_setup_platform(
|
||||
"Receiver (re)connected: %s (%s)", receiver.name, receiver.host
|
||||
)
|
||||
|
||||
for entity in entities[origin].values():
|
||||
for entity in entities.values():
|
||||
entity.backfill_state()
|
||||
|
||||
_LOGGER.debug("Creating receiver: %s (%s)", info.model_name, info.host)
|
||||
@@ -237,9 +253,7 @@ async def async_setup_platform(
|
||||
receiver.name = name or info.model_name
|
||||
receiver.discovered = discovered
|
||||
|
||||
# Store the receiver object and create a dictionary to store its entities.
|
||||
receivers[receiver.host] = receiver
|
||||
entities[receiver.host] = {}
|
||||
|
||||
# Discover what zones are available for the receiver by querying the power.
|
||||
# If we get a response for the specific zone, it means it is available.
|
||||
@@ -251,7 +265,7 @@ async def async_setup_platform(
|
||||
main_entity = OnkyoMediaPlayer(
|
||||
receiver, sources, "main", max_volume, receiver_max_volume
|
||||
)
|
||||
entities[receiver.host]["main"] = main_entity
|
||||
entities["main"] = main_entity
|
||||
async_add_entities([main_entity])
|
||||
|
||||
if host is not None:
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.exceptions import (
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
@@ -88,7 +89,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool:
|
||||
"""Set up OpenAI Conversation from a config entry."""
|
||||
client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY])
|
||||
client = openai.AsyncOpenAI(
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
http_client=get_async_client(hass),
|
||||
)
|
||||
|
||||
# Cache current platform data which gets added to each request (caching done by library)
|
||||
_ = await hass.async_add_executor_job(client.platform_headers)
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
|
||||
except openai.AuthenticationError as err:
|
||||
|
||||
@@ -110,7 +110,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
)
|
||||
|
||||
last_stat = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, cost_statistic_id, True, set()
|
||||
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
|
||||
)
|
||||
if not last_stat:
|
||||
_LOGGER.debug("Updating statistic for the first time")
|
||||
@@ -124,7 +124,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
cost_reads = await self._async_get_cost_reads(
|
||||
account,
|
||||
self.api.utility.timezone(),
|
||||
last_stat[cost_statistic_id][0]["start"],
|
||||
last_stat[consumption_statistic_id][0]["start"],
|
||||
)
|
||||
if not cost_reads:
|
||||
_LOGGER.debug("No recent usage/cost data. Skipping update")
|
||||
@@ -141,7 +141,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
)
|
||||
cost_sum = cast(float, stats[cost_statistic_id][0]["sum"])
|
||||
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
|
||||
last_stats_time = stats[cost_statistic_id][0]["start"]
|
||||
last_stats_time = stats[consumption_statistic_id][0]["start"]
|
||||
|
||||
cost_statistics = []
|
||||
consumption_statistics = []
|
||||
@@ -187,7 +187,17 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
else UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(cost_statistics),
|
||||
cost_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(self.hass, cost_metadata, cost_statistics)
|
||||
_LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(consumption_statistics),
|
||||
consumption_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(
|
||||
self.hass, consumption_metadata, consumption_statistics
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import (
|
||||
ConductivityConverter,
|
||||
DataRateConverter,
|
||||
DistanceConverter,
|
||||
DurationConverter,
|
||||
@@ -48,7 +49,7 @@ from .util import PERIOD_SCHEMA, get_instance, resolve_period
|
||||
|
||||
UNIT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("conductivity"): vol.In(DataRateConverter.VALID_UNITS),
|
||||
vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS),
|
||||
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
|
||||
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS),
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["renault-api==0.2.5"]
|
||||
"requirements": ["renault-api==0.2.7"]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
history_data = self._device.last_history
|
||||
if history_data:
|
||||
self._last_event = history_data[0]
|
||||
# will call async_update to update the attributes and get the
|
||||
# video url from the api
|
||||
self.async_schedule_update_ha_state(True)
|
||||
else:
|
||||
self._last_event = None
|
||||
@@ -183,7 +185,7 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
|
||||
await self._device.async_set_motion_detection(new_state)
|
||||
self._attr_motion_detection_enabled = new_state
|
||||
self.async_schedule_update_ha_state(False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_enable_motion_detection(self) -> None:
|
||||
"""Enable motion detection in the camera."""
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_2FA, DOMAIN
|
||||
|
||||
@@ -31,7 +32,10 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
auth = Auth(f"{APPLICATION_NAME}/{ha_version}")
|
||||
auth = Auth(
|
||||
f"{APPLICATION_NAME}/{ha_version}",
|
||||
http_client_session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
try:
|
||||
token = await auth.async_fetch_token(
|
||||
|
||||
@@ -86,7 +86,7 @@ class RingLight(RingEntity[RingStickUpCam], LightEntity):
|
||||
|
||||
self._attr_is_on = new_state == OnOffState.ON
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.async_schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on for 30 seconds."""
|
||||
|
||||
@@ -87,7 +87,7 @@ class SirenSwitch(BaseRingSwitch):
|
||||
|
||||
self._attr_is_on = new_state > 0
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.async_schedule_update_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on for 30 seconds."""
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioruckus", "xmltodict"],
|
||||
"requirements": ["aioruckus==0.34"]
|
||||
"loggers": ["aioruckus"],
|
||||
"requirements": ["aioruckus==0.41"]
|
||||
}
|
||||
|
||||
@@ -793,8 +793,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
|
||||
LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
|
||||
try:
|
||||
await self.device.update_status()
|
||||
await self.device.get_dynamic_components()
|
||||
await self.device.poll()
|
||||
except (DeviceConnectionError, RpcCallError) as err:
|
||||
raise UpdateFailed(f"Device disconnected: {err!r}") from err
|
||||
except InvalidAuthError:
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==11.3.0"],
|
||||
"requirements": ["aioshelly==11.4.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Support for controlling Sisyphus Kinetic Art Tables."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
# from sisyphus_control import Table
|
||||
from sisyphus_control import Table
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
"domain": "sisyphus",
|
||||
"name": "Sisyphus",
|
||||
"codeowners": ["@jkeljo"],
|
||||
"disabled": "This integration is disabled because it uses an old version of socketio.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sisyphus_control"],
|
||||
"requirements": ["sisyphus-control==3.1.3"]
|
||||
"requirements": ["sisyphus-control==3.1.4"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Support for track controls on the Sisyphus Kinetic Art Table."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
from sisyphus_control import Track
|
||||
|
||||
# from sisyphus_control import Track
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.1", "numpy==1.26.0"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5", "ha-av==10.1.1", "numpy==1.26.0"]
|
||||
}
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"requirements": ["PySwitchbot==0.48.1"]
|
||||
"requirements": ["PySwitchbot==0.48.2"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.climate import (
|
||||
SWING_BOTH,
|
||||
SWING_HORIZONTAL,
|
||||
SWING_OFF,
|
||||
SWING_ON,
|
||||
SWING_VERTICAL,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
@@ -47,7 +48,6 @@ from .const import (
|
||||
HA_TO_TADO_FAN_MODE_MAP,
|
||||
HA_TO_TADO_FAN_MODE_MAP_LEGACY,
|
||||
HA_TO_TADO_HVAC_MODE_MAP,
|
||||
HA_TO_TADO_SWING_MODE_MAP,
|
||||
ORDERED_KNOWN_TADO_MODES,
|
||||
PRESET_AUTO,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
@@ -55,17 +55,20 @@ from .const import (
|
||||
SUPPORT_PRESET_MANUAL,
|
||||
TADO_DEFAULT_MAX_TEMP,
|
||||
TADO_DEFAULT_MIN_TEMP,
|
||||
TADO_FAN_LEVELS,
|
||||
TADO_FAN_SPEEDS,
|
||||
TADO_FANLEVEL_SETTING,
|
||||
TADO_FANSPEED_SETTING,
|
||||
TADO_HORIZONTAL_SWING_SETTING,
|
||||
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
|
||||
TADO_MODES_WITH_NO_TEMP_SETTING,
|
||||
TADO_SWING_OFF,
|
||||
TADO_SWING_ON,
|
||||
TADO_SWING_SETTING,
|
||||
TADO_TO_HA_FAN_MODE_MAP,
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY,
|
||||
TADO_TO_HA_HVAC_MODE_MAP,
|
||||
TADO_TO_HA_OFFSET_MAP,
|
||||
TADO_TO_HA_SWING_MODE_MAP,
|
||||
TADO_VERTICAL_SWING_SETTING,
|
||||
TEMP_OFFSET,
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_HEATING,
|
||||
@@ -166,29 +169,30 @@ def create_climate_entity(
|
||||
|
||||
supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode])
|
||||
if (
|
||||
capabilities[mode].get("swings")
|
||||
or capabilities[mode].get("verticalSwing")
|
||||
or capabilities[mode].get("horizontalSwing")
|
||||
TADO_SWING_SETTING in capabilities[mode]
|
||||
or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
|
||||
or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
|
||||
):
|
||||
support_flags |= ClimateEntityFeature.SWING_MODE
|
||||
supported_swing_modes = []
|
||||
if capabilities[mode].get("swings"):
|
||||
if TADO_SWING_SETTING in capabilities[mode]:
|
||||
supported_swing_modes.append(
|
||||
TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON]
|
||||
)
|
||||
if capabilities[mode].get("verticalSwing"):
|
||||
if TADO_VERTICAL_SWING_SETTING in capabilities[mode]:
|
||||
supported_swing_modes.append(SWING_VERTICAL)
|
||||
if capabilities[mode].get("horizontalSwing"):
|
||||
if TADO_HORIZONTAL_SWING_SETTING in capabilities[mode]:
|
||||
supported_swing_modes.append(SWING_HORIZONTAL)
|
||||
if (
|
||||
SWING_HORIZONTAL in supported_swing_modes
|
||||
and SWING_HORIZONTAL in supported_swing_modes
|
||||
and SWING_VERTICAL in supported_swing_modes
|
||||
):
|
||||
supported_swing_modes.append(SWING_BOTH)
|
||||
supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF])
|
||||
|
||||
if not capabilities[mode].get("fanSpeeds") and not capabilities[mode].get(
|
||||
"fanLevel"
|
||||
if (
|
||||
TADO_FANSPEED_SETTING not in capabilities[mode]
|
||||
and TADO_FANLEVEL_SETTING not in capabilities[mode]
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -197,14 +201,15 @@ def create_climate_entity(
|
||||
if supported_fan_modes:
|
||||
continue
|
||||
|
||||
if capabilities[mode].get("fanSpeeds"):
|
||||
if TADO_FANSPEED_SETTING in capabilities[mode]:
|
||||
supported_fan_modes = generate_supported_fanmodes(
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"]
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY,
|
||||
capabilities[mode][TADO_FANSPEED_SETTING],
|
||||
)
|
||||
|
||||
else:
|
||||
supported_fan_modes = generate_supported_fanmodes(
|
||||
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"]
|
||||
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode][TADO_FANLEVEL_SETTING]
|
||||
)
|
||||
|
||||
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
|
||||
@@ -316,12 +321,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
self._target_temp: float | None = None
|
||||
|
||||
self._current_tado_fan_speed = CONST_FAN_OFF
|
||||
self._current_tado_fan_level = CONST_FAN_OFF
|
||||
self._current_tado_hvac_mode = CONST_MODE_OFF
|
||||
self._current_tado_hvac_action = HVACAction.OFF
|
||||
self._current_tado_swing_mode = TADO_SWING_OFF
|
||||
self._current_tado_vertical_swing = TADO_SWING_OFF
|
||||
self._current_tado_horizontal_swing = TADO_SWING_OFF
|
||||
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
self._current_tado_capabilities = capabilities
|
||||
|
||||
self._tado_zone_data: PyTado.TadoZone = {}
|
||||
self._tado_geofence_data: dict[str, str] | None = None
|
||||
|
||||
@@ -382,20 +391,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the fan setting."""
|
||||
if self._ac_device:
|
||||
return TADO_TO_HA_FAN_MODE_MAP.get(
|
||||
self._current_tado_fan_speed,
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY.get(
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
return TADO_TO_HA_FAN_MODE_MAP_LEGACY.get(
|
||||
self._current_tado_fan_speed, FAN_AUTO
|
||||
),
|
||||
)
|
||||
)
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
return TADO_TO_HA_FAN_MODE_MAP.get(
|
||||
self._current_tado_fan_level, FAN_AUTO
|
||||
)
|
||||
return FAN_AUTO
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Turn fan on/off."""
|
||||
if self._current_tado_fan_speed in TADO_FAN_LEVELS:
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
|
||||
else:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode])
|
||||
elif self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
@@ -555,24 +567,30 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
swing = None
|
||||
if self._attr_swing_modes is None:
|
||||
return
|
||||
if (
|
||||
SWING_VERTICAL in self._attr_swing_modes
|
||||
or SWING_HORIZONTAL in self._attr_swing_modes
|
||||
):
|
||||
if swing_mode == SWING_VERTICAL:
|
||||
if swing_mode == SWING_OFF:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
|
||||
swing = TADO_SWING_OFF
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
horizontal_swing = TADO_SWING_OFF
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
vertical_swing = TADO_SWING_OFF
|
||||
if swing_mode == SWING_ON:
|
||||
swing = TADO_SWING_ON
|
||||
if swing_mode == SWING_VERTICAL:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
vertical_swing = TADO_SWING_ON
|
||||
elif swing_mode == SWING_HORIZONTAL:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
horizontal_swing = TADO_SWING_OFF
|
||||
if swing_mode == SWING_HORIZONTAL:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
vertical_swing = TADO_SWING_OFF
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
horizontal_swing = TADO_SWING_ON
|
||||
elif swing_mode == SWING_BOTH:
|
||||
if swing_mode == SWING_BOTH:
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
vertical_swing = TADO_SWING_ON
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
horizontal_swing = TADO_SWING_ON
|
||||
elif swing_mode == SWING_OFF:
|
||||
if SWING_VERTICAL in self._attr_swing_modes:
|
||||
vertical_swing = TADO_SWING_OFF
|
||||
if SWING_HORIZONTAL in self._attr_swing_modes:
|
||||
horizontal_swing = TADO_SWING_OFF
|
||||
else:
|
||||
swing = HA_TO_TADO_SWING_MODE_MAP[swing_mode]
|
||||
|
||||
self._control_hvac(
|
||||
swing_mode=swing,
|
||||
@@ -596,21 +614,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
self._device_id
|
||||
][TEMP_OFFSET][offset_key]
|
||||
|
||||
self._current_tado_fan_speed = (
|
||||
self._tado_zone_data.current_fan_level
|
||||
if self._tado_zone_data.current_fan_level is not None
|
||||
else self._tado_zone_data.current_fan_speed
|
||||
)
|
||||
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
|
||||
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
|
||||
self._current_tado_vertical_swing = (
|
||||
self._tado_zone_data.current_vertical_swing_mode
|
||||
)
|
||||
self._current_tado_horizontal_swing = (
|
||||
self._tado_zone_data.current_horizontal_swing_mode
|
||||
)
|
||||
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._current_tado_fan_level = self._tado_zone_data.current_fan_level
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
|
||||
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
|
||||
self._current_tado_vertical_swing = (
|
||||
self._tado_zone_data.current_vertical_swing_mode
|
||||
)
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
|
||||
self._current_tado_horizontal_swing = (
|
||||
self._tado_zone_data.current_horizontal_swing_mode
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_zone_callback(self) -> None:
|
||||
@@ -665,7 +685,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
self._target_temp = target_temp
|
||||
|
||||
if fan_mode:
|
||||
self._current_tado_fan_speed = fan_mode
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
|
||||
self._current_tado_fan_speed = fan_mode
|
||||
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
|
||||
self._current_tado_fan_level = fan_mode
|
||||
|
||||
if swing_mode:
|
||||
self._current_tado_swing_mode = swing_mode
|
||||
@@ -735,21 +758,32 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
fan_speed = None
|
||||
fan_level = None
|
||||
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
||||
if self._current_tado_fan_speed in TADO_FAN_LEVELS:
|
||||
fan_level = self._current_tado_fan_speed
|
||||
elif self._current_tado_fan_speed in TADO_FAN_SPEEDS:
|
||||
if self._is_current_setting_supported_by_current_hvac_mode(
|
||||
TADO_FANSPEED_SETTING, self._current_tado_fan_speed
|
||||
):
|
||||
fan_speed = self._current_tado_fan_speed
|
||||
if self._is_current_setting_supported_by_current_hvac_mode(
|
||||
TADO_FANLEVEL_SETTING, self._current_tado_fan_level
|
||||
):
|
||||
fan_level = self._current_tado_fan_level
|
||||
|
||||
swing = None
|
||||
vertical_swing = None
|
||||
horizontal_swing = None
|
||||
if (
|
||||
self.supported_features & ClimateEntityFeature.SWING_MODE
|
||||
) and self._attr_swing_modes is not None:
|
||||
if SWING_VERTICAL in self._attr_swing_modes:
|
||||
if self._is_current_setting_supported_by_current_hvac_mode(
|
||||
TADO_VERTICAL_SWING_SETTING, self._current_tado_vertical_swing
|
||||
):
|
||||
vertical_swing = self._current_tado_vertical_swing
|
||||
if SWING_HORIZONTAL in self._attr_swing_modes:
|
||||
if self._is_current_setting_supported_by_current_hvac_mode(
|
||||
TADO_HORIZONTAL_SWING_SETTING, self._current_tado_horizontal_swing
|
||||
):
|
||||
horizontal_swing = self._current_tado_horizontal_swing
|
||||
if vertical_swing is None and horizontal_swing is None:
|
||||
if self._is_current_setting_supported_by_current_hvac_mode(
|
||||
TADO_SWING_SETTING, self._current_tado_swing_mode
|
||||
):
|
||||
swing = self._current_tado_swing_mode
|
||||
|
||||
self._tado.set_zone_overlay(
|
||||
@@ -765,3 +799,20 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None
|
||||
horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None
|
||||
)
|
||||
|
||||
def _is_valid_setting_for_hvac_mode(self, setting: str) -> bool:
|
||||
return (
|
||||
self._current_tado_capabilities.get(self._current_tado_hvac_mode, {}).get(
|
||||
setting
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
def _is_current_setting_supported_by_current_hvac_mode(
|
||||
self, setting: str, current_state: str | None
|
||||
) -> bool:
|
||||
if self._is_valid_setting_for_hvac_mode(setting):
|
||||
return current_state in self._current_tado_capabilities[
|
||||
self._current_tado_hvac_mode
|
||||
].get(setting, [])
|
||||
return False
|
||||
|
||||
@@ -234,3 +234,10 @@ CONF_READING = "reading"
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback"
|
||||
|
||||
TADO_SWING_SETTING = "swings"
|
||||
TADO_FANSPEED_SETTING = "fanSpeeds"
|
||||
|
||||
TADO_FANLEVEL_SETTING = "fanLevel"
|
||||
TADO_VERTICAL_SWING_SETTING = "verticalSwing"
|
||||
TADO_HORIZONTAL_SWING_SETTING = "horizontalSwing"
|
||||
|
||||
@@ -41,6 +41,7 @@ from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_loaded_integration
|
||||
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -378,7 +379,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
for p_config in domain_config:
|
||||
# Each platform config gets its own bot
|
||||
bot = initialize_bot(hass, p_config)
|
||||
bot = await hass.async_add_executor_job(initialize_bot, hass, p_config)
|
||||
p_type: str = p_config[CONF_PLATFORM]
|
||||
|
||||
platform = platforms[p_type]
|
||||
@@ -486,7 +487,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot:
|
||||
# Auth can actually be stuffed into the URL, but the docs have previously
|
||||
# indicated to put them here.
|
||||
auth = proxy_params.pop("username"), proxy_params.pop("password")
|
||||
ir.async_create_issue(
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_auth_deprecation",
|
||||
@@ -503,7 +504,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot:
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/112778",
|
||||
)
|
||||
else:
|
||||
ir.async_create_issue(
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_deprecation",
|
||||
@@ -852,7 +853,11 @@ class TelegramNotificationService:
|
||||
username=kwargs.get(ATTR_USERNAME),
|
||||
password=kwargs.get(ATTR_PASSWORD),
|
||||
authentication=kwargs.get(ATTR_AUTHENTICATION),
|
||||
verify_ssl=kwargs.get(ATTR_VERIFY_SSL),
|
||||
verify_ssl=(
|
||||
get_default_context()
|
||||
if kwargs.get(ATTR_VERIFY_SSL, False)
|
||||
else get_default_no_verify_context()
|
||||
),
|
||||
)
|
||||
|
||||
if file_content:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/telegram_bot",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["telegram"],
|
||||
"requirements": ["python-telegram-bot[socks]==21.0.1"]
|
||||
"requirements": ["python-telegram-bot[socks]==21.5"]
|
||||
}
|
||||
|
||||
@@ -25,14 +25,22 @@ async def async_setup_platform(hass, bot, config):
|
||||
|
||||
async def process_error(update: Update, context: CallbackContext) -> None:
|
||||
"""Telegram bot error handler."""
|
||||
if context.error:
|
||||
error_callback(context.error, update)
|
||||
|
||||
|
||||
def error_callback(error: Exception, update: Update | None = None) -> None:
|
||||
"""Log the error."""
|
||||
try:
|
||||
if context.error:
|
||||
raise context.error
|
||||
raise error
|
||||
except (TimedOut, NetworkError, RetryAfter):
|
||||
# Long polling timeout or connection problem. Nothing serious.
|
||||
pass
|
||||
except TelegramError:
|
||||
_LOGGER.error('Update "%s" caused error: "%s"', update, context.error)
|
||||
if update is not None:
|
||||
_LOGGER.error('Update "%s" caused error: "%s"', update, error)
|
||||
else:
|
||||
_LOGGER.error("%s: %s", error.__class__.__name__, error)
|
||||
|
||||
|
||||
class PollBot(BaseTelegramBotEntity):
|
||||
@@ -53,7 +61,7 @@ class PollBot(BaseTelegramBotEntity):
|
||||
"""Start the polling task."""
|
||||
_LOGGER.debug("Starting polling")
|
||||
await self.application.initialize()
|
||||
await self.application.updater.start_polling()
|
||||
await self.application.updater.start_polling(error_callback=error_callback)
|
||||
await self.application.start()
|
||||
|
||||
async def stop_polling(self, event=None):
|
||||
|
||||
@@ -70,7 +70,7 @@ NUMBER_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_STEP): cv.template,
|
||||
vol.Optional(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_MIN): cv.template,
|
||||
vol.Optional(CONF_MAX): cv.template,
|
||||
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
|
||||
@@ -154,11 +154,10 @@ class TemplateNumber(TemplateEntity, NumberEntity):
|
||||
super().__init__(hass, config=config, unique_id=unique_id)
|
||||
assert self._attr_name is not None
|
||||
self._value_template = config[CONF_STATE]
|
||||
self._command_set_value = (
|
||||
Script(hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN)
|
||||
if config.get(CONF_SET_VALUE, None) is not None
|
||||
else None
|
||||
self._command_set_value = Script(
|
||||
hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN
|
||||
)
|
||||
|
||||
self._step_template = config[CONF_STEP]
|
||||
self._min_value_template = config[CONF_MIN]
|
||||
self._max_value_template = config[CONF_MAX]
|
||||
|
||||
@@ -68,6 +68,8 @@ EXCLUDED_FEATURES = {
|
||||
# update
|
||||
"current_firmware_version",
|
||||
"available_firmware_version",
|
||||
"update_available",
|
||||
"check_latest_firmware",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -301,5 +301,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kasa"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-kasa[speedups]==0.7.1"]
|
||||
"requirements": ["python-kasa[speedups]==0.7.2"]
|
||||
}
|
||||
|
||||
@@ -112,61 +112,36 @@ def _build_entities(
|
||||
|
||||
entities: list[ViCareBinarySensor] = []
|
||||
for device in device_list:
|
||||
entities.extend(_build_entities_for_device(device.api, device.config))
|
||||
# add device entities
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_circuits(device.api), device.config, CIRCUIT_SENSORS
|
||||
ViCareBinarySensor(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
)
|
||||
for description in GLOBAL_SENSORS
|
||||
if is_supported(description.key, description, device.api)
|
||||
)
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_burners(device.api), device.config, BURNER_SENSORS
|
||||
# add component entities
|
||||
for component_list, entity_description_list in (
|
||||
(get_circuits(device.api), CIRCUIT_SENSORS),
|
||||
(get_burners(device.api), BURNER_SENSORS),
|
||||
(get_compressors(device.api), COMPRESSOR_SENSORS),
|
||||
):
|
||||
entities.extend(
|
||||
ViCareBinarySensor(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
component,
|
||||
)
|
||||
for component in component_list
|
||||
for description in entity_description_list
|
||||
if is_supported(description.key, description, component)
|
||||
)
|
||||
)
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_compressors(device.api), device.config, COMPRESSOR_SENSORS
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
def _build_entities_for_device(
|
||||
device: PyViCareDevice,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
) -> list[ViCareBinarySensor]:
|
||||
"""Create device specific ViCare binary sensor entities."""
|
||||
|
||||
return [
|
||||
ViCareBinarySensor(
|
||||
device_config,
|
||||
device,
|
||||
description,
|
||||
)
|
||||
for description in GLOBAL_SENSORS
|
||||
if is_supported(description.key, description, device)
|
||||
]
|
||||
|
||||
|
||||
def _build_entities_for_component(
|
||||
components: list[PyViCareHeatingDeviceComponent],
|
||||
device_config: PyViCareDeviceConfig,
|
||||
entity_descriptions: tuple[ViCareBinarySensorEntityDescription, ...],
|
||||
) -> list[ViCareBinarySensor]:
|
||||
"""Create component specific ViCare binary sensor entities."""
|
||||
|
||||
return [
|
||||
ViCareBinarySensor(
|
||||
device_config,
|
||||
component,
|
||||
description,
|
||||
)
|
||||
for component in components
|
||||
for description in entity_descriptions
|
||||
if is_supported(description.key, description, component)
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -190,12 +165,13 @@ class ViCareBinarySensor(ViCareEntity, BinarySensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
|
||||
description: ViCareBinarySensorEntityDescription,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
device: PyViCareDevice,
|
||||
component: PyViCareHeatingDeviceComponent | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(device_config, api, description.key)
|
||||
super().__init__(description.key, device_config, device, component)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
|
||||
@@ -54,9 +54,9 @@ def _build_entities(
|
||||
|
||||
return [
|
||||
ViCareButton(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
description,
|
||||
)
|
||||
for device in device_list
|
||||
for description in BUTTON_DESCRIPTIONS
|
||||
@@ -87,12 +87,12 @@ class ViCareButton(ViCareEntity, ButtonEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: ViCareButtonEntityDescription,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
device: PyViCareDevice,
|
||||
description: ViCareButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(device_config, device, description.key)
|
||||
super().__init__(description.key, device_config, device)
|
||||
self.entity_description = description
|
||||
|
||||
def press(self) -> None:
|
||||
|
||||
@@ -148,7 +148,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity):
|
||||
circuit: PyViCareHeatingCircuit,
|
||||
) -> None:
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(device_config, device, circuit.id)
|
||||
super().__init__(circuit.id, device_config, device)
|
||||
self._circuit = circuit
|
||||
self._attributes: dict[str, Any] = {}
|
||||
self._attributes["vicare_programs"] = self._circuit.getPrograms()
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from PyViCare.PyViCareDevice import Device as PyViCareDevice
|
||||
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
|
||||
from PyViCare.PyViCareHeatingDevice import (
|
||||
HeatingDeviceWithComponent as PyViCareHeatingDeviceComponent,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -16,21 +19,24 @@ class ViCareEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id_suffix: str,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
device: PyViCareDevice,
|
||||
unique_id_suffix: str,
|
||||
component: PyViCareHeatingDeviceComponent | None = None,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._api = device
|
||||
self._api: PyViCareDevice | PyViCareHeatingDeviceComponent = (
|
||||
component if component else device
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{device_config.getConfig().serial}-{unique_id_suffix}"
|
||||
# valid for compressors, circuits, burners (HeatingDeviceWithComponent)
|
||||
if hasattr(device, "id"):
|
||||
self._attr_unique_id += f"-{device.id}"
|
||||
if component:
|
||||
self._attr_unique_id += f"-{component.id}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_config.getConfig().serial)},
|
||||
serial_number=device_config.getConfig().serial,
|
||||
serial_number=device.getSerial(),
|
||||
name=device_config.getModel(),
|
||||
manufacturer="Viessmann",
|
||||
model=device_config.getModel(),
|
||||
|
||||
@@ -129,7 +129,7 @@ class ViCareFan(ViCareEntity, FanEntity):
|
||||
device: PyViCareDevice,
|
||||
) -> None:
|
||||
"""Initialize the fan entity."""
|
||||
super().__init__(device_config, device, self._attr_translation_key)
|
||||
super().__init__(self._attr_translation_key, device_config, device)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update state of fan."""
|
||||
|
||||
@@ -233,30 +233,30 @@ def _build_entities(
|
||||
) -> list[ViCareNumber]:
|
||||
"""Create ViCare number entities for a device."""
|
||||
|
||||
entities: list[ViCareNumber] = [
|
||||
ViCareNumber(
|
||||
device.config,
|
||||
device.api,
|
||||
description,
|
||||
)
|
||||
for device in device_list
|
||||
for description in DEVICE_ENTITY_DESCRIPTIONS
|
||||
if is_supported(description.key, description, device.api)
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
entities: list[ViCareNumber] = []
|
||||
for device in device_list:
|
||||
# add device entities
|
||||
entities.extend(
|
||||
ViCareNumber(
|
||||
device.config,
|
||||
circuit,
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
)
|
||||
for description in DEVICE_ENTITY_DESCRIPTIONS
|
||||
if is_supported(description.key, description, device.api)
|
||||
)
|
||||
# add component entities
|
||||
entities.extend(
|
||||
ViCareNumber(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
circuit,
|
||||
)
|
||||
for device in device_list
|
||||
for circuit in get_circuits(device.api)
|
||||
for description in CIRCUIT_ENTITY_DESCRIPTIONS
|
||||
if is_supported(description.key, description, circuit)
|
||||
]
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
@@ -283,12 +283,13 @@ class ViCareNumber(ViCareEntity, NumberEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
|
||||
description: ViCareNumberEntityDescription,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
device: PyViCareDevice,
|
||||
component: PyViCareHeatingDeviceComponent | None = None,
|
||||
) -> None:
|
||||
"""Initialize the number."""
|
||||
super().__init__(device_config, api, description.key)
|
||||
super().__init__(description.key, device_config, device, component)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
|
||||
@@ -747,7 +747,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
|
||||
ViCareSensorEntityDescription(
|
||||
key="supply_temperature",
|
||||
@@ -865,61 +864,36 @@ def _build_entities(
|
||||
|
||||
entities: list[ViCareSensor] = []
|
||||
for device in device_list:
|
||||
entities.extend(_build_entities_for_device(device.api, device.config))
|
||||
# add device entities
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_circuits(device.api), device.config, CIRCUIT_SENSORS
|
||||
ViCareSensor(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
)
|
||||
for description in GLOBAL_SENSORS
|
||||
if is_supported(description.key, description, device.api)
|
||||
)
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_burners(device.api), device.config, BURNER_SENSORS
|
||||
# add component entities
|
||||
for component_list, entity_description_list in (
|
||||
(get_circuits(device.api), CIRCUIT_SENSORS),
|
||||
(get_burners(device.api), BURNER_SENSORS),
|
||||
(get_compressors(device.api), COMPRESSOR_SENSORS),
|
||||
):
|
||||
entities.extend(
|
||||
ViCareSensor(
|
||||
description,
|
||||
device.config,
|
||||
device.api,
|
||||
component,
|
||||
)
|
||||
for component in component_list
|
||||
for description in entity_description_list
|
||||
if is_supported(description.key, description, component)
|
||||
)
|
||||
)
|
||||
entities.extend(
|
||||
_build_entities_for_component(
|
||||
get_compressors(device.api), device.config, COMPRESSOR_SENSORS
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
def _build_entities_for_device(
|
||||
device: PyViCareDevice,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
) -> list[ViCareSensor]:
|
||||
"""Create device specific ViCare sensor entities."""
|
||||
|
||||
return [
|
||||
ViCareSensor(
|
||||
device_config,
|
||||
device,
|
||||
description,
|
||||
)
|
||||
for description in GLOBAL_SENSORS
|
||||
if is_supported(description.key, description, device)
|
||||
]
|
||||
|
||||
|
||||
def _build_entities_for_component(
|
||||
components: list[PyViCareHeatingDeviceComponent],
|
||||
device_config: PyViCareDeviceConfig,
|
||||
entity_descriptions: tuple[ViCareSensorEntityDescription, ...],
|
||||
) -> list[ViCareSensor]:
|
||||
"""Create component specific ViCare sensor entities."""
|
||||
|
||||
return [
|
||||
ViCareSensor(
|
||||
device_config,
|
||||
component,
|
||||
description,
|
||||
)
|
||||
for component in components
|
||||
for description in entity_descriptions
|
||||
if is_supported(description.key, description, component)
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -943,12 +917,13 @@ class ViCareSensor(ViCareEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
|
||||
description: ViCareSensorEntityDescription,
|
||||
device_config: PyViCareDeviceConfig,
|
||||
device: PyViCareDevice,
|
||||
component: PyViCareHeatingDeviceComponent | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(device_config, api, description.key)
|
||||
super().__init__(description.key, device_config, device, component)
|
||||
self.entity_description = description
|
||||
# run update to have device_class set depending on unit_of_measurement
|
||||
self.update()
|
||||
|
||||
@@ -113,7 +113,7 @@ class ViCareWater(ViCareEntity, WaterHeaterEntity):
|
||||
circuit: PyViCareHeatingCircuit,
|
||||
) -> None:
|
||||
"""Initialize the DHW water_heater device."""
|
||||
super().__init__(device_config, device, circuit.id)
|
||||
super().__init__(circuit.id, device_config, device)
|
||||
self._circuit = circuit
|
||||
self._attributes: dict[str, Any] = {}
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["weatherflow4py"],
|
||||
"requirements": ["weatherflow4py==0.2.21"]
|
||||
"requirements": ["weatherflow4py==0.2.23"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user