Merge branch 'dev' into programTemp

This commit is contained in:
Christopher Fenner
2023-12-02 12:00:37 +01:00
committed by GitHub
186 changed files with 2605 additions and 1895 deletions
+3 -2
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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",
+2 -2
View File
@@ -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
+52 -27
View File
@@ -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"
]
}
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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)
+4 -1
View File
@@ -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."
}
}
},
+4
View File
@@ -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."
}
}
},
+28 -39
View File
@@ -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
)
+25 -18
View File
@@ -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"]
}
@@ -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"]
}
+10 -3
View File
@@ -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": {
+2 -2
View File
@@ -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",
+8 -2
View File
@@ -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"]
}
+1 -1
View File
@@ -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
+24 -7
View File
@@ -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:
+1 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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
+7 -12
View File
@@ -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."""
+13 -11
View File
@@ -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."""
+3 -8
View File
@@ -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(
+28 -2
View File
@@ -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:
+10 -12
View File
@@ -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}
+6 -7
View File
@@ -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:
+6 -8
View File
@@ -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