mirror of
https://github.com/home-assistant/core.git
synced 2026-08-14 09:13:06 +01:00
Merge branch 'dev' into programTemp
This commit is contained in:
+3
-2
@@ -404,6 +404,9 @@ omit =
|
||||
homeassistant/components/fjaraskupan/sensor.py
|
||||
homeassistant/components/fleetgo/device_tracker.py
|
||||
homeassistant/components/flexit/climate.py
|
||||
homeassistant/components/flexit_bacnet/__init__.py
|
||||
homeassistant/components/flexit_bacnet/const.py
|
||||
homeassistant/components/flexit_bacnet/climate.py
|
||||
homeassistant/components/flic/binary_sensor.py
|
||||
homeassistant/components/flick_electric/__init__.py
|
||||
homeassistant/components/flick_electric/sensor.py
|
||||
@@ -633,8 +636,6 @@ omit =
|
||||
homeassistant/components/kodi/browse_media.py
|
||||
homeassistant/components/kodi/media_player.py
|
||||
homeassistant/components/kodi/notify.py
|
||||
homeassistant/components/komfovent/__init__.py
|
||||
homeassistant/components/komfovent/climate.py
|
||||
homeassistant/components/konnected/__init__.py
|
||||
homeassistant/components/konnected/panel.py
|
||||
homeassistant/components/konnected/switch.py
|
||||
|
||||
@@ -128,6 +128,7 @@ homeassistant.components.file_upload.*
|
||||
homeassistant.components.filesize.*
|
||||
homeassistant.components.filter.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flexit_bacnet.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
homeassistant.components.fritz.*
|
||||
|
||||
+4
-4
@@ -395,6 +395,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fivem/ @Sander0542
|
||||
/homeassistant/components/fjaraskupan/ @elupus
|
||||
/tests/components/fjaraskupan/ @elupus
|
||||
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/homeassistant/components/flick_electric/ @ZephireNZ
|
||||
/tests/components/flick_electric/ @ZephireNZ
|
||||
/homeassistant/components/flipr/ @cnico
|
||||
@@ -663,8 +665,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/komfovent/ @ProstoSanja
|
||||
/tests/components/komfovent/ @ProstoSanja
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
@@ -1398,8 +1398,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/versasense/ @imstevenxyz
|
||||
/homeassistant/components/version/ @ludeeus
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "flexit",
|
||||
"name": "Flexit",
|
||||
"integrations": ["flexit", "flexit_bacnet"]
|
||||
}
|
||||
@@ -9,6 +9,7 @@ from typing import Final
|
||||
|
||||
from apcaccess import status
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -32,6 +33,8 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
||||
updates from the server.
|
||||
"""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
|
||||
"""Initialize the data object."""
|
||||
super().__init__(
|
||||
@@ -70,13 +73,10 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
||||
return self.data.get("SERIALNO")
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo | None:
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
|
||||
if not self.ups_serial_no:
|
||||
return None
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.ups_serial_no)},
|
||||
identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)},
|
||||
model=self.ups_model,
|
||||
manufacturer="APC",
|
||||
name=self.ups_name if self.ups_name else "APC UPS",
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .services import setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,7 +74,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Blink."""
|
||||
|
||||
await async_setup_services(hass)
|
||||
setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Services for the Blink integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@@ -14,7 +12,7 @@ from homeassistant.const import (
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
@@ -27,56 +25,67 @@ from .const import (
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILENAME): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_DEVICE_ID): cv.ensure_list, vol.Optional(CONF_PIN): cv.string}
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Blink integration."""
|
||||
|
||||
async def collect_coordinators(
|
||||
def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[BlinkUpdateCoordinator]:
|
||||
config_entries = list[ConfigEntry]()
|
||||
config_entries: list[ConfigEntry] = []
|
||||
registry = dr.async_get(hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries = list[ConfigEntry]()
|
||||
device_entries: list[ConfigEntry] = []
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' is not a {DOMAIN} device"
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device",
|
||||
translation_placeholders={"target": target, "domain": DOMAIN},
|
||||
)
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' not found in device registry"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"target": target},
|
||||
)
|
||||
coordinators = list[BlinkUpdateCoordinator]()
|
||||
|
||||
coordinators: list[BlinkUpdateCoordinator] = []
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
|
||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
@@ -85,24 +94,36 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
camera_name = call.data[CONF_NAME]
|
||||
video_path = call.data[CONF_FILENAME]
|
||||
if not hass.config.is_allowed_path(video_path):
|
||||
_LOGGER.error("Can't write %s, no access to path!", video_path)
|
||||
return
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": video_path},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
await all_cameras[camera_name].video_to_file(video_path)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
|
||||
"""Save multiple recent clips to output directory."""
|
||||
camera_name = call.data[CONF_NAME]
|
||||
clips_dir = call.data[CONF_FILE_PATH]
|
||||
if not hass.config.is_allowed_path(clips_dir):
|
||||
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
|
||||
return
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": clips_dir},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
@@ -110,11 +131,15 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
output_dir=clips_dir
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write recent clips to directory: %s", err)
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
@@ -122,7 +147,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def blink_refresh(call: ServiceCall):
|
||||
"""Call blink to refresh info."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.api.refresh(force_cache=True)
|
||||
|
||||
# Register all the above services
|
||||
|
||||
@@ -101,5 +101,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_device": {
|
||||
"message": "Device '{target}' is not a {domain} device"
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Device '{target}' not found in device registry"
|
||||
},
|
||||
"no_path": {
|
||||
"message": "Can't write to directory {target}, no access to path!"
|
||||
},
|
||||
"cant_write": {
|
||||
"message": "Can't write to file"
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import logging
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from bleak import BleakError
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
|
||||
from .passive_update_coordinator import PassiveBluetoothDataUpdateCoordinator
|
||||
|
||||
@@ -9,10 +9,10 @@ import logging
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from bleak import BleakError
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
|
||||
from .passive_update_processor import PassiveBluetoothProcessorCoordinator
|
||||
|
||||
@@ -14,6 +14,7 @@ from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from bleak_retry_connector import NO_RSSI_VALUE
|
||||
from bluetooth_adapters import DiscoveredDeviceAdvertisementData, adapter_human_name
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
@@ -25,7 +26,6 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from . import models
|
||||
from .const import (
|
||||
|
||||
@@ -16,6 +16,7 @@ from bluetooth_adapters import (
|
||||
AdapterDetails,
|
||||
BluetoothAdapters,
|
||||
)
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import EVENT_LOGGING_CHANGED
|
||||
@@ -27,7 +28,6 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from .advertisement_tracker import (
|
||||
TRACKER_BUFFERING_WOBBLE_SECONDS,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"bleak-retry-connector==3.3.0",
|
||||
"bluetooth-adapters==0.16.1",
|
||||
"bluetooth-auto-recovery==1.2.3",
|
||||
"bluetooth-data-tools==1.15.0",
|
||||
"bluetooth-data-tools==1.16.0",
|
||||
"dbus-fast==2.14.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ from enum import Enum
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from bleak import BaseBleakClient
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import BluetoothManager
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
|
||||
from bluetooth_adapters import BluetoothAdapters
|
||||
from bluetooth_auto_recovery import recover_adapter
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from .models import BluetoothServiceInfoBleak
|
||||
from .storage import BluetoothStorage
|
||||
|
||||
@@ -11,7 +11,11 @@ async def async_get_calendars(
|
||||
hass: HomeAssistant, client: caldav.DAVClient, component: str
|
||||
) -> list[caldav.Calendar]:
|
||||
"""Get all calendars that support the specified component."""
|
||||
calendars = await hass.async_add_executor_job(client.principal().calendars)
|
||||
|
||||
def _get_calendars() -> list[caldav.Calendar]:
|
||||
return client.principal().calendars()
|
||||
|
||||
calendars = await hass.async_add_executor_job(_get_calendars)
|
||||
components_results = await asyncio.gather(
|
||||
*[
|
||||
hass.async_add_executor_job(calendar.get_supported_components)
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your deCONZ host."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings > Gateway > Advanced\n2. Press \"Authenticate app\" button"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "deCONZ Zigbee gateway via Home Assistant add-on",
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"web_port": "Web port (for visiting service)"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Deluge device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your DirectTV device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"use_legacy_protocol": "Use legacy protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your D-Link device",
|
||||
"password": "Default: PIN code on the back."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "Device Name",
|
||||
"name": "Device name",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your DoorBird device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Dremel 3D printer."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -799,6 +799,10 @@ class DSMREntity(SensorEntity):
|
||||
float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
|
||||
)
|
||||
|
||||
# Make sure we do not return a zero value for an energy sensor
|
||||
if not value and self.state_class == SensorStateClass.TOTAL_INCREASING:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "Ensure that your player is turned on.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Dune HD device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Duotecno device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Ecoforest device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Elgato device."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your SiteSage Emonitor device."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Enphase Envoy gateway."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Epson projector."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==19.2.1",
|
||||
"bluetooth-data-tools==1.15.0",
|
||||
"bluetooth-data-tools==1.16.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Evil Genius Labs device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -190,14 +190,14 @@ def _handle_exception(err) -> None:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Create a (EMEA/EU-based) Honeywell TCC system."""
|
||||
|
||||
async def load_auth_tokens(store) -> tuple[dict, dict | None]:
|
||||
async def load_auth_tokens(store) -> tuple[dict[str, str | dt], dict[str, str]]:
|
||||
app_storage = await store.async_load()
|
||||
tokens = dict(app_storage or {})
|
||||
|
||||
if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]:
|
||||
# any tokens won't be valid, and store might be corrupt
|
||||
await store.async_save({})
|
||||
return ({}, None)
|
||||
return ({}, {})
|
||||
|
||||
# evohomeasync2 requires naive/local datetimes as strings
|
||||
if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and (
|
||||
@@ -205,7 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
):
|
||||
tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive(expires)
|
||||
|
||||
user_data = tokens.pop(USER_DATA, None)
|
||||
user_data = tokens.pop(USER_DATA, {})
|
||||
return (tokens, user_data)
|
||||
|
||||
store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
|
||||
@@ -214,7 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
client_v2 = evohomeasync2.EvohomeClient(
|
||||
config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD],
|
||||
**tokens,
|
||||
**tokens, # type: ignore[arg-type]
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
@@ -253,7 +253,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
client_v1 = evohomeasync.EvohomeClient(
|
||||
client_v2.username,
|
||||
client_v2.password,
|
||||
user_data=user_data,
|
||||
session_id=user_data.get("sessionId") if user_data else None, # STORAGE_VER 1
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
@@ -425,7 +425,7 @@ class EvoBroker:
|
||||
self.tcs_utc_offset = timedelta(
|
||||
minutes=client.locations[loc_idx].timeZone[UTC_OFFSET]
|
||||
)
|
||||
self.temps: dict[str, Any] | None = {}
|
||||
self.temps: dict[str, float | None] = {}
|
||||
|
||||
async def save_auth_tokens(self) -> None:
|
||||
"""Save access tokens and session IDs to the store for later use."""
|
||||
@@ -441,14 +441,12 @@ class EvoBroker:
|
||||
ACCESS_TOKEN_EXPIRES: access_token_expires.isoformat(),
|
||||
}
|
||||
|
||||
if self.client_v1 and self.client_v1.user_data:
|
||||
user_id = self.client_v1.user_data["userInfo"]["userID"] # type: ignore[index]
|
||||
if self.client_v1:
|
||||
app_storage[USER_DATA] = { # type: ignore[assignment]
|
||||
"userInfo": {"userID": user_id},
|
||||
"sessionId": self.client_v1.user_data["sessionId"],
|
||||
}
|
||||
"sessionId": self.client_v1.broker.session_id,
|
||||
} # this is the schema for STORAGE_VER == 1
|
||||
else:
|
||||
app_storage[USER_DATA] = None
|
||||
app_storage[USER_DATA] = {} # type: ignore[assignment]
|
||||
|
||||
await self._store.async_save(app_storage)
|
||||
|
||||
@@ -468,16 +466,13 @@ class EvoBroker:
|
||||
async def _update_v1_api_temps(self, *args, **kwargs) -> None:
|
||||
"""Get the latest high-precision temperatures of the default Location."""
|
||||
|
||||
assert self.client_v1
|
||||
assert self.client_v1 # mypy check
|
||||
|
||||
def get_session_id(client_v1) -> str | None:
|
||||
user_data = client_v1.user_data if client_v1 else None
|
||||
return user_data.get("sessionId") if user_data else None
|
||||
|
||||
session_id = get_session_id(self.client_v1)
|
||||
session_id = self.client_v1.broker.session_id # maybe receive a new session_id?
|
||||
|
||||
self.temps = {} # these are now stale, will fall back to v2 temps
|
||||
try:
|
||||
temps = list(await self.client_v1.temperatures(force_refresh=True))
|
||||
temps = await self.client_v1.get_temperatures()
|
||||
|
||||
except evohomeasync.InvalidSchema as exc:
|
||||
_LOGGER.warning(
|
||||
@@ -489,7 +484,7 @@ class EvoBroker:
|
||||
),
|
||||
exc,
|
||||
)
|
||||
self.temps = self.client_v1 = None
|
||||
self.client_v1 = None
|
||||
|
||||
except evohomeasync.EvohomeError as exc:
|
||||
_LOGGER.warning(
|
||||
@@ -501,7 +496,6 @@ class EvoBroker:
|
||||
),
|
||||
exc,
|
||||
)
|
||||
self.temps = None # these are now stale, will fall back to v2 temps
|
||||
|
||||
else:
|
||||
if (
|
||||
@@ -513,19 +507,20 @@ class EvoBroker:
|
||||
"the v1 API's default location (there is more than one location), "
|
||||
"so the high-precision feature will be disabled until next restart"
|
||||
)
|
||||
self.temps = self.client_v1 = None
|
||||
self.client_v1 = None
|
||||
else:
|
||||
self.temps = {str(i["id"]): i["temp"] for i in temps}
|
||||
|
||||
finally:
|
||||
if session_id != get_session_id(self.client_v1):
|
||||
if self.client_v1 and session_id != self.client_v1.broker.session_id:
|
||||
await self.save_auth_tokens()
|
||||
|
||||
_LOGGER.debug("Temperatures = %s", self.temps)
|
||||
|
||||
async def _update_v2_api_state(self, *args, **kwargs) -> None:
|
||||
"""Get the latest modes, temperatures, setpoints of a Location."""
|
||||
access_token = self.client.access_token
|
||||
|
||||
access_token = self.client.access_token # maybe receive a new token?
|
||||
|
||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||
try:
|
||||
@@ -536,9 +531,9 @@ class EvoBroker:
|
||||
async_dispatcher_send(self.hass, DOMAIN)
|
||||
|
||||
_LOGGER.debug("Status = %s", status)
|
||||
|
||||
if access_token != self.client.access_token:
|
||||
await self.save_auth_tokens()
|
||||
finally:
|
||||
if access_token != self.client.access_token:
|
||||
await self.save_auth_tokens()
|
||||
|
||||
async def async_update(self, *args, **kwargs) -> None:
|
||||
"""Get the latest state data of an entire Honeywell TCC Location.
|
||||
@@ -562,6 +557,8 @@ class EvoDevice(Entity):
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
_evo_id: str
|
||||
|
||||
def __init__(self, evo_broker, evo_device) -> None:
|
||||
"""Initialize the evohome entity."""
|
||||
self._evo_device = evo_device
|
||||
@@ -623,18 +620,10 @@ class EvoChild(EvoDevice):
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature of a Zone."""
|
||||
if self._evo_device.TYPE == "domesticHotWater":
|
||||
dev_id = self._evo_device.dhwId
|
||||
else:
|
||||
dev_id = self._evo_device.zoneId
|
||||
|
||||
if self._evo_broker.temps and self._evo_broker.temps[dev_id] is not None:
|
||||
return self._evo_broker.temps[dev_id]
|
||||
|
||||
if self._evo_device.temperatureStatus["isAvailable"]:
|
||||
return self._evo_device.temperatureStatus["temperature"]
|
||||
|
||||
return None
|
||||
if self._evo_broker.temps.get(self._evo_id) is not None:
|
||||
return self._evo_broker.temps[self._evo_id]
|
||||
return self._evo_device.temperature
|
||||
|
||||
@property
|
||||
def setpoints(self) -> dict[str, Any]:
|
||||
@@ -679,7 +668,7 @@ class EvoChild(EvoDevice):
|
||||
switchpoint_time_of_day = dt_util.parse_datetime(
|
||||
f"{sp_date}T{switchpoint['TimeOfDay']}"
|
||||
)
|
||||
assert switchpoint_time_of_day
|
||||
assert switchpoint_time_of_day # mypy check
|
||||
dt_aware = _dt_evo_to_aware(
|
||||
switchpoint_time_of_day, self._evo_broker.tcs_utc_offset
|
||||
)
|
||||
|
||||
@@ -150,6 +150,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
self._attr_unique_id = f"{evo_device.zoneId}z"
|
||||
else:
|
||||
self._attr_unique_id = evo_device.zoneId
|
||||
self._evo_id = evo_device.zoneId
|
||||
|
||||
self._attr_name = evo_device.name
|
||||
|
||||
@@ -189,24 +190,27 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return the current operating mode of a Zone."""
|
||||
if self._evo_tcs.systemModeStatus["mode"] in (EVO_AWAY, EVO_HEATOFF):
|
||||
if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF):
|
||||
return HVACMode.AUTO
|
||||
is_off = self.target_temperature <= self.min_temp
|
||||
return HVACMode.OFF if is_off else HVACMode.HEAT
|
||||
if self.target_temperature is None:
|
||||
return None
|
||||
if self.target_temperature <= self.min_temp:
|
||||
return HVACMode.OFF
|
||||
return HVACMode.HEAT
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature of a Zone."""
|
||||
return self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||
return self._evo_device.target_heat_temperature
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
if self._evo_tcs.systemModeStatus["mode"] in (EVO_AWAY, EVO_HEATOFF):
|
||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
|
||||
return EVO_PRESET_TO_HA.get(self._evo_device.setpointStatus["setpointMode"])
|
||||
if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF):
|
||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
|
||||
return EVO_PRESET_TO_HA.get(self._evo_device.mode)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
@@ -214,7 +218,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
|
||||
The default is 5, but is user-configurable within 5-35 (in Celsius).
|
||||
"""
|
||||
return self._evo_device.setpointCapabilities["minHeatSetpoint"]
|
||||
return self._evo_device.min_heat_setpoint
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
@@ -222,17 +226,17 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
|
||||
The default is 35, but is user-configurable within 5-35 (in Celsius).
|
||||
"""
|
||||
return self._evo_device.setpointCapabilities["maxHeatSetpoint"]
|
||||
return self._evo_device.max_heat_setpoint
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature."""
|
||||
temperature = kwargs["temperature"]
|
||||
|
||||
if (until := kwargs.get("until")) is None:
|
||||
if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||
if self._evo_device.mode == EVO_FOLLOW:
|
||||
await self._update_schedule()
|
||||
until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", ""))
|
||||
elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER:
|
||||
elif self._evo_device.mode == EVO_TEMPOVER:
|
||||
until = dt_util.parse_datetime(self._evo_device.setpointStatus["until"])
|
||||
|
||||
until = dt_util.as_utc(until) if until else None
|
||||
@@ -272,7 +276,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
await self._evo_broker.call_client_api(self._evo_device.reset_mode())
|
||||
return
|
||||
|
||||
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||
temperature = self._evo_device.target_heat_temperature
|
||||
|
||||
if evo_preset_mode == EVO_TEMPOVER:
|
||||
await self._update_schedule()
|
||||
@@ -311,6 +315,7 @@ class EvoController(EvoClimateEntity):
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._attr_unique_id = evo_device.systemId
|
||||
self._evo_id = evo_device.systemId
|
||||
self._attr_name = evo_device.location.name
|
||||
|
||||
modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]]
|
||||
@@ -352,7 +357,7 @@ class EvoController(EvoClimateEntity):
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the current operating mode of a Controller."""
|
||||
tcs_mode = self._evo_tcs.systemModeStatus["mode"]
|
||||
tcs_mode = self._evo_tcs.system_mode
|
||||
return HVACMode.OFF if tcs_mode == EVO_HEATOFF else HVACMode.HEAT
|
||||
|
||||
@property
|
||||
@@ -362,16 +367,18 @@ class EvoController(EvoClimateEntity):
|
||||
Controllers do not have a current temp, but one is expected by HA.
|
||||
"""
|
||||
temps = [
|
||||
z.temperatureStatus["temperature"]
|
||||
z.temperature
|
||||
for z in self._evo_tcs.zones.values()
|
||||
if z.temperatureStatus["isAvailable"]
|
||||
if z.temperature is not None
|
||||
]
|
||||
return round(sum(temps) / len(temps), 1) if temps else None
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
|
||||
if not self._evo_tcs.system_mode:
|
||||
return None
|
||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Raise exception as Controllers don't have a target temperature."""
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"requirements": ["evohome-async==0.4.6"]
|
||||
"requirements": ["evohome-async==0.4.9"]
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
super().__init__(evo_broker, evo_device)
|
||||
|
||||
self._attr_unique_id = evo_device.dhwId
|
||||
self._evo_id = evo_device.dhwId
|
||||
|
||||
self._attr_precision = (
|
||||
PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE
|
||||
@@ -79,15 +80,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
@property
|
||||
def current_operation(self) -> str:
|
||||
"""Return the current operating mode (Auto, On, or Off)."""
|
||||
if self._evo_device.stateStatus["mode"] == EVO_FOLLOW:
|
||||
if self._evo_device.mode == EVO_FOLLOW:
|
||||
return STATE_AUTO
|
||||
return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]]
|
||||
return EVO_STATE_TO_HA[self._evo_device.state]
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return True if away mode is on."""
|
||||
is_off = EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] == STATE_OFF
|
||||
is_permanent = self._evo_device.stateStatus["mode"] == EVO_PERMOVER
|
||||
is_off = EVO_STATE_TO_HA[self._evo_device.state] == STATE_OFF
|
||||
is_permanent = self._evo_device.mode == EVO_PERMOVER
|
||||
return is_off and is_permanent
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Fast.com sensor."""
|
||||
async_add_entities([SpeedtestSensor(hass.data[DOMAIN])])
|
||||
async_add_entities([SpeedtestSensor(entry.entry_id, hass.data[DOMAIN])])
|
||||
|
||||
|
||||
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||
@@ -38,9 +38,10 @@ class SpeedtestSensor(RestoreEntity, SensorEntity):
|
||||
_attr_icon = "mdi:speedometer"
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, speedtest_data: dict[str, Any]) -> None:
|
||||
def __init__(self, entry_id: str, speedtest_data: dict[str, Any]) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._speedtest_data = speedtest_data
|
||||
self._attr_unique_id = entry_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FiveM server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""The Flexit Nordic (BACnet) integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio.exceptions
|
||||
|
||||
from flexit_bacnet import FlexitBACnet
|
||||
from flexit_bacnet.bacnet import DecodingError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_IP_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Flexit Nordic (BACnet) from a config entry."""
|
||||
|
||||
device = FlexitBACnet(entry.data[CONF_IP_ADDRESS], entry.data[CONF_DEVICE_ID])
|
||||
|
||||
try:
|
||||
await device.update()
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting to {entry.data['address']}"
|
||||
) from exc
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,148 @@
|
||||
"""The Flexit Nordic (BACnet) integration."""
|
||||
import asyncio.exceptions
|
||||
from typing import Any
|
||||
|
||||
from flexit_bacnet import (
|
||||
VENTILATION_MODE_AWAY,
|
||||
VENTILATION_MODE_HOME,
|
||||
VENTILATION_MODE_STOP,
|
||||
FlexitBACnet,
|
||||
)
|
||||
from flexit_bacnet.bacnet import DecodingError
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_HOME,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PRESET_TO_VENTILATION_MODE_MAP,
|
||||
VENTILATION_TO_PRESET_MODE_MAP,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Flexit Nordic unit."""
|
||||
device = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_devices([FlexitClimateEntity(device)])
|
||||
|
||||
|
||||
class FlexitClimateEntity(ClimateEntity):
|
||||
"""Flexit air handling unit."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
_attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.FAN_ONLY,
|
||||
]
|
||||
|
||||
_attr_preset_modes = [
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, device: FlexitBACnet) -> None:
|
||||
"""Initialize the unit."""
|
||||
self._device = device
|
||||
self._attr_unique_id = device.serial_number
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, device.serial_number),
|
||||
},
|
||||
name=device.device_name,
|
||||
manufacturer="Flexit",
|
||||
model="Nordic",
|
||||
serial_number=device.serial_number,
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh unit state."""
|
||||
await self._device.update()
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return self._device.room_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._device.ventilation_mode == VENTILATION_MODE_AWAY:
|
||||
return self._device.air_temp_setpoint_away
|
||||
|
||||
return self._device.air_temp_setpoint_home
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if self._device.ventilation_mode == VENTILATION_MODE_AWAY:
|
||||
await self._device.set_air_temp_setpoint_away(temperature)
|
||||
else:
|
||||
await self._device.set_air_temp_setpoint_home(temperature)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
"""Return the current preset mode, e.g., home, away, temp.
|
||||
|
||||
Requires ClimateEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
return VENTILATION_TO_PRESET_MODE_MAP[self._device.ventilation_mode]
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
|
||||
|
||||
try:
|
||||
await self._device.set_ventilation_mode(ventilation_mode)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
if self._device.ventilation_mode == VENTILATION_MODE_STOP:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.FAN_ONLY
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
try:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self._device.set_ventilation_mode(VENTILATION_MODE_STOP)
|
||||
else:
|
||||
await self._device.set_ventilation_mode(VENTILATION_MODE_HOME)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError from exc
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Config flow for Flexit Nordic (BACnet) integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio.exceptions
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from flexit_bacnet import FlexitBACnet
|
||||
from flexit_bacnet.bacnet import DecodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_IP_ADDRESS
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DEVICE_ID = 2
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_IP_ADDRESS): str,
|
||||
vol.Required(CONF_DEVICE_ID, default=DEFAULT_DEVICE_ID): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Flexit Nordic (BACnet)."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
device = FlexitBACnet(
|
||||
user_input[CONF_IP_ADDRESS], user_input[CONF_DEVICE_ID]
|
||||
)
|
||||
try:
|
||||
await device.update()
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(device.serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=device.device_name, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Constants for the Flexit Nordic (BACnet) integration."""
|
||||
from flexit_bacnet import (
|
||||
VENTILATION_MODE_AWAY,
|
||||
VENTILATION_MODE_HIGH,
|
||||
VENTILATION_MODE_HOME,
|
||||
VENTILATION_MODE_STOP,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
)
|
||||
|
||||
DOMAIN = "flexit_bacnet"
|
||||
|
||||
VENTILATION_TO_PRESET_MODE_MAP = {
|
||||
VENTILATION_MODE_STOP: PRESET_NONE,
|
||||
VENTILATION_MODE_AWAY: PRESET_AWAY,
|
||||
VENTILATION_MODE_HOME: PRESET_HOME,
|
||||
VENTILATION_MODE_HIGH: PRESET_BOOST,
|
||||
}
|
||||
|
||||
PRESET_TO_VENTILATION_MODE_MAP = {
|
||||
PRESET_NONE: VENTILATION_MODE_STOP,
|
||||
PRESET_AWAY: VENTILATION_MODE_AWAY,
|
||||
PRESET_HOME: VENTILATION_MODE_HOME,
|
||||
PRESET_BOOST: VENTILATION_MODE_HIGH,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "flexit_bacnet",
|
||||
"name": "Flexit Nordic (BACnet)",
|
||||
"codeowners": ["@lellky", "@piotrbulinski"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["flexit_bacnet==2.1.0"]
|
||||
}
|
||||
+2
-5
@@ -3,16 +3,13 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"device_id": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_input": "Failed to parse provided hostname",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Flo device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"rtsp_port": "RTSP port",
|
||||
"stream": "Stream"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Foscam camera."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Freebox router."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
},
|
||||
"phonebook": {
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20231129.1"]
|
||||
"requirements": ["home-assistant-frontend==20231130.0"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""API for persistent storage for the frontend."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
@@ -50,12 +50,19 @@ async def async_user_store(
|
||||
return store, data[user_id]
|
||||
|
||||
|
||||
def with_store(orig_func: Callable) -> Callable:
|
||||
def with_store(
|
||||
orig_func: Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], Store, dict[str, Any]],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
) -> Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]
|
||||
]:
|
||||
"""Decorate function to provide data."""
|
||||
|
||||
@wraps(orig_func)
|
||||
async def with_store_func(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Provide user specific data and store to function."""
|
||||
user_id = connection.user.id
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Frontier Silicon device."
|
||||
}
|
||||
},
|
||||
"device_config": {
|
||||
"title": "Device Configuration",
|
||||
"title": "Device configuration",
|
||||
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
|
||||
@@ -19,13 +19,14 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.use_ssl = entry.data.get(CONF_SSL, False)
|
||||
self.fully = FullyKiosk(
|
||||
async_get_clientsession(hass),
|
||||
entry.data[CONF_HOST],
|
||||
DEFAULT_PORT,
|
||||
entry.data[CONF_PASSWORD],
|
||||
use_ssl=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
use_ssl=self.use_ssl,
|
||||
verify_ssl=entry.data.get(CONF_VERIFY_SSL, False),
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -33,7 +34,6 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
name=entry.data[CONF_HOST],
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self.use_ssl = entry.data[CONF_SSL]
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the device running your Fully Kiosk Browser application."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"version": "Glances API Version (2 or 3)",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the system running your Glances system monitor."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Goal Zero Yeti."
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
|
||||
@@ -686,8 +686,12 @@ class GoogleEntity:
|
||||
return device
|
||||
|
||||
# Add Matter info
|
||||
if "matter" in self.hass.config.components and (
|
||||
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
||||
if (
|
||||
"matter" in self.hass.config.components
|
||||
and any(x for x in device_entry.identifiers if x[0] == "matter")
|
||||
and (
|
||||
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
||||
)
|
||||
):
|
||||
device["matterUniqueId"] = matter_info["unique_id"]
|
||||
device["matterOriginalVendorId"] = matter_info["vendor_id"]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from aioguardian import Client
|
||||
from aioguardian.errors import GuardianError
|
||||
@@ -170,7 +170,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@callback
|
||||
def call_with_data(func: Callable) -> Callable:
|
||||
def call_with_data(
|
||||
func: Callable[[ServiceCall, GuardianData], Coroutine[Any, Any, None]]
|
||||
) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]:
|
||||
"""Hydrate a service call with the appropriate GuardianData object."""
|
||||
|
||||
async def wrapper(call: ServiceCall) -> None:
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "Hub Name"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Logitech Harmony Hub."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
||||
@@ -27,7 +27,7 @@ async def async_setup_entry(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_switches",
|
||||
breaks_in_ha_version="2023.8.0",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches",
|
||||
@@ -91,7 +91,7 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity):
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_switches_{self.entity_id}_{item}",
|
||||
breaks_in_ha_version="2023.8.0",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches_entity",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Repairs implementation for supervisor integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
@@ -116,7 +117,12 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
@staticmethod
|
||||
def _async_step(suggestion: Suggestion) -> Callable:
|
||||
def _async_step(
|
||||
suggestion: Suggestion,
|
||||
) -> Callable[
|
||||
[SupervisorIssueRepairFlow, dict[str, str] | None],
|
||||
Coroutine[Any, Any, FlowResult],
|
||||
]:
|
||||
"""Generate a step handler for a suggestion."""
|
||||
|
||||
async def _async_step(
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your HEOS device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,12 +5,18 @@
|
||||
"title": "Pick Hue bridge",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hue bridge."
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"title": "Manual configure a Hue bridge",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hue bridge."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hyperion server."
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of Antifurto365 iAlarm system."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your IoTaWatt device."
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Keenetic router."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your KMtronic device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the system hosting your Kodi server."
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"""The Komfovent integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import komfovent_api
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Komfovent from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
_, credentials = komfovent_api.get_credentials(host, username, password)
|
||||
result, settings = await komfovent_api.get_settings(credentials)
|
||||
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to {host}: {result}")
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (credentials, settings)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,91 +0,0 @@
|
||||
"""Ventilation Units from Komfovent integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import komfovent_api
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
HASS_TO_KOMFOVENT_MODES = {
|
||||
HVACMode.COOL: komfovent_api.KomfoventModes.COOL,
|
||||
HVACMode.HEAT_COOL: komfovent_api.KomfoventModes.HEAT_COOL,
|
||||
HVACMode.OFF: komfovent_api.KomfoventModes.OFF,
|
||||
HVACMode.AUTO: komfovent_api.KomfoventModes.AUTO,
|
||||
}
|
||||
KOMFOVENT_TO_HASS_MODES = {v: k for k, v in HASS_TO_KOMFOVENT_MODES.items()}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Komfovent unit control."""
|
||||
credentials, settings = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([KomfoventDevice(credentials, settings)], True)
|
||||
|
||||
|
||||
class KomfoventDevice(ClimateEntity):
|
||||
"""Representation of a ventilation unit."""
|
||||
|
||||
_attr_hvac_modes = list(HASS_TO_KOMFOVENT_MODES.keys())
|
||||
_attr_preset_modes = [mode.name for mode in komfovent_api.KomfoventPresets]
|
||||
_attr_supported_features = ClimateEntityFeature.PRESET_MODE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials: komfovent_api.KomfoventCredentials,
|
||||
settings: komfovent_api.KomfoventSettings,
|
||||
) -> None:
|
||||
"""Initialize the ventilation unit."""
|
||||
self._komfovent_credentials = credentials
|
||||
self._komfovent_settings = settings
|
||||
|
||||
self._attr_unique_id = settings.serial_number
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, settings.serial_number)},
|
||||
model=settings.model,
|
||||
name=settings.name,
|
||||
serial_number=settings.serial_number,
|
||||
sw_version=settings.version,
|
||||
manufacturer="Komfovent",
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
await komfovent_api.set_preset(
|
||||
self._komfovent_credentials,
|
||||
komfovent_api.KomfoventPresets[preset_mode],
|
||||
)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
await komfovent_api.set_mode(
|
||||
self._komfovent_credentials, HASS_TO_KOMFOVENT_MODES[hvac_mode]
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
result, status = await komfovent_api.get_unit_status(
|
||||
self._komfovent_credentials
|
||||
)
|
||||
if result != komfovent_api.KomfoventConnectionResult.SUCCESS or not status:
|
||||
self._attr_available = False
|
||||
return
|
||||
self._attr_available = True
|
||||
self._attr_preset_mode = status.preset
|
||||
self._attr_current_temperature = status.temp_extract
|
||||
self._attr_hvac_mode = KOMFOVENT_TO_HASS_MODES[status.mode]
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Config flow for Komfovent integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import komfovent_api
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER = "user"
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_USERNAME, default="user"): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
ERRORS_MAP = {
|
||||
komfovent_api.KomfoventConnectionResult.NOT_FOUND: "cannot_connect",
|
||||
komfovent_api.KomfoventConnectionResult.UNAUTHORISED: "invalid_auth",
|
||||
komfovent_api.KomfoventConnectionResult.INVALID_INPUT: "invalid_input",
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Komfovent."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __return_error(
|
||||
self, result: komfovent_api.KomfoventConnectionResult
|
||||
) -> FlowResult:
|
||||
return self.async_show_form(
|
||||
step_id=STEP_USER,
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors={"base": ERRORS_MAP.get(result, "unknown")},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=STEP_USER, data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
conf_host = user_input[CONF_HOST]
|
||||
conf_username = user_input[CONF_USERNAME]
|
||||
conf_password = user_input[CONF_PASSWORD]
|
||||
|
||||
result, credentials = komfovent_api.get_credentials(
|
||||
conf_host, conf_username, conf_password
|
||||
)
|
||||
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
|
||||
return self.__return_error(result)
|
||||
|
||||
result, settings = await komfovent_api.get_settings(credentials)
|
||||
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
|
||||
return self.__return_error(result)
|
||||
|
||||
await self.async_set_unique_id(settings.serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=settings.name, data=user_input)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Constants for the Komfovent integration."""
|
||||
|
||||
DOMAIN = "komfovent"
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "komfovent",
|
||||
"name": "Komfovent",
|
||||
"codeowners": ["@ProstoSanja"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/komfovent",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["komfovent-api==0.0.3"]
|
||||
}
|
||||
@@ -209,7 +209,7 @@ class LaCrosseHumidity(LaCrosseSensor):
|
||||
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_icon = "mdi:water-percent"
|
||||
_attr_device_class = SensorDeviceClass.HUMIDITY
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.15.0", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.16.0", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.15.0", "led-ble==1.0.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.16.0", "led-ble==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your LG Soundbar."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -500,6 +500,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
)
|
||||
elif ColorMode.XY in supported_color_modes:
|
||||
params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
|
||||
elif ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
xy_color = color_util.color_hs_to_xy(*hs_color)
|
||||
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
|
||||
*xy_color
|
||||
)
|
||||
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
|
||||
params[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes:
|
||||
assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None
|
||||
if ColorMode.RGBW in supported_color_modes:
|
||||
@@ -515,6 +523,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
|
||||
elif ColorMode.XY in supported_color_modes:
|
||||
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
|
||||
elif ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
xy_color = color_util.color_RGB_to_xy(*rgb_color)
|
||||
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
|
||||
*xy_color
|
||||
)
|
||||
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
|
||||
params[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
elif ATTR_XY_COLOR in params and ColorMode.XY not in supported_color_modes:
|
||||
xy_color = params.pop(ATTR_XY_COLOR)
|
||||
if ColorMode.HS in supported_color_modes:
|
||||
@@ -529,6 +545,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
|
||||
*rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
|
||||
)
|
||||
elif ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
|
||||
*xy_color
|
||||
)
|
||||
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
|
||||
params[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes:
|
||||
rgbw_color = params.pop(ATTR_RGBW_COLOR)
|
||||
rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
|
||||
@@ -542,6 +565,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
|
||||
elif ColorMode.XY in supported_color_modes:
|
||||
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
|
||||
elif ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
xy_color = color_util.color_RGB_to_xy(*rgb_color)
|
||||
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
|
||||
*xy_color
|
||||
)
|
||||
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
|
||||
params[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
elif (
|
||||
ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes
|
||||
):
|
||||
@@ -558,6 +589,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
|
||||
elif ColorMode.XY in supported_color_modes:
|
||||
params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
|
||||
elif ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
xy_color = color_util.color_RGB_to_xy(*rgb_color)
|
||||
params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
|
||||
*xy_color
|
||||
)
|
||||
params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
|
||||
params[ATTR_COLOR_TEMP_KELVIN]
|
||||
)
|
||||
|
||||
# If white is set to True, set it to the light's brightness
|
||||
# Add a warning in Home Assistant Core 2023.5 if the brightness is set to an
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Handle websocket api for Matter."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
from typing import Any, Concatenate, ParamSpec
|
||||
|
||||
from matter_server.common.errors import MatterError
|
||||
import voluptuous as vol
|
||||
@@ -15,6 +15,8 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from .adapter import MatterAdapter
|
||||
from .helpers import get_matter
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
ID = "id"
|
||||
TYPE = "type"
|
||||
|
||||
@@ -28,12 +30,19 @@ def async_register_api(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, websocket_set_wifi_credentials)
|
||||
|
||||
|
||||
def async_get_matter_adapter(func: Callable) -> Callable:
|
||||
def async_get_matter_adapter(
|
||||
func: Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any], MatterAdapter],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
) -> Callable[
|
||||
[HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None]
|
||||
]:
|
||||
"""Decorate function to get the MatterAdapter."""
|
||||
|
||||
@wraps(func)
|
||||
async def _get_matter(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Provide the Matter client to the function."""
|
||||
matter = get_matter(hass)
|
||||
@@ -43,7 +52,15 @@ def async_get_matter_adapter(func: Callable) -> Callable:
|
||||
return _get_matter
|
||||
|
||||
|
||||
def async_handle_failed_command(func: Callable) -> Callable:
|
||||
def async_handle_failed_command(
|
||||
func: Callable[
|
||||
Concatenate[HomeAssistant, ActiveConnection, dict[str, Any], _P],
|
||||
Coroutine[Any, Any, None],
|
||||
],
|
||||
) -> Callable[
|
||||
Concatenate[HomeAssistant, ActiveConnection, dict[str, Any], _P],
|
||||
Coroutine[Any, Any, None],
|
||||
]:
|
||||
"""Decorate function to handle MatterError and send relevant error."""
|
||||
|
||||
@wraps(func)
|
||||
@@ -51,8 +68,8 @@ def async_handle_failed_command(func: Callable) -> Callable:
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> None:
|
||||
"""Handle MatterError within function and send relevant error."""
|
||||
try:
|
||||
|
||||
@@ -94,7 +94,7 @@ def get_node_from_device_entry(
|
||||
)
|
||||
|
||||
if device_id_full is None:
|
||||
raise ValueError(f"Device {device.id} is not a Matter device")
|
||||
return None
|
||||
|
||||
device_id = device_id_full.lstrip(device_id_type_prefix)
|
||||
matter_client = matter.matter_client
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==4.0.2"]
|
||||
"requirements": ["python-matter-server==5.0.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mill", "mill_local"],
|
||||
"requirements": ["millheater==0.11.6", "mill-local==0.3.0"]
|
||||
"requirements": ["millheater==0.11.7", "mill-local==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"count": "Ping count"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the device you want to ping."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -122,9 +122,9 @@ class PowerwallDataManager:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Tesla Powerwall from a config entry."""
|
||||
http_session = requests.Session()
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
ip_address: str = entry.data[CONF_IP_ADDRESS]
|
||||
|
||||
password = entry.data.get(CONF_PASSWORD)
|
||||
password: str | None = entry.data.get(CONF_PASSWORD)
|
||||
power_wall = Powerwall(ip_address, http_session=http_session)
|
||||
try:
|
||||
base_info = await hass.async_add_executor_job(
|
||||
@@ -184,7 +184,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
def _login_and_fetch_base_info(
|
||||
power_wall: Powerwall, host: str, password: str
|
||||
power_wall: Powerwall, host: str, password: str | None
|
||||
) -> PowerwallBaseInfo:
|
||||
"""Login to the powerwall and fetch the base info."""
|
||||
if password is not None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/private_ble_device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.15.0"]
|
||||
"requirements": ["bluetooth-data-tools==1.16.0"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your ProgettiHWSW board."
|
||||
}
|
||||
},
|
||||
"relay_modes": {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your QNAP device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial, wraps
|
||||
@@ -326,10 +326,17 @@ async def async_setup_entry( # noqa: C901
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
def call_with_controller(update_programs_and_zones: bool = True) -> Callable:
|
||||
def call_with_controller(
|
||||
update_programs_and_zones: bool = True,
|
||||
) -> Callable[
|
||||
[Callable[[ServiceCall, Controller], Coroutine[Any, Any, None]]],
|
||||
Callable[[ServiceCall], Coroutine[Any, Any, None]],
|
||||
]:
|
||||
"""Hydrate a service call with the appropriate controller."""
|
||||
|
||||
def decorator(func: Callable) -> Callable[..., Awaitable]:
|
||||
def decorator(
|
||||
func: Callable[[ServiceCall, Controller], Coroutine[Any, Any, None]]
|
||||
) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]:
|
||||
"""Define the decorator."""
|
||||
|
||||
@wraps(func)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Helper to test significant Remote state changes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_CURRENT_ACTIVITY
|
||||
|
||||
|
||||
@callback
|
||||
def async_check_significant_change(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict,
|
||||
new_state: str,
|
||||
new_attrs: dict,
|
||||
**kwargs: Any,
|
||||
) -> bool | None:
|
||||
"""Test if state significantly changed."""
|
||||
if old_state != new_state:
|
||||
return True
|
||||
|
||||
if old_attrs.get(ATTR_CURRENT_ACTIVITY) != new_attrs.get(ATTR_CURRENT_ACTIVITY):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -183,7 +183,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
def cleanup_disconnected_cams(
|
||||
hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
|
||||
) -> None:
|
||||
"""Clean-up disconnected camera channels or channels where a different model camera is connected."""
|
||||
"""Clean-up disconnected camera channels."""
|
||||
if not host.api.is_nvr:
|
||||
return
|
||||
|
||||
@@ -206,14 +206,16 @@ def cleanup_disconnected_cams(
|
||||
if ch not in host.api.channels:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, since no camera is connected to NVR channel %s anymore",
|
||||
"Removing Reolink device %s, "
|
||||
"since no camera is connected to NVR channel %s anymore",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
if ch_model not in [device.model, "Unknown"]:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, since the camera model connected to channel %s changed from %s to %s",
|
||||
"Removing Reolink device %s, "
|
||||
"since the camera model connected to channel %s changed from %s to %s",
|
||||
device.name,
|
||||
ch,
|
||||
device.model,
|
||||
@@ -222,12 +224,5 @@ def cleanup_disconnected_cams(
|
||||
if not remove:
|
||||
continue
|
||||
|
||||
# clean entity and device registry
|
||||
entity_reg = er.async_get(hass)
|
||||
entities = er.async_entries_for_device(
|
||||
entity_reg, device.id, include_disabled_entities=True
|
||||
)
|
||||
for entity in entities:
|
||||
entity_reg.async_remove(entity.entity_id)
|
||||
|
||||
# clean device registry and associated entities
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
@@ -25,16 +25,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
class ReolinkBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes binary sensor entities."""
|
||||
|
||||
icon_off: str = "mdi:motion-sensor-off"
|
||||
icon: str = "mdi:motion-sensor"
|
||||
supported: Callable[[Host, int], bool] = lambda host, ch: True
|
||||
value: Callable[[Host, int], bool]
|
||||
|
||||
|
||||
@@ -128,8 +130,8 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
|
||||
entity_description: ReolinkBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink binary sensor."""
|
||||
super().__init__(reolink_data, channel)
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, channel)
|
||||
|
||||
if self._host.api.model in DUAL_LENS_DUAL_MOTION_MODELS:
|
||||
if entity_description.translation_key is not None:
|
||||
@@ -138,10 +140,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
|
||||
key = entity_description.key
|
||||
self._attr_translation_key = f"{key}_lens_{self._channel}"
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{self._channel}_{entity_description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Icon of the sensor."""
|
||||
|
||||
@@ -27,7 +27,12 @@ from homeassistant.helpers.entity_platform import (
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkHostCoordinatorEntity
|
||||
from .entity import (
|
||||
ReolinkChannelCoordinatorEntity,
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
|
||||
ATTR_SPEED = "speed"
|
||||
SUPPORT_PTZ_SPEED = CameraEntityFeature.STREAM
|
||||
@@ -36,21 +41,23 @@ SUPPORT_PTZ_SPEED = CameraEntityFeature.STREAM
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkButtonEntityDescription(
|
||||
ButtonEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes button entities for a camera channel."""
|
||||
|
||||
enabled_default: Callable[[Host, int], bool] | None = None
|
||||
method: Callable[[Host, int], Any]
|
||||
supported: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
ptz_cmd: str | None = None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkHostButtonEntityDescription(ButtonEntityDescription):
|
||||
class ReolinkHostButtonEntityDescription(
|
||||
ButtonEntityDescription,
|
||||
ReolinkHostEntityDescription,
|
||||
):
|
||||
"""A class that describes button entities for the host."""
|
||||
|
||||
method: Callable[[Host], Any]
|
||||
supported: Callable[[Host], bool] = lambda api: True
|
||||
|
||||
|
||||
BUTTON_ENTITIES = (
|
||||
@@ -195,12 +202,9 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity):
|
||||
entity_description: ReolinkButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink button entity."""
|
||||
super().__init__(reolink_data, channel)
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, channel)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{entity_description.key}"
|
||||
)
|
||||
if entity_description.enabled_default is not None:
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
entity_description.enabled_default(self._host.api, self._channel)
|
||||
@@ -241,10 +245,8 @@ class ReolinkHostButtonEntity(ReolinkHostCoordinatorEntity, ButtonEntity):
|
||||
entity_description: ReolinkHostButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink button entity."""
|
||||
super().__init__(reolink_data)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = f"{self._host.unique_id}_{entity_description.key}"
|
||||
super().__init__(reolink_data)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute the button action."""
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Component providing support for Reolink IP cameras."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from reolink_aio.api import DUAL_LENS_MODELS, Host
|
||||
from reolink_aio.api import DUAL_LENS_MODELS
|
||||
from reolink_aio.exceptions import ReolinkError
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
@@ -20,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,11 +27,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkCameraEntityDescription(
|
||||
CameraEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes camera entities for a camera channel."""
|
||||
|
||||
stream: str
|
||||
supported: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
|
||||
|
||||
CAMERA_ENTITIES = (
|
||||
@@ -135,10 +134,6 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
|
||||
f"{entity_description.translation_key}_lens_{self._channel}"
|
||||
)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{entity_description.key}"
|
||||
)
|
||||
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the source of the stream."""
|
||||
return await self._host.api.get_stream_source(
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Reolink parent entity class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeVar
|
||||
|
||||
from reolink_aio.api import DUAL_LENS_MODELS
|
||||
from reolink_aio.api import DUAL_LENS_MODELS, Host
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
@@ -17,8 +20,22 @@ from .const import DOMAIN
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkChannelEntityDescription(EntityDescription):
|
||||
"""A class that describes entities for a camera channel."""
|
||||
|
||||
supported: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkHostEntityDescription(EntityDescription):
|
||||
"""A class that describes host entities."""
|
||||
|
||||
supported: Callable[[Host], bool] = lambda api: True
|
||||
|
||||
|
||||
class ReolinkBaseCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[_T]]):
|
||||
"""Parent class fo Reolink entities."""
|
||||
"""Parent class for Reolink entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@@ -59,14 +76,20 @@ class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]):
|
||||
basically a NVR with a single channel that has the camera connected to that channel.
|
||||
"""
|
||||
|
||||
entity_description: ReolinkHostEntityDescription | ReolinkChannelEntityDescription
|
||||
|
||||
def __init__(self, reolink_data: ReolinkData) -> None:
|
||||
"""Initialize ReolinkHostCoordinatorEntity."""
|
||||
super().__init__(reolink_data, reolink_data.device_coordinator)
|
||||
|
||||
self._attr_unique_id = f"{self._host.unique_id}_{self.entity_description.key}"
|
||||
|
||||
|
||||
class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
"""Parent class for Reolink hardware camera entities connected to a channel of the NVR."""
|
||||
|
||||
entity_description: ReolinkChannelEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
@@ -76,6 +99,9 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
super().__init__(reolink_data)
|
||||
|
||||
self._channel = channel
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{self.entity_description.key}"
|
||||
)
|
||||
|
||||
dev_ch = channel
|
||||
if self._host.api.model in DUAL_LENS_MODELS:
|
||||
|
||||
@@ -22,17 +22,19 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkLightEntityDescription(LightEntityDescription):
|
||||
class ReolinkLightEntityDescription(
|
||||
LightEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes light entities."""
|
||||
|
||||
get_brightness_fn: Callable[[Host, int], int | None] | None = None
|
||||
is_on_fn: Callable[[Host, int], bool]
|
||||
set_brightness_fn: Callable[[Host, int, int], Any] | None = None
|
||||
supported_fn: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
turn_on_off_fn: Callable[[Host, int, bool], Any]
|
||||
|
||||
|
||||
@@ -41,7 +43,7 @@ LIGHT_ENTITIES = (
|
||||
key="floodlight",
|
||||
translation_key="floodlight",
|
||||
icon="mdi:spotlight-beam",
|
||||
supported_fn=lambda api, ch: api.supported(ch, "floodLight"),
|
||||
supported=lambda api, ch: api.supported(ch, "floodLight"),
|
||||
is_on_fn=lambda api, ch: api.whiteled_state(ch),
|
||||
turn_on_off_fn=lambda api, ch, value: api.set_whiteled(ch, state=value),
|
||||
get_brightness_fn=lambda api, ch: api.whiteled_brightness(ch),
|
||||
@@ -52,7 +54,7 @@ LIGHT_ENTITIES = (
|
||||
translation_key="ir_lights",
|
||||
icon="mdi:led-off",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported_fn=lambda api, ch: api.supported(ch, "ir_lights"),
|
||||
supported=lambda api, ch: api.supported(ch, "ir_lights"),
|
||||
is_on_fn=lambda api, ch: api.ir_enabled(ch),
|
||||
turn_on_off_fn=lambda api, ch, value: api.set_ir_lights(ch, value),
|
||||
),
|
||||
@@ -61,7 +63,7 @@ LIGHT_ENTITIES = (
|
||||
translation_key="status_led",
|
||||
icon="mdi:lightning-bolt-circle",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported_fn=lambda api, ch: api.supported(ch, "power_led"),
|
||||
supported=lambda api, ch: api.supported(ch, "power_led"),
|
||||
is_on_fn=lambda api, ch: api.status_led_enabled(ch),
|
||||
turn_on_off_fn=lambda api, ch, value: api.set_status_led(ch, value),
|
||||
),
|
||||
@@ -80,7 +82,7 @@ async def async_setup_entry(
|
||||
ReolinkLightEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in LIGHT_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
if entity_description.supported_fn(reolink_data.host.api, channel)
|
||||
if entity_description.supported(reolink_data.host.api, channel)
|
||||
)
|
||||
|
||||
|
||||
@@ -96,12 +98,8 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
entity_description: ReolinkLightEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink light entity."""
|
||||
super().__init__(reolink_data, channel)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{entity_description.key}"
|
||||
)
|
||||
super().__init__(reolink_data, channel)
|
||||
|
||||
if entity_description.set_brightness_fn is None:
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
@@ -21,18 +21,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkNumberEntityDescription(NumberEntityDescription):
|
||||
class ReolinkNumberEntityDescription(
|
||||
NumberEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes number entities."""
|
||||
|
||||
get_max_value: Callable[[Host, int], float] | None = None
|
||||
get_min_value: Callable[[Host, int], float] | None = None
|
||||
method: Callable[[Host, int, float], Any]
|
||||
mode: NumberMode = NumberMode.AUTO
|
||||
supported: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
value: Callable[[Host, int], float | None]
|
||||
|
||||
|
||||
@@ -378,8 +380,8 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
||||
entity_description: ReolinkNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink number entity."""
|
||||
super().__init__(reolink_data, channel)
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, channel)
|
||||
|
||||
if entity_description.get_min_value is not None:
|
||||
self._attr_native_min_value = entity_description.get_min_value(
|
||||
@@ -390,9 +392,6 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
||||
self._host.api, channel
|
||||
)
|
||||
self._attr_mode = entity_description.mode
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{entity_description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
|
||||
@@ -24,18 +24,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ReolinkData
|
||||
from .const import DOMAIN
|
||||
from .entity import ReolinkChannelCoordinatorEntity
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReolinkSelectEntityDescription(SelectEntityDescription):
|
||||
class ReolinkSelectEntityDescription(
|
||||
SelectEntityDescription,
|
||||
ReolinkChannelEntityDescription,
|
||||
):
|
||||
"""A class that describes select entities."""
|
||||
|
||||
get_options: list[str] | Callable[[Host, int], list[str]]
|
||||
method: Callable[[Host, int, str], Any]
|
||||
supported: Callable[[Host, int], bool] = lambda api, ch: True
|
||||
value: Callable[[Host, int], str] | None = None
|
||||
|
||||
|
||||
@@ -131,14 +133,10 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
|
||||
entity_description: ReolinkSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink select entity."""
|
||||
super().__init__(reolink_data, channel)
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, channel)
|
||||
self._log_error = True
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_{channel}_{entity_description.key}"
|
||||
)
|
||||
|
||||
if callable(entity_description.get_options):
|
||||
self._attr_options = entity_description.get_options(self._host.api, channel)
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user