Compare commits

..

21 Commits

Author SHA1 Message Date
Paulus Schoutsen
b6e50135f5 Bumped version to 2023.3.0b2 2023-02-24 21:41:02 -05:00
Bram Kragten
64197aa5f5 Update frontend to 20230224.0 (#88721) 2023-02-24 21:40:56 -05:00
J. Nick Koston
5a2d7a5dd4 Reduce overhead to save json data to postgresql (#88717)
* Reduce overhead to strip nulls from json

* Reduce overhead to strip nulls from json

* small cleanup
2023-02-24 21:40:55 -05:00
J. Nick Koston
2d6f84b2a8 Fix timeout in purpleapi test (#88715)
https://github.com/home-assistant/core/actions/runs/4264644494/jobs/7423099757
2023-02-24 21:40:54 -05:00
J. Nick Koston
0c6a469218 Fix migration failing when existing data has duplicates (#88712) 2023-02-24 21:40:53 -05:00
J. Nick Koston
e69271cb46 Bump aioesphomeapi to 13.4.1 (#88703)
changelog: https://github.com/esphome/aioesphomeapi/releases/tag/v13.4.1
2023-02-24 21:40:52 -05:00
Michael Hansen
02bd3f897d Make a copy of matching states so translated state names can be used (#88683) 2023-02-24 21:40:51 -05:00
J. Nick Koston
64ad5326dd Bump mopeka_iot_ble to 0.4.1 (#88680)
* Bump mopeka_iot_ble to 0.4.1

closes #88232

* adjust tests
2023-02-24 21:40:50 -05:00
puddly
74696a3fac Name the Yellow-internal radio and multi-PAN addon as ZHA serial ports (#88208)
* Expose the Yellow-internal radio and multi-PAN addon as named serial ports

* Remove the serial number if it isn't available

* Use consistent names for the addon and Zigbee radio

* Add `homeassistant_hardware` and `_yellow` as `after_dependencies`

* Handle `hassio` not existing when listing serial ports

* Add unit tests
2023-02-24 21:40:49 -05:00
Paulus Schoutsen
70e1d14da0 Bumped version to 2023.3.0b1 2023-02-23 15:00:13 -05:00
Bram Kragten
25f066d476 Update frontend to 20230223.0 (#88677) 2023-02-23 15:00:07 -05:00
Marcel van der Veldt
5adf1dcc90 Fix support for Bridge(d) and composed devices in Matter (#88662)
* Refactor discovery of entities to support composed and bridged devices

* Bump library version to 3.1.0

* move discovery schemas to platforms

* optimize a tiny bit

* simplify even more

* fixed bug in light platform

* fix color control logic

* fix some issues

* Update homeassistant/components/matter/discovery.py

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* fix some tests

* fix light test

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-02-23 15:00:05 -05:00
epenet
0fb28dcf9e Add missing async_setup_entry mock in openuv (#88661) 2023-02-23 15:00:04 -05:00
Allen Porter
2fddbcedcf Fix local calendar issue with events created with fixed UTC offsets (#88650)
Fix issue with events created with UTC offsets
2023-02-23 15:00:03 -05:00
J. Nick Koston
951df3df57 Fix untrapped exceptions during Yale Access Bluetooth first setup (#88642) 2023-02-23 15:00:02 -05:00
starkillerOG
35142e456a Bump reolink-aio to 0.5.1 and check if update supported (#88641) 2023-02-23 15:00:01 -05:00
Paulus Schoutsen
cfaba87dd6 Error checking for OTBR (#88620)
* Error checking for OTBR

* Other errors in flow too

* Tests
2023-02-23 15:00:00 -05:00
Erik Montnemery
2db8d4b73a Bump python-otbr-api to 1.0.4 (#88613)
* Bump python-otbr-api to 1.0.4

* Adjust tests
2023-02-23 14:59:59 -05:00
Raman Gupta
0d2006bf33 Add support for firmware target in zwave_js FirmwareUploadView (#88523)
* Add support for firmware target in zwave_js FirmwareUploadView

fix

* Update tests/components/zwave_js/test_api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/zwave_js/test_api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/zwave_js/test_api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/zwave_js/test_api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* fix types

* Switch back to using Any

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-02-23 14:59:58 -05:00
puddly
45547d226e Disable the ZHA bellows UART thread when connecting to a TCP coordinator (#88202)
Disable the bellows UART thread when connecting to a TCP coordinator
2023-02-23 14:59:56 -05:00
Franck Nijhof
cebc6dd096 Bumped version to 2023.3.0b0 2023-02-22 20:44:37 +01:00
198 changed files with 1354 additions and 4648 deletions

View File

@@ -639,10 +639,6 @@ omit =
homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
homeassistant/components/livisi/__init__.py
homeassistant/components/livisi/climate.py
homeassistant/components/livisi/coordinator.py
homeassistant/components/livisi/switch.py
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/logi_circle/__init__.py
homeassistant/components/logi_circle/camera.py
@@ -807,8 +803,7 @@ omit =
homeassistant/components/nuki/sensor.py
homeassistant/components/nx584/alarm_control_panel.py
homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/obihai/connectivity.py
homeassistant/components/obihai/sensor.py
homeassistant/components/obihai/*
homeassistant/components/octoprint/__init__.py
homeassistant/components/oem/climate.py
homeassistant/components/ohmconnect/sensor.py

View File

@@ -31,7 +31,7 @@ env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.4
HA_SHORT_VERSION: 2023.3
DEFAULT_PYTHON: "3.10"
ALL_PYTHON_VERSIONS: "['3.10', '3.11']"
# 10.3 is the oldest supported version
@@ -1073,10 +1073,10 @@ jobs:
ffmpeg \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@v3.3.0
uses: actions/checkout@v3.1.0
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.5.0
uses: actions/setup-python@v4.3.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true

View File

@@ -186,7 +186,6 @@ homeassistant.components.ld2410_ble.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*
homeassistant.components.local_ip.*
homeassistant.components.lock.*

View File

@@ -825,8 +825,7 @@ build.json @home-assistant/supervisor
/tests/components/nws/ @MatthewFlamm @kamiyo
/homeassistant/components/nzbget/ @chriscla
/tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney
/tests/components/obihai/ @dshokouhi @ejpenney
/homeassistant/components/obihai/ @dshokouhi
/homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480
@@ -1139,8 +1138,8 @@ build.json @home-assistant/supervisor
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich
/tests/components/statistics/ @ThomDietrich
/homeassistant/components/statistics/ @fabaff @ThomDietrich
/tests/components/statistics/ @fabaff @ThomDietrich
/homeassistant/components/steam_online/ @tkdrob
/tests/components/steam_online/ @tkdrob
/homeassistant/components/steamist/ @bdraco

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from AIOAladdinConnect import AladdinConnectClient
@@ -19,6 +20,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CLIENT_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
@@ -131,6 +134,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(
self, import_data: dict[str, Any] | None = None
) -> FlowResult:
"""Import Aladin Connect config from configuration.yaml."""
return await self.async_step_user(import_data)
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -2,24 +2,63 @@
from __future__ import annotations
from datetime import timedelta
from typing import Any
import logging
from typing import Any, Final
from AIOAladdinConnect import AladdinConnectClient
import voluptuous as vol
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.components.cover import (
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPENING,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES
from .model import DoorDevice
_LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
SCAN_INTERVAL = timedelta(seconds=300)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up Aladdin Connect devices yaml depreciated."""
_LOGGER.warning(
"Configuring Aladdin Connect through yaml is deprecated. Please remove it from"
" your configuration as it has already been imported to a config entry"
)
await hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,

View File

@@ -5,7 +5,6 @@ import asyncio
from http import HTTPStatus
import json
import logging
from typing import cast
import aiohttp
import async_timeout
@@ -16,7 +15,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.significant_change import create_checker
import homeassistant.util.dt as dt_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
@@ -164,10 +162,9 @@ async def async_send_changereport_message(
if response.status == HTTPStatus.ACCEPTED:
return
response_json = json_loads_object(response_text)
response_payload = cast(JsonObjectType, response_json["payload"])
response_json = json.loads(response_text)
if response_payload["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
if invalidate_access_token:
# Invalidate the access token and try again
config.async_invalidate_access_token()
@@ -183,8 +180,8 @@ async def async_send_changereport_message(
_LOGGER.error(
"Error when sending ChangeReport for %s to Alexa: %s: %s",
alexa_entity.entity_id,
response_payload["code"],
response_payload["description"],
response_json["payload"]["code"],
response_json["payload"]["description"],
)
@@ -302,12 +299,11 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
if response.status == HTTPStatus.ACCEPTED:
return
response_json = json_loads_object(response_text)
response_payload = cast(JsonObjectType, response_json["payload"])
response_json = json.loads(response_text)
_LOGGER.error(
"Error when sending DoorbellPress event for %s to Alexa: %s: %s",
alexa_entity.entity_id,
response_payload["code"],
response_payload["description"],
response_json["payload"]["code"],
response_json["payload"]["description"],
)

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/apprise",
"iot_class": "cloud_push",
"loggers": ["apprise"],
"requirements": ["apprise==1.3.0"]
"requirements": ["apprise==1.2.1"]
}

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.2.7", "yalexs_ble==2.0.4"]
"requirements": ["yalexs==1.2.7", "yalexs_ble==2.0.3"]
}

View File

@@ -106,8 +106,6 @@ class ActiveBluetoothDataUpdateCoordinator(
def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool:
"""Return true if time to try and poll."""
if self.hass.is_stopping:
return False
poll_age: float | None = None
if self._last_poll:
poll_age = monotonic_time_coarse() - self._last_poll

View File

@@ -99,8 +99,6 @@ class ActiveBluetoothProcessorCoordinator(
def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool:
"""Return true if time to try and poll."""
if self.hass.is_stopping:
return False
poll_age: float | None = None
if self._last_poll:
poll_age = monotonic_time_coarse() - self._last_poll

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==2.7.0"]
"requirements": ["bthome-ble==2.5.2"]
}

View File

@@ -119,16 +119,6 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# Gas (m3)
(
BTHomeSensorDeviceClass.GAS,
Units.VOLUME_CUBIC_METERS,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.GAS}_{Units.VOLUME_CUBIC_METERS}",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# Humidity in (percent)
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.2.0"]
"requirements": ["caldav==1.1.1"]
}

View File

@@ -3,7 +3,6 @@
DOMAIN = "conversation"
DEFAULT_EXPOSED_DOMAINS = {
"binary_sensor",
"climate",
"cover",
"fan",
@@ -17,5 +16,3 @@ DEFAULT_EXPOSED_DOMAINS = {
"vacuum",
"water_heater",
}
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}

View File

@@ -28,7 +28,7 @@ from homeassistant.helpers import (
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN
from .const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
@@ -479,12 +479,6 @@ class DefaultAgent(AbstractConversationAgent):
for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr_key, attr_value in state.attributes.items():
if attr_key not in DEFAULT_EXPOSED_ATTRIBUTES:
continue
context[attr_key] = attr_value
entity = entities.async_get(state.entity_id)
if entity is not None:
@@ -524,9 +518,6 @@ class DefaultAgent(AbstractConversationAgent):
for alias in area.aliases:
area_names.append((alias, area.id))
_LOGGER.debug("Exposed areas: %s", area_names)
_LOGGER.debug("Exposed entities: %s", entity_names)
self._slot_lists = {
"area": TextSlotList.from_tuples(area_names, allow_template=False),
"name": TextSlotList.from_tuples(entity_names, allow_template=False),

View File

@@ -8,7 +8,6 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import ConditionProtocol, trace_condition_function
from homeassistant.helpers.typing import ConfigType
from . import DeviceAutomationType, async_get_device_automation_platform
@@ -18,13 +17,24 @@ if TYPE_CHECKING:
from homeassistant.helpers import condition
class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
class DeviceAutomationConditionProtocol(Protocol):
"""Define the format of device_condition modules.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config
from ConditionProtocol.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config.
"""
CONDITION_SCHEMA: vol.Schema
async def async_validate_condition_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
def async_condition_from_config(
self, hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
async def async_get_condition_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
@@ -52,4 +62,4 @@ async def async_condition_from_config(
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
)
return trace_condition_function(platform.async_condition_from_config(hass, config))
return platform.async_condition_from_config(hass, config)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env_canada==0.5.29"]
"requirements": ["env_canada==0.5.28"]
}

View File

@@ -87,23 +87,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, STARTUP_SCAN_TIMEOUT
)
@callback
def _async_start_background_discovery(*_: Any) -> None:
"""Run discovery in the background."""
hass.async_create_background_task(_async_discovery(), "flux_led-discovery")
async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT)
)
async_trigger_discovery(hass, domain_data[FLUX_LED_DISCOVERY])
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, _async_start_background_discovery
)
async_track_time_interval(
hass, _async_start_background_discovery, DISCOVERY_INTERVAL
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery)
async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL)
return True

View File

@@ -341,11 +341,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
is_dev = repo_path is not None
root_path = _frontend_root(repo_path)
if is_dev:
from .dev import async_setup_frontend_dev
async_setup_frontend_dev(hass)
for path, should_cache in (
("service_worker.js", False),
("robots.txt", False),

View File

@@ -1,60 +0,0 @@
"""Development helpers for the frontend."""
import aiohttp
from aiohttp import hdrs, web
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
@callback
def async_setup_frontend_dev(hass: HomeAssistant) -> None:
"""Set up frontend dev views."""
hass.http.register_view( # type: ignore
FrontendDevView(
"http://localhost:8000", aiohttp_client.async_get_clientsession(hass)
)
)
FILTER_RESPONSE_HEADERS = {hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING}
class FrontendDevView(HomeAssistantView):
"""Frontend dev view."""
name = "_dev:frontend"
url = "/_dev_frontend/{path:.*}"
requires_auth = False
extra_urls = ["/__web-dev-server__/{path:.*}"]
def __init__(self, forward_base: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io ingress view."""
self._forward_base = forward_base
self._websession = websession
async def get(self, request: web.Request, path: str) -> web.Response:
"""Frontend routing."""
# To deal with: import * as commonjsHelpers from '/__web-dev-server__/rollup/commonjsHelpers.js
if request.path.startswith("/__web-dev-server__/"):
path = f"__web-dev-server__/{path}"
url = f"{self._forward_base}/{path}"
if request.query_string:
url += f"?{request.query_string}"
async with self._websession.get(
url,
headers=request.headers,
allow_redirects=False,
) as result:
return web.Response(
headers={
hdr: val
for hdr, val in result.headers.items()
if hdr not in FILTER_RESPONSE_HEADERS
},
status=result.status,
body=await result.read(),
)

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230227.0"]
"requirements": ["home-assistant-frontend==20230224.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
"iot_class": "cloud_polling",
"requirements": ["odp-amsterdam==5.1.0"]
"requirements": ["odp-amsterdam==5.0.1"]
}

View File

@@ -1,6 +1,7 @@
{
"domain": "hassio",
"name": "Home Assistant Supervisor",
"after_dependencies": ["panel_custom"],
"codeowners": ["@home-assistant/supervisor"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/hassio",

View File

@@ -1,7 +1,6 @@
"""Config flow for HLK-SW16."""
import asyncio
import async_timeout
from hlk_sw16 import create_hlk_sw16_connection
import voluptuous as vol
@@ -36,8 +35,7 @@ async def connect_client(hass, user_input):
reconnect_interval=DEFAULT_RECONNECT_INTERVAL,
keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL,
)
async with async_timeout.timeout(CONNECTION_TIMEOUT):
return await client_aw
return await asyncio.wait_for(client_aw, timeout=CONNECTION_TIMEOUT)
async def validate_input(hass: HomeAssistant, user_input):

View File

@@ -14,7 +14,6 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,

View File

@@ -1,39 +0,0 @@
"""Helper functions for Homematicip Cloud Integration."""
from functools import wraps
import json
import logging
from homeassistant.exceptions import HomeAssistantError
from . import HomematicipGenericEntity
_LOGGER = logging.getLogger(__name__)
def is_error_response(response) -> bool:
"""Response from async call contains errors or not."""
if isinstance(response, dict):
return response.get("errorCode") not in ("", None)
return False
def handle_errors(func):
"""Handle async errors."""
@wraps(func)
async def inner(self: HomematicipGenericEntity) -> None:
"""Handle errors from async call."""
result = await func(self)
if is_error_response(result):
_LOGGER.error(
"Error while execute function %s: %s",
__name__,
json.dumps(result),
)
raise HomeAssistantError(
f"Error while execute function {func.__name__}: {result.get('errorCode')}. See log for more information."
)
return inner

View File

@@ -1,95 +0,0 @@
"""Support for HomematicIP Cloud lock devices."""
from __future__ import annotations
import logging
from typing import Any
from homematicip.aio.device import AsyncDoorLockDrive
from homematicip.base.enums import LockState, MotorState
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity
from .helpers import handle_errors
_LOGGER = logging.getLogger(__name__)
ATTR_AUTO_RELOCK_DELAY = "auto_relock_delay"
ATTR_DOOR_HANDLE_TYPE = "door_handle_type"
ATTR_DOOR_LOCK_DIRECTION = "door_lock_direction"
ATTR_DOOR_LOCK_NEUTRAL_POSITION = "door_lock_neutral_position"
ATTR_DOOR_LOCK_TURNS = "door_lock_turns"
DEVICE_DLD_ATTRIBUTES = {
"autoRelockDelay": ATTR_AUTO_RELOCK_DELAY,
"doorHandleType": ATTR_DOOR_HANDLE_TYPE,
"doorLockDirection": ATTR_DOOR_LOCK_DIRECTION,
"doorLockNeutralPosition": ATTR_DOOR_LOCK_NEUTRAL_POSITION,
"doorLockTurns": ATTR_DOOR_LOCK_TURNS,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the HomematicIP locks from a config entry."""
hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipDoorLockDrive(hap, device)
for device in hap.home.devices
if isinstance(device, AsyncDoorLockDrive)
)
class HomematicipDoorLockDrive(HomematicipGenericEntity, LockEntity):
"""Representation of the HomematicIP DoorLockDrive."""
_attr_supported_features = LockEntityFeature.OPEN
@property
def is_locked(self) -> bool | None:
"""Return true if device is locked."""
return (
self._device.lockState == LockState.LOCKED
and self._device.motorState == MotorState.STOPPED
)
@property
def is_locking(self) -> bool:
"""Return true if device is locking."""
return self._device.motorState == MotorState.CLOSING
@property
def is_unlocking(self) -> bool:
"""Return true if device is unlocking."""
return self._device.motorState == MotorState.OPENING
@handle_errors
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
return await self._device.set_lock_state(LockState.LOCKED)
@handle_errors
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
return await self._device.set_lock_state(LockState.UNLOCKED)
@handle_errors
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
return await self._device.set_lock_state(LockState.OPEN)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
return super().extra_state_attributes | {
attr_key: attr_value
for attr, attr_key in DEVICE_DLD_ATTRIBUTES.items()
if (attr_value := getattr(self._device, attr, None)) is not None
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"iot_class": "cloud_polling",
"loggers": ["somecomfort"],
"requirements": ["aiosomecomfort==0.0.10"]
"requirements": ["aiosomecomfort==0.0.8"]
}

View File

@@ -7,13 +7,6 @@
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Honeywell integration needs to re-authenticate your account",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {

View File

@@ -1,13 +1,22 @@
"""The islamic_prayer_times component."""
from __future__ import annotations
from datetime import timedelta
import logging
from prayer_times_calculator import PrayerTimesCalculator, exceptions
from requests.exceptions import ConnectionError as ConnError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
import homeassistant.util.dt as dt_util
from .const import DOMAIN
from .coordinator import IslamicPrayerDataUpdateCoordinator
from .const import CONF_CALC_METHOD, DATA_UPDATED, DEFAULT_CALC_METHOD, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
@@ -16,32 +25,154 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Islamic Prayer Component."""
coordinator = IslamicPrayerDataUpdateCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, coordinator)
config_entry.async_on_unload(
config_entry.add_update_listener(async_options_updated)
)
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
client = IslamicPrayerClient(hass, config_entry)
hass.data[DOMAIN] = client
await client.async_setup()
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Islamic Prayer entry from config_entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data.pop(DOMAIN)
if coordinator.event_unsub:
coordinator.event_unsub()
return unload_ok
if hass.data[DOMAIN].event_unsub:
hass.data[DOMAIN].event_unsub()
hass.data.pop(DOMAIN)
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Triggered by config entry options updates."""
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN]
if coordinator.event_unsub:
coordinator.event_unsub()
await coordinator.async_request_refresh()
class IslamicPrayerClient:
"""Islamic Prayer Client Object."""
def __init__(self, hass, config_entry):
"""Initialize the Islamic Prayer client."""
self.hass = hass
self.config_entry = config_entry
self.prayer_times_info = {}
self.available = True
self.event_unsub = None
@property
def calc_method(self):
"""Return the calculation method."""
return self.config_entry.options[CONF_CALC_METHOD]
def get_new_prayer_times(self):
"""Fetch prayer times for today."""
calc = PrayerTimesCalculator(
latitude=self.hass.config.latitude,
longitude=self.hass.config.longitude,
calculation_method=self.calc_method,
date=str(dt_util.now().date()),
)
return calc.fetch_prayer_times()
async def async_schedule_future_update(self):
"""Schedule future update for sensors.
Midnight is a calculated time. The specifics of the calculation
depends on the method of the prayer time calculation. This calculated
midnight is the time at which the time to pray the Isha prayers have
expired.
Calculated Midnight: The Islamic midnight.
Traditional Midnight: 12:00AM
Update logic for prayer times:
If the Calculated Midnight is before the traditional midnight then wait
until the traditional midnight to run the update. This way the day
will have changed over and we don't need to do any fancy calculations.
If the Calculated Midnight is after the traditional midnight, then wait
until after the calculated Midnight. We don't want to update the prayer
times too early or else the timings might be incorrect.
Example:
calculated midnight = 11:23PM (before traditional midnight)
Update time: 12:00AM
calculated midnight = 1:35AM (after traditional midnight)
update time: 1:36AM.
"""
_LOGGER.debug("Scheduling next update for Islamic prayer times")
now = dt_util.utcnow()
midnight_dt = self.prayer_times_info["Midnight"]
if now > dt_util.as_utc(midnight_dt):
next_update_at = midnight_dt + timedelta(days=1, minutes=1)
_LOGGER.debug(
"Midnight is after day the changes so schedule update for after"
" Midnight the next day"
)
else:
_LOGGER.debug(
"Midnight is before the day changes so schedule update for the next"
" start of day"
)
next_update_at = dt_util.start_of_local_day(now + timedelta(days=1))
_LOGGER.info("Next update scheduled for: %s", next_update_at)
self.event_unsub = async_track_point_in_time(
self.hass, self.async_update, next_update_at
)
async def async_update(self, *_):
"""Update sensors with new prayer times."""
try:
prayer_times = await self.hass.async_add_executor_job(
self.get_new_prayer_times
)
self.available = True
except (exceptions.InvalidResponseError, ConnError):
self.available = False
_LOGGER.debug("Error retrieving prayer times")
async_call_later(self.hass, 60, self.async_update)
return
for prayer, time in prayer_times.items():
self.prayer_times_info[prayer] = dt_util.parse_datetime(
f"{dt_util.now().date()} {time}"
)
await self.async_schedule_future_update()
_LOGGER.debug("New prayer times retrieved. Updating sensors")
async_dispatcher_send(self.hass, DATA_UPDATED)
async def async_setup(self):
"""Set up the Islamic prayer client."""
await self.async_add_options()
try:
await self.hass.async_add_executor_job(self.get_new_prayer_times)
except (exceptions.InvalidResponseError, ConnError) as err:
raise ConfigEntryNotReady from err
await self.async_update()
self.config_entry.add_update_listener(self.async_options_updated)
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS
)
return True
async def async_add_options(self):
"""Add options for entry."""
if not self.config_entry.options:
data = dict(self.config_entry.data)
calc_method = data.pop(CONF_CALC_METHOD, DEFAULT_CALC_METHOD)
self.hass.config_entries.async_update_entry(
self.config_entry, data=data, options={CONF_CALC_METHOD: calc_method}
)
@staticmethod
async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Triggered by config entry options updates."""
if hass.data[DOMAIN].event_unsub:
hass.data[DOMAIN].event_unsub()
await hass.data[DOMAIN].async_update()

View File

@@ -1,13 +1,10 @@
"""Config flow for Islamic Prayer Times integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME
@@ -25,9 +22,7 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return IslamicPrayerOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -45,9 +40,7 @@ class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_init(self, user_input=None):
"""Manage options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

@@ -1,12 +1,23 @@
"""Constants for the Islamic Prayer component."""
from typing import Final
from prayer_times_calculator import PrayerTimesCalculator
DOMAIN: Final = "islamic_prayer_times"
NAME: Final = "Islamic Prayer Times"
DOMAIN = "islamic_prayer_times"
NAME = "Islamic Prayer Times"
PRAYER_TIMES_ICON = "mdi:calendar-clock"
CONF_CALC_METHOD: Final = "calculation_method"
SENSOR_TYPES = {
"Fajr": "prayer",
"Sunrise": "time",
"Dhuhr": "prayer",
"Asr": "prayer",
"Maghrib": "prayer",
"Isha": "prayer",
"Midnight": "time",
}
CONF_CALC_METHOD = "calculation_method"
CALC_METHODS: list[str] = list(PrayerTimesCalculator.CALCULATION_METHODS)
DEFAULT_CALC_METHOD: Final = "isna"
DEFAULT_CALC_METHOD = "isna"
DATA_UPDATED = "Islamic_prayer_data_updated"

View File

@@ -1,121 +0,0 @@
"""Coordinator for the Islamic prayer times integration."""
from __future__ import annotations
from datetime import datetime, timedelta
import logging
from prayer_times_calculator import PrayerTimesCalculator, exceptions
from requests.exceptions import ConnectionError as ConnError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
from .const import CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN
_LOGGER = logging.getLogger(__name__)
class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetime]]):
"""Islamic Prayer Client Object."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the Islamic Prayer client."""
self.event_unsub: CALLBACK_TYPE | None = None
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
)
@property
def calc_method(self) -> str:
"""Return the calculation method."""
return self.config_entry.options.get(CONF_CALC_METHOD, DEFAULT_CALC_METHOD)
def get_new_prayer_times(self) -> dict[str, str]:
"""Fetch prayer times for today."""
calc = PrayerTimesCalculator(
latitude=self.hass.config.latitude,
longitude=self.hass.config.longitude,
calculation_method=self.calc_method,
date=str(dt_util.now().date()),
)
return calc.fetch_prayer_times()
@callback
def async_schedule_future_update(self, midnight_dt: datetime) -> None:
"""Schedule future update for sensors.
Midnight is a calculated time. The specifics of the calculation
depends on the method of the prayer time calculation. This calculated
midnight is the time at which the time to pray the Isha prayers have
expired.
Calculated Midnight: The Islamic midnight.
Traditional Midnight: 12:00AM
Update logic for prayer times:
If the Calculated Midnight is before the traditional midnight then wait
until the traditional midnight to run the update. This way the day
will have changed over and we don't need to do any fancy calculations.
If the Calculated Midnight is after the traditional midnight, then wait
until after the calculated Midnight. We don't want to update the prayer
times too early or else the timings might be incorrect.
Example:
calculated midnight = 11:23PM (before traditional midnight)
Update time: 12:00AM
calculated midnight = 1:35AM (after traditional midnight)
update time: 1:36AM.
"""
_LOGGER.debug("Scheduling next update for Islamic prayer times")
now = dt_util.utcnow()
if now > midnight_dt:
next_update_at = midnight_dt + timedelta(days=1, minutes=1)
_LOGGER.debug(
"Midnight is after the day changes so schedule update for after Midnight the next day"
)
else:
_LOGGER.debug(
"Midnight is before the day changes so schedule update for the next start of day"
)
next_update_at = dt_util.start_of_local_day(now + timedelta(days=1))
_LOGGER.debug("Next update scheduled for: %s", next_update_at)
self.event_unsub = async_track_point_in_time(
self.hass, self.async_request_update, next_update_at
)
async def async_request_update(self, *_) -> None:
"""Request update from coordinator."""
await self.async_request_refresh()
async def _async_update_data(self) -> dict[str, datetime]:
"""Update sensors with new prayer times."""
try:
prayer_times = await self.hass.async_add_executor_job(
self.get_new_prayer_times
)
except (exceptions.InvalidResponseError, ConnError) as err:
async_call_later(self.hass, 60, self.async_request_update)
raise UpdateFailed from err
prayer_times_info: dict[str, datetime] = {}
for prayer, time in prayer_times.items():
if prayer_time := dt_util.parse_datetime(f"{dt_util.now().date()} {time}"):
prayer_times_info[prayer] = dt_util.as_utc(prayer_time)
self.async_schedule_future_update(prayer_times_info["Midnight"])
return prayer_times_info

View File

@@ -1,51 +1,12 @@
"""Platform to retrieve Islamic prayer times information for Home Assistant."""
from datetime import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
import homeassistant.util.dt as dt_util
from . import IslamicPrayerDataUpdateCoordinator
from .const import DOMAIN, NAME
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="Fajr",
name="Fajr prayer",
),
SensorEntityDescription(
key="Sunrise",
name="Sunrise time",
),
SensorEntityDescription(
key="Dhuhr",
name="Dhuhr prayer",
),
SensorEntityDescription(
key="Asr",
name="Asr prayer",
),
SensorEntityDescription(
key="Maghrib",
name="Maghrib prayer",
),
SensorEntityDescription(
key="Isha",
name="Isha prayer",
),
SensorEntityDescription(
key="Midnight",
name="Midnight time",
),
)
from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES
async def async_setup_entry(
@@ -55,38 +16,46 @@ async def async_setup_entry(
) -> None:
"""Set up the Islamic prayer times sensor platform."""
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN]
client = hass.data[DOMAIN]
async_add_entities(
IslamicPrayerTimeSensor(coordinator, description)
for description in SENSOR_TYPES
)
entities = []
for sensor_type in SENSOR_TYPES:
entities.append(IslamicPrayerTimeSensor(sensor_type, client))
async_add_entities(entities, True)
class IslamicPrayerTimeSensor(
CoordinatorEntity[IslamicPrayerDataUpdateCoordinator], SensorEntity
):
class IslamicPrayerTimeSensor(SensorEntity):
"""Representation of an Islamic prayer time sensor."""
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_has_entity_name = True
_attr_icon = PRAYER_TIMES_ICON
_attr_should_poll = False
def __init__(
self,
coordinator: IslamicPrayerDataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
def __init__(self, sensor_type, client):
"""Initialize the Islamic prayer time sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = description.key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=NAME,
entry_type=DeviceEntryType.SERVICE,
)
self.sensor_type = sensor_type
self.client = client
@property
def native_value(self) -> datetime:
def name(self):
"""Return the name of the sensor."""
return f"{self.sensor_type} {SENSOR_TYPES[self.sensor_type]}"
@property
def unique_id(self):
"""Return the unique id of the entity."""
return self.sensor_type
@property
def native_value(self):
"""Return the state of the sensor."""
return self.coordinator.data[self.entity_description.key]
return self.client.prayer_times_info.get(self.sensor_type).astimezone(
dt_util.UTC
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(self.hass, DATA_UPDATED, self.async_write_ha_state)
)

View File

@@ -8,43 +8,16 @@ from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity
from .services import (
SERVICE_DELETE_USER_CODE_SCHEMA,
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE,
SERVICE_SET_USER_CODE_SCHEMA,
SERVICE_SET_ZWAVE_LOCK_USER_CODE,
)
VALUE_TO_STATE = {0: False, 100: True}
@callback
def async_setup_lock_services(hass: HomeAssistant) -> None:
"""Create lock-specific services for the ISY Integration."""
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_ZWAVE_LOCK_USER_CODE,
SERVICE_SET_USER_CODE_SCHEMA,
"async_set_zwave_lock_user_code",
)
platform.async_register_entity_service(
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE,
SERVICE_DELETE_USER_CODE_SCHEMA,
"async_delete_zwave_lock_user_code",
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@@ -59,7 +32,6 @@ async def async_setup_entry(
entities.append(ISYLockProgramEntity(name, status, actions))
async_add_entities(entities)
async_setup_lock_services(hass)
class ISYLockEntity(ISYNodeEntity, LockEntity):
@@ -75,26 +47,12 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Send the lock command to the ISY device."""
if not await self._node.secure_lock():
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
_LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs: Any) -> None:
"""Send the unlock command to the ISY device."""
if not await self._node.secure_unlock():
raise HomeAssistantError(f"Unable to unlock device {self._node.address}")
async def async_set_zwave_lock_user_code(self, user_num: int, code: int) -> None:
"""Set a user lock code for a Z-Wave Lock."""
if not await self._node.set_zwave_lock_code(user_num, code):
raise HomeAssistantError(
f"Could not set user code {user_num} for {self._node.address}"
)
async def async_delete_zwave_lock_user_code(self, user_num: int) -> None:
"""Delete a user lock code for a Z-Wave Lock."""
if not await self._node.delete_zwave_lock_code(user_num):
raise HomeAssistantError(
f"Could not delete user code {user_num} for {self._node.address}"
)
_LOGGER.error("Unable to lock device")
class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
@@ -108,9 +66,9 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
if not await self._actions.run_then():
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
_LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
if not await self._actions.run_else():
raise HomeAssistantError(f"Unable to unlock device {self._node.address}")
_LOGGER.error("Unable to unlock device")

View File

@@ -24,7 +24,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyisy"],
"requirements": ["pyisy==3.1.14"],
"requirements": ["pyisy==3.1.13"],
"ssdp": [
{
"manufacturer": "Universal Devices Inc.",

View File

@@ -52,14 +52,8 @@ SERVICE_RENAME_NODE = "rename_node"
SERVICE_SET_ON_LEVEL = "set_on_level"
SERVICE_SET_RAMP_RATE = "set_ramp_rate"
# Services valid only for Z-Wave Locks
SERVICE_SET_ZWAVE_LOCK_USER_CODE = "set_zwave_lock_user_code"
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE = "delete_zwave_lock_user_code"
CONF_PARAMETER = "parameter"
CONF_PARAMETERS = "parameters"
CONF_USER_NUM = "user_num"
CONF_CODE = "code"
CONF_VALUE = "value"
CONF_INIT = "init"
CONF_ISY = "isy"
@@ -135,13 +129,6 @@ SERVICE_SET_ZWAVE_PARAMETER_SCHEMA = {
vol.Required(CONF_SIZE): vol.All(vol.Coerce(int), vol.In(VALID_PARAMETER_SIZES)),
}
SERVICE_SET_USER_CODE_SCHEMA = {
vol.Required(CONF_USER_NUM): vol.Coerce(int),
vol.Required(CONF_CODE): vol.Coerce(int),
}
SERVICE_DELETE_USER_CODE_SCHEMA = {vol.Required(CONF_USER_NUM): vol.Coerce(int)}
SERVICE_SET_VARIABLE_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_ADDRESS, CONF_TYPE, CONF_NAME),
vol.Schema(

View File

@@ -118,52 +118,6 @@ set_zwave_parameter:
- "1"
- "2"
- "4"
set_zwave_lock_user_code:
name: Set Z-Wave Lock User Code
description: >-
Set a Z-Wave Lock User Code via the ISY.
target:
entity:
integration: isy994
domain: lock
fields:
user_num:
name: User Number
description: The user slot number on the lock
required: true
example: 8
selector:
number:
min: 1
max: 255
code:
name: Code
description: The code to set for the user.
required: true
example: 33491663
selector:
number:
min: 1
max: 99999999
mode: box
delete_zwave_lock_user_code:
name: Delete Z-Wave Lock User Code
description: >-
Delete a Z-Wave Lock User Code via the ISY.
target:
entity:
integration: isy994
domain: lock
fields:
user_num:
name: User Number
description: The user slot number on the lock
required: true
example: 8
selector:
number:
min: 1
max: 255
rename_node:
name: Rename Node on ISY
description: >-

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["xknx"],
"quality_scale": "platinum",
"requirements": ["xknx==2.6.0"]
"requirements": ["xknx==2.5.0"]
}

View File

@@ -17,9 +17,10 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later, async_track_time_interval
@@ -166,9 +167,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
We do not want the discovery task to block startup.
"""
hass.async_create_background_task(
discovery_manager.async_discovery(), "lifx-discovery"
)
task = asyncio.create_task(discovery_manager.async_discovery())
@callback
def _async_stop(_: Event) -> None:
if not task.done():
task.cancel()
# Task must be shut down when home assistant is closing
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
# Let the system settle a bit before starting discovery
# to reduce the risk we miss devices because the event

View File

@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
system.on_connected_changed(handle_connected_changed)
async def handle_stop(event: Event) -> None:
async def handle_stop(event) -> None:
await system.close()
entry.async_on_unload(

View File

@@ -76,7 +76,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult:
async def async_step_import(self, import_data):
"""Import litejet config from configuration.yaml."""
return self.async_create_entry(title=import_data[CONF_PORT], data=import_data)

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from collections.abc import Callable
from datetime import datetime
from typing import cast
from pylitejet import LiteJet
import voluptuous as vol
@@ -44,7 +42,7 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Listen for events based on configuration."""
trigger_data = trigger_info["trigger_data"]
number = cast(int, config[CONF_NUMBER])
number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN)
held_less_than = config.get(CONF_HELD_LESS_THAN)
pressed_time = None
@@ -52,7 +50,7 @@ async def async_attach_trigger(
job = HassJob(action)
@callback
def call_action() -> None:
def call_action():
"""Call action with right context."""
hass.async_run_hass_job(
job,
@@ -74,11 +72,11 @@ async def async_attach_trigger(
# neither: trigger on pressed
@callback
def pressed_more_than_satisfied(now: datetime) -> None:
def pressed_more_than_satisfied(now):
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
call_action()
def pressed() -> None:
def pressed():
"""Handle the press of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
@@ -90,12 +88,10 @@ async def async_attach_trigger(
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
)
def released() -> None:
def released():
"""Handle the release of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
if pressed_time is None:
return
if cancel_pressed_more_than is not None:
cancel_pressed_more_than()
cancel_pressed_more_than = None
@@ -114,7 +110,7 @@ async def async_attach_trigger(
system.on_switch_released(number, released)
@callback
def async_remove() -> None:
def async_remove():
"""Remove all subscriptions used for this trigger."""
system.unsubscribe(pressed)
system.unsubscribe(released)

View File

@@ -8,15 +8,14 @@ from aiolivisi import AioLivisi
from homeassistant import core
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry as dr
from .const import DOMAIN
from .const import DOMAIN, SWITCH_PLATFORM
from .coordinator import LivisiDataUpdateCoordinator
PLATFORMS: Final = [Platform.CLIMATE, Platform.SWITCH]
PLATFORMS: Final = [SWITCH_PLATFORM]
async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -1,212 +0,0 @@
"""Code to handle a Livisi Virtual Climate Control."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aiolivisi.const import CAPABILITY_MAP
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DOMAIN,
LIVISI_REACHABILITY_CHANGE,
LIVISI_STATE_CHANGE,
LOGGER,
MAX_TEMPERATURE,
MIN_TEMPERATURE,
VRCC_DEVICE_TYPE,
)
from .coordinator import LivisiDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up climate device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
@callback
def handle_coordinator_update() -> None:
"""Add climate device."""
shc_devices: list[dict[str, Any]] = coordinator.data
entities: list[ClimateEntity] = []
for device in shc_devices:
if (
device["type"] == VRCC_DEVICE_TYPE
and device["id"] not in coordinator.devices
):
livisi_climate: ClimateEntity = create_entity(
config_entry, device, coordinator
)
LOGGER.debug("Include device type: %s", device.get("type"))
coordinator.devices.add(device["id"])
entities.append(livisi_climate)
async_add_entities(entities)
config_entry.async_on_unload(
coordinator.async_add_listener(handle_coordinator_update)
)
def create_entity(
config_entry: ConfigEntry,
device: dict[str, Any],
coordinator: LivisiDataUpdateCoordinator,
) -> ClimateEntity:
"""Create Climate Entity."""
capabilities: Mapping[str, Any] = device[CAPABILITY_MAP]
room_id: str = device["location"]
room_name: str = coordinator.rooms[room_id]
livisi_climate = LivisiClimate(
config_entry,
coordinator,
unique_id=device["id"],
manufacturer=device["manufacturer"],
device_type=device["type"],
target_temperature_capability=capabilities["RoomSetpoint"],
temperature_capability=capabilities["RoomTemperature"],
humidity_capability=capabilities["RoomHumidity"],
room=room_name,
)
return livisi_climate
class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntity):
"""Represents the Livisi Climate."""
_attr_hvac_modes = [HVACMode.HEAT]
_attr_hvac_mode = HVACMode.HEAT
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_target_temperature_high = MAX_TEMPERATURE
_attr_target_temperature_low = MIN_TEMPERATURE
def __init__(
self,
config_entry: ConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
unique_id: str,
manufacturer: str,
device_type: str,
target_temperature_capability: str,
temperature_capability: str,
humidity_capability: str,
room: str,
) -> None:
"""Initialize the Livisi Climate."""
self.config_entry = config_entry
self._attr_unique_id = unique_id
self._target_temperature_capability = target_temperature_capability
self._temperature_capability = temperature_capability
self._humidity_capability = humidity_capability
self.aio_livisi = coordinator.aiolivisi
self._attr_available = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
manufacturer=manufacturer,
model=device_type,
name=room,
suggested_area=room,
via_device=(DOMAIN, config_entry.entry_id),
)
super().__init__(coordinator)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
response = await self.aio_livisi.async_vrcc_set_temperature(
self._target_temperature_capability,
kwargs.get(ATTR_TEMPERATURE),
self.coordinator.is_avatar,
)
if response is None:
self._attr_available = False
raise HomeAssistantError(f"Failed to turn off {self._attr_name}")
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Do nothing as LIVISI devices do not support changing the hvac mode."""
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
target_temperature = await self.coordinator.async_get_vrcc_target_temperature(
self._target_temperature_capability
)
temperature = await self.coordinator.async_get_vrcc_temperature(
self._temperature_capability
)
humidity = await self.coordinator.async_get_vrcc_humidity(
self._humidity_capability
)
if temperature is None:
self._attr_current_temperature = None
self._attr_available = False
else:
self._attr_target_temperature = target_temperature
self._attr_current_temperature = temperature
self._attr_current_humidity = humidity
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._target_temperature_capability}",
self.update_target_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._temperature_capability}",
self.update_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._humidity_capability}",
self.update_humidity,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}",
self.update_reachability,
)
)
@callback
def update_target_temperature(self, target_temperature: float) -> None:
"""Update the target temperature of the climate device."""
self._attr_target_temperature = target_temperature
self.async_write_ha_state()
@callback
def update_temperature(self, current_temperature: float) -> None:
"""Update the current temperature of the climate device."""
self._attr_current_temperature = current_temperature
self.async_write_ha_state()
@callback
def update_humidity(self, humidity: int) -> None:
"""Update the humidity temperature of the climate device."""
self._attr_current_humidity = humidity
self.async_write_ha_state()
@callback
def update_reachability(self, is_reachable: bool) -> None:
"""Update the reachability of the climate device."""
self._attr_available = is_reachable
self.async_write_ha_state()

View File

@@ -7,15 +7,12 @@ DOMAIN = "livisi"
CONF_HOST = "host"
CONF_PASSWORD: Final = "password"
AVATAR = "Avatar"
AVATAR_PORT: Final = 9090
CLASSIC_PORT: Final = 8080
DEVICE_POLLING_DELAY: Final = 60
LIVISI_STATE_CHANGE: Final = "livisi_state_change"
LIVISI_REACHABILITY_CHANGE: Final = "livisi_reachability_change"
PSS_DEVICE_TYPE: Final = "PSS"
VRCC_DEVICE_TYPE: Final = "VRCC"
SWITCH_PLATFORM: Final = "switch"
MAX_TEMPERATURE: Final = 30.0
MIN_TEMPERATURE: Final = 6.0
PSS_DEVICE_TYPE: Final = "PSS"

View File

@@ -13,7 +13,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
AVATAR,
AVATAR_PORT,
CLASSIC_PORT,
CONF_HOST,
@@ -70,14 +69,14 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
livisi_connection_data=livisi_connection_data
)
controller_data = await self.aiolivisi.async_get_controller()
if (controller_type := controller_data["controllerType"]) == AVATAR:
if controller_data["controllerType"] == "Avatar":
self.port = AVATAR_PORT
self.is_avatar = True
else:
self.port = CLASSIC_PORT
self.is_avatar = False
self.controller_type = controller_type
self.serial_number = controller_data["serialNumber"]
self.controller_type = controller_data["controllerType"]
async def async_get_devices(self) -> list[dict[str, Any]]:
"""Set the discovered devices list."""
@@ -85,7 +84,7 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
async def async_get_pss_state(self, capability: str) -> bool | None:
"""Set the PSS state."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
response: dict[str, Any] = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
@@ -93,35 +92,6 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
on_state = response["onState"]
return on_state["value"]
async def async_get_vrcc_target_temperature(self, capability: str) -> float | None:
"""Get the target temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
if self.is_avatar:
return response["setpointTemperature"]["value"]
return response["pointTemperature"]["value"]
async def async_get_vrcc_temperature(self, capability: str) -> float | None:
"""Get the temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["temperature"]["value"]
async def async_get_vrcc_humidity(self, capability: str) -> int | None:
"""Get the humidity of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["humidity"]["value"]
async def async_set_all_rooms(self) -> None:
"""Set the room list."""
response: list[dict[str, Any]] = await self.aiolivisi.async_get_all_rooms()
@@ -138,12 +108,6 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.onState,
)
if event_data.vrccData is not None:
async_dispatcher_send(
self.hass,
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.vrccData,
)
if event_data.isReachable is not None:
async_dispatcher_send(
self.hass,

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/livisi",
"iot_class": "local_polling",
"requirements": ["aiolivisi==0.0.16"]
"requirements": ["aiolivisi==0.0.15"]
}

View File

@@ -33,7 +33,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.service import remove_entity_service_fields
from homeassistant.helpers.typing import ConfigType, StateType
_LOGGER = logging.getLogger(__name__)
@@ -93,7 +92,7 @@ async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for locking {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_lock(**remove_entity_service_fields(service_call))
await entity.async_lock(**service_call.data)
async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
@@ -103,7 +102,7 @@ async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for unlocking {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_unlock(**remove_entity_service_fields(service_call))
await entity.async_unlock(**service_call.data)
async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
@@ -113,7 +112,7 @@ async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None:
raise ValueError(
f"Code '{code}' for opening {entity.entity_id} doesn't match pattern {entity.code_format}"
)
await entity.async_open(**remove_entity_service_fields(service_call))
await entity.async_open(**service_call.data)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -1,7 +1,6 @@
"""Matter light."""
from __future__ import annotations
from enum import IntFlag
from typing import Any
from chip.clusters import Objects as clusters
@@ -261,16 +260,12 @@ class MatterLight(MatterEntity, LightEntity):
color_temp = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
if self.supported_color_modes is not None:
if hs_color is not None and ColorMode.HS in self.supported_color_modes:
await self._set_hs_color(hs_color)
elif xy_color is not None and ColorMode.XY in self.supported_color_modes:
await self._set_xy_color(xy_color)
elif (
color_temp is not None
and ColorMode.COLOR_TEMP in self.supported_color_modes
):
await self._set_color_temp(color_temp)
if hs_color is not None and self.supports_color:
await self._set_hs_color(hs_color)
elif xy_color is not None:
await self._set_xy_color(xy_color)
elif color_temp is not None and self.supports_color_temperature:
await self._set_color_temp(color_temp)
if brightness is not None and self.supports_brightness:
await self._set_brightness(brightness)
@@ -289,6 +284,7 @@ class MatterLight(MatterEntity, LightEntity):
@callback
def _update_from_device(self) -> None:
"""Update from device."""
if self._attr_supported_color_modes is None:
# work out what (color)features are supported
supported_color_modes: set[ColorMode] = set()
@@ -301,19 +297,30 @@ class MatterLight(MatterEntity, LightEntity):
if self._entity_info.endpoint.has_attribute(
None, clusters.ColorControl.Attributes.ColorMode
):
capabilities = self.get_matter_attribute_value(
clusters.ColorControl.Attributes.ColorCapabilities
# device has some color support, check which color modes
# are supported with the featuremap on the ColorControl cluster
color_feature_map = self.get_matter_attribute_value(
clusters.ColorControl.Attributes.FeatureMap,
)
assert capabilities is not None
if capabilities & ColorCapabilities.kHueSaturationSupported:
if (
color_feature_map
& clusters.ColorControl.Attributes.CurrentHue.attribute_id
):
supported_color_modes.add(ColorMode.HS)
if capabilities & ColorCapabilities.kXYAttributesSupported:
if (
color_feature_map
& clusters.ColorControl.Attributes.CurrentX.attribute_id
):
supported_color_modes.add(ColorMode.XY)
if capabilities & ColorCapabilities.kColorTemperatureSupported:
# color temperature support detection using the featuremap is not reliable
# (temporary?) fallback to checking the value
if (
self.get_matter_attribute_value(
clusters.ColorControl.Attributes.ColorTemperatureMireds
)
is not None
):
supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_supported_color_modes = supported_color_modes
@@ -344,23 +351,11 @@ class MatterLight(MatterEntity, LightEntity):
self._attr_brightness = self._get_brightness()
# This enum should be removed once the ColorControlCapabilities enum is added to the CHIP (Matter) library
# clusters.ColorControl.Bitmap.ColorCapabilities
class ColorCapabilities(IntFlag):
"""Color control capabilities bitmap."""
kHueSaturationSupported = 0x1
kEnhancedHueSupported = 0x2
kColorLoopSupported = 0x4
kXYAttributesSupported = 0x8
kColorTemperatureSupported = 0x10
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LIGHT,
entity_description=LightEntityDescription(key="MatterLight"),
entity_description=LightEntityDescription(key="ExtendedMatterLight"),
entity_class=MatterLight,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
optional_attributes=(

View File

@@ -8,11 +8,11 @@ from datetime import timedelta
from functools import cached_property
from typing import Any, Generic, TypeVar
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from nibe.connection import Connection
from nibe.connection.modbus import Modbus
from nibe.connection.nibegw import NibeGW, ProductInfo
from nibe.exceptions import CoilNotFoundException, ReadException
from nibe.exceptions import CoilNotFoundException, CoilReadException
from nibe.heatpump import HeatPump, Model, Series
from homeassistant.config_entries import ConfigEntry
@@ -182,7 +182,7 @@ class ContextCoordinator(
return release_update
class Coordinator(ContextCoordinator[dict[int, CoilData], int]):
class Coordinator(ContextCoordinator[dict[int, Coil], int]):
"""Update coordinator for nibe heat pumps."""
config_entry: ConfigEntry
@@ -199,18 +199,17 @@ class Coordinator(ContextCoordinator[dict[int, CoilData], int]):
)
self.data = {}
self.seed: dict[int, CoilData] = {}
self.seed: dict[int, Coil] = {}
self.connection = connection
self.heatpump = heatpump
self.task: asyncio.Task | None = None
heatpump.subscribe(heatpump.COIL_UPDATE_EVENT, self._on_coil_update)
def _on_coil_update(self, data: CoilData):
def _on_coil_update(self, coil: Coil):
"""Handle callback on coil updates."""
coil = data.coil
self.data[coil.address] = data
self.seed[coil.address] = data
self.data[coil.address] = coil
self.seed[coil.address] = coil
self.async_update_context_listeners([coil.address])
@property
@@ -247,26 +246,26 @@ class Coordinator(ContextCoordinator[dict[int, CoilData], int]):
async def async_write_coil(self, coil: Coil, value: int | float | str) -> None:
"""Write coil and update state."""
data = CoilData(coil, value)
await self.connection.write_coil(data)
coil.value = value
coil = await self.connection.write_coil(coil)
self.data[coil.address] = data
self.data[coil.address] = coil
self.async_update_context_listeners([coil.address])
async def async_read_coil(self, coil: Coil) -> CoilData:
async def async_read_coil(self, coil: Coil) -> Coil:
"""Read coil and update state using callbacks."""
return await self.connection.read_coil(coil)
async def _async_update_data(self) -> dict[int, CoilData]:
async def _async_update_data(self) -> dict[int, Coil]:
self.task = asyncio.current_task()
try:
return await self._async_update_data_internal()
finally:
self.task = None
async def _async_update_data_internal(self) -> dict[int, CoilData]:
result: dict[int, CoilData] = {}
async def _async_update_data_internal(self) -> dict[int, Coil]:
result: dict[int, Coil] = {}
def _get_coils() -> Iterable[Coil]:
for address in sorted(self.context_callbacks.keys()):
@@ -283,10 +282,10 @@ class Coordinator(ContextCoordinator[dict[int, CoilData], int]):
yield coil
try:
async for data in self.connection.read_coils(_get_coils()):
result[data.coil.address] = data
self.seed.pop(data.coil.address, None)
except ReadException as exception:
async for coil in self.connection.read_coils(_get_coils()):
result[coil.address] = coil
self.seed.pop(coil.address, None)
except CoilReadException as exception:
if not result:
raise UpdateFailed(f"Failed to update: {exception}") from exception
self.logger.debug(
@@ -330,7 +329,7 @@ class CoilEntity(CoordinatorEntity[Coordinator]):
self.coordinator.data or {}
)
def _async_read_coil(self, data: CoilData):
def _async_read_coil(self, coil: Coil):
"""Update state of entity based on coil data."""
async def _async_write_coil(self, value: int | float | str):
@@ -338,9 +337,10 @@ class CoilEntity(CoordinatorEntity[Coordinator]):
await self.coordinator.async_write_coil(self._coil, value)
def _handle_coordinator_update(self) -> None:
data = self.coordinator.data.get(self._coil.address)
if data is None:
coil = self.coordinator.data.get(self._coil.address)
if coil is None:
return
self._async_read_coil(data)
self._coil = coil
self._async_read_coil(coil)
self.async_write_ha_state()

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump binary sensors."""
from __future__ import annotations
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
@@ -37,5 +37,5 @@ class BinarySensor(CoilEntity, BinarySensorEntity):
"""Initialize entity."""
super().__init__(coordinator, coil, ENTITY_ID_FORMAT)
def _async_read_coil(self, data: CoilData) -> None:
self._attr_is_on = data.value == "ON"
def _async_read_coil(self, coil: Coil) -> None:
self._attr_is_on = coil.value == "ON"

View File

@@ -8,10 +8,10 @@ from nibe.connection.nibegw import NibeGW
from nibe.exceptions import (
AddressInUseException,
CoilNotFoundException,
CoilReadException,
CoilReadSendException,
CoilWriteException,
CoilWriteSendException,
ReadException,
ReadSendException,
WriteException,
)
from nibe.heatpump import HeatPump, Model
import voluptuous as vol
@@ -108,13 +108,13 @@ async def validate_nibegw_input(
try:
await connection.verify_connectivity()
except (ReadSendException, CoilWriteSendException) as exception:
except (CoilReadSendException, CoilWriteSendException) as exception:
raise FieldError(str(exception), CONF_IP_ADDRESS, "address") from exception
except CoilNotFoundException as exception:
raise FieldError("Coils not found", "base", "model") from exception
except ReadException as exception:
except CoilReadException as exception:
raise FieldError("Timeout on read from pump", "base", "read") from exception
except WriteException as exception:
except CoilWriteException as exception:
raise FieldError("Timeout on writing to pump", "base", "write") from exception
finally:
await connection.stop()
@@ -147,13 +147,13 @@ async def validate_modbus_input(
try:
await connection.verify_connectivity()
except (ReadSendException, CoilWriteSendException) as exception:
except (CoilReadSendException, CoilWriteSendException) as exception:
raise FieldError(str(exception), CONF_MODBUS_URL, "address") from exception
except CoilNotFoundException as exception:
raise FieldError("Coils not found", "base", "model") from exception
except ReadException as exception:
except CoilReadException as exception:
raise FieldError("Timeout on read from pump", "base", "read") from exception
except WriteException as exception:
except CoilWriteException as exception:
raise FieldError("Timeout on writing to pump", "base", "write") from exception
finally:
await connection.stop()

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
"iot_class": "local_polling",
"requirements": ["nibe==2.0.0"]
"requirements": ["nibe==1.6.0"]
}

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump numbers."""
from __future__ import annotations
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from homeassistant.components.number import ENTITY_ID_FORMAT, NumberEntity
from homeassistant.config_entries import ConfigEntry
@@ -58,13 +58,13 @@ class Number(CoilEntity, NumberEntity):
self._attr_native_unit_of_measurement = coil.unit
self._attr_native_value = None
def _async_read_coil(self, data: CoilData) -> None:
if data.value is None:
def _async_read_coil(self, coil: Coil) -> None:
if coil.value is None:
self._attr_native_value = None
return
try:
self._attr_native_value = float(data.value)
self._attr_native_value = float(coil.value)
except ValueError:
self._attr_native_value = None

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump select."""
from __future__ import annotations
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from homeassistant.components.select import ENTITY_ID_FORMAT, SelectEntity
from homeassistant.config_entries import ConfigEntry
@@ -40,12 +40,12 @@ class Select(CoilEntity, SelectEntity):
self._attr_options = list(coil.mappings.values())
self._attr_current_option = None
def _async_read_coil(self, data: CoilData) -> None:
if not isinstance(data.value, str):
def _async_read_coil(self, coil: Coil) -> None:
if not isinstance(coil.value, str):
self._attr_current_option = None
return
self._attr_current_option = data.value
self._attr_current_option = coil.value
async def async_select_option(self, option: str) -> None:
"""Support writing value."""

View File

@@ -1,7 +1,7 @@
"""The Nibe Heat Pump sensors."""
from __future__ import annotations
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from homeassistant.components.sensor import (
ENTITY_ID_FORMAT,
@@ -146,5 +146,5 @@ class Sensor(CoilEntity, SensorEntity):
self._attr_native_unit_of_measurement = coil.unit
self._attr_entity_category = EntityCategory.DIAGNOSTIC
def _async_read_coil(self, data: CoilData):
self._attr_native_value = data.value
def _async_read_coil(self, coil: Coil):
self._attr_native_value = coil.value

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from nibe.coil import Coil, CoilData
from nibe.coil import Coil
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -40,8 +40,8 @@ class Switch(CoilEntity, SwitchEntity):
super().__init__(coordinator, coil, ENTITY_ID_FORMAT)
self._attr_is_on = None
def _async_read_coil(self, data: CoilData) -> None:
self._attr_is_on = data.value == "ON"
def _async_read_coil(self, coil: Coil) -> None:
self._attr_is_on = coil.value == "ON"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""

View File

@@ -1,18 +1 @@
"""The Obihai integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
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)

View File

@@ -1,73 +0,0 @@
"""Config flow to configure the Obihai integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .connectivity import validate_auth
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(
CONF_USERNAME,
default=DEFAULT_USERNAME,
): str,
vol.Optional(
CONF_PASSWORD,
default=DEFAULT_PASSWORD,
): str,
}
)
class ObihaiFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Obihai."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
if await self.hass.async_add_executor_job(
validate_auth,
user_input[CONF_HOST],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
):
return self.async_create_entry(
title=user_input[CONF_HOST],
data=user_input,
)
errors["base"] = "cannot_connect"
data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input)
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=data_schema,
)
# DEPRECATED
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Handle a flow initialized by importing a config."""
self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]})
return self.async_create_entry(
title=config.get(CONF_NAME, config[CONF_HOST]),
data={
CONF_HOST: config[CONF_HOST],
CONF_PASSWORD: config[CONF_PASSWORD],
CONF_USERNAME: config[CONF_USERNAME],
},
)

View File

@@ -1,67 +0,0 @@
"""Support for Obihai Connectivity."""
from __future__ import annotations
from pyobihai import PyObihai
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, LOGGER
def get_pyobihai(
host: str,
username: str,
password: str,
) -> PyObihai:
"""Retrieve an authenticated PyObihai."""
return PyObihai(host, username, password)
def validate_auth(
host: str,
username: str,
password: str,
) -> bool:
"""Test if the given setting works as expected."""
obi = get_pyobihai(host, username, password)
login = obi.check_account()
if not login:
LOGGER.debug("Invalid credentials")
return False
return True
class ObihaiConnection:
"""Contains a list of Obihai Sensors."""
def __init__(
self,
host: str,
username: str = DEFAULT_USERNAME,
password: str = DEFAULT_PASSWORD,
) -> None:
"""Store configuration."""
self.sensors: list = []
self.host = host
self.username = username
self.password = password
self.serial: list = []
self.services: list = []
self.line_services: list = []
self.call_direction: list = []
self.pyobihai: PyObihai = None
def update(self) -> bool:
"""Validate connection and retrieve a list of sensors."""
if not self.pyobihai:
self.pyobihai = get_pyobihai(self.host, self.username, self.password)
if not self.pyobihai.check_account():
return False
self.serial = self.pyobihai.get_device_serial()
self.services = self.pyobihai.get_state()
self.line_services = self.pyobihai.get_line_state()
self.call_direction = self.pyobihai.get_call_direction()
return True

View File

@@ -1,15 +0,0 @@
"""Constants for the Obihai integration."""
import logging
from typing import Final
from homeassistant.const import Platform
DOMAIN: Final = "obihai"
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
OBIHAI = "Obihai"
LOGGER = logging.getLogger(__package__)
PLATFORMS: Final = [Platform.SENSOR]

View File

@@ -1,8 +1,7 @@
{
"domain": "obihai",
"name": "Obihai",
"codeowners": ["@dshokouhi", "@ejpenney"],
"config_flow": true,
"codeowners": ["@dshokouhi"],
"documentation": "https://www.home-assistant.io/integrations/obihai",
"iot_class": "local_polling",
"loggers": ["pyobihai"],

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
from datetime import timedelta
import logging
from pyobihai import PyObihai
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -10,19 +12,20 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .connectivity import ObihaiConnection
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, DOMAIN, OBIHAI
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
OBIHAI = "Obihai"
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@@ -32,58 +35,46 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
# DEPRECATED
async def async_setup_platform(
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Obihai sensor platform."""
issue_registry.async_create_issue(
hass,
DOMAIN,
"manual_migration",
breaks_in_ha_version="2023.6.0",
is_fixable=False,
severity=issue_registry.IssueSeverity.ERROR,
translation_key="manual_migration",
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
host = config[CONF_HOST]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Obihai sensor entries."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
host = entry.data[CONF_HOST]
requester = ObihaiConnection(host, username, password)
await hass.async_add_executor_job(requester.update)
sensors = []
for key in requester.services:
sensors.append(ObihaiServiceSensors(requester.pyobihai, requester.serial, key))
if requester.line_services is not None:
for key in requester.line_services:
sensors.append(
ObihaiServiceSensors(requester.pyobihai, requester.serial, key)
)
pyobihai = PyObihai(host, username, password)
for key in requester.call_direction:
sensors.append(ObihaiServiceSensors(requester.pyobihai, requester.serial, key))
login = pyobihai.check_account()
if not login:
_LOGGER.error("Invalid credentials")
return
async_add_entities(sensors, update_before_add=True)
serial = pyobihai.get_device_serial()
services = pyobihai.get_state()
line_services = pyobihai.get_line_state()
call_direction = pyobihai.get_call_direction()
for key in services:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
if line_services is not None:
for key in line_services:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
for key in call_direction:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
add_entities(sensors)
class ObihaiServiceSensors(SensorEntity):
@@ -157,10 +148,6 @@ class ObihaiServiceSensors(SensorEntity):
def update(self) -> None:
"""Update the sensor."""
if not self._pyobihai.check_account():
self._state = None
return
services = self._pyobihai.get_state()
if self._service_name in services:

View File

@@ -1,25 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"issues": {
"manual_migration": {
"title": "Manual migration required for Obihai",
"description": "Configuration of the Obihai platform in YAML is deprecated and will be removed in Home Assistant 2023.6; Your existing configuration has been imported into the UI automatically and can be safely removed from your configuration.yaml file."
}
}
}

View File

@@ -3,7 +3,6 @@ import asyncio
from datetime import date, datetime
import logging
import async_timeout
import pyotgw
import pyotgw.vars as gw_vars
from serial import SerialException
@@ -113,8 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
config_entry.add_update_listener(options_updated)
try:
async with async_timeout.timeout(CONNECTION_TIMEOUT):
await gateway.connect_and_subscribe()
await asyncio.wait_for(
gateway.connect_and_subscribe(),
timeout=CONNECTION_TIMEOUT,
)
except (asyncio.TimeoutError, ConnectionError, SerialException) as ex:
await gateway.cleanup()
raise ConfigEntryNotReady(

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import async_timeout
import pyotgw
from pyotgw import vars as gw_vars
from serial import SerialException
@@ -69,8 +68,10 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
try:
async with async_timeout.timeout(CONNECTION_TIMEOUT):
await test_connection()
await asyncio.wait_for(
test_connection(),
timeout=CONNECTION_TIMEOUT,
)
except asyncio.TimeoutError:
return self._show_form({"base": "timeout_connect"})
except (ConnectionError, SerialException):

View File

@@ -46,23 +46,11 @@ class OTBRData:
url: str
api: python_otbr_api.OTBR
@_handle_otbr_error
async def set_enabled(self, enabled: bool) -> None:
"""Enable or disable the router."""
return await self.api.set_enabled(enabled)
@_handle_otbr_error
async def get_active_dataset_tlvs(self) -> bytes | None:
"""Get current active operational dataset in TLVS format, or None."""
return await self.api.get_active_dataset_tlvs()
@_handle_otbr_error
async def create_active_dataset(
self, dataset: python_otbr_api.OperationalDataSet
) -> None:
"""Create an active operational dataset."""
return await self.api.create_active_dataset(dataset)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Open Thread Border Router component."""

View File

@@ -1,8 +1,6 @@
"""Websocket API for OTBR."""
from typing import TYPE_CHECKING
import python_otbr_api
from homeassistant.components.websocket_api import (
ActiveConnection,
async_register_command,
@@ -22,7 +20,6 @@ if TYPE_CHECKING:
def async_setup(hass: HomeAssistant) -> None:
"""Set up the OTBR Websocket API."""
async_register_command(hass, websocket_info)
async_register_command(hass, websocket_create_network)
@websocket_command(
@@ -54,42 +51,3 @@ async def websocket_info(
"active_dataset_tlvs": dataset.hex() if dataset else None,
},
)
@websocket_command(
{
"type": "otbr/create_network",
}
)
@async_response
async def websocket_create_network(
hass: HomeAssistant, connection: ActiveConnection, msg: dict
) -> None:
"""Create a new Thread network."""
if DOMAIN not in hass.data:
connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
return
data: OTBRData = hass.data[DOMAIN]
try:
await data.set_enabled(False)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
try:
await data.create_active_dataset(
python_otbr_api.OperationalDataSet(network_name="home-assistant")
)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "create_active_dataset_failed", str(exc))
return
try:
await data.set_enabled(True)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
connection.send_result(msg["id"])

View File

@@ -10,7 +10,6 @@ from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl
from .somfy_thermostat import SomfyThermostat
from .valve_heating_temperature_interface import ValveHeatingTemperatureInterface
WIDGET_TO_CLIMATE_ENTITY = {
UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater,
@@ -22,5 +21,4 @@ WIDGET_TO_CLIMATE_ENTITY = {
UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: AtlanticPassAPCHeatingZone,
UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl,
UIWidget.SOMFY_THERMOSTAT: SomfyThermostat,
UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: ValveHeatingTemperatureInterface,
}

View File

@@ -15,7 +15,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import UnitOfTemperature
from ..const import DOMAIN
from ..entity import OverkizEntity
PRESET_COMFORT1 = "comfort-1"
@@ -48,7 +47,6 @@ class AtlanticElectricalHeater(OverkizEntity, ClimateEntity):
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
_attr_supported_features = ClimateEntityFeature.PRESET_MODE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
@property
def hvac_mode(self) -> HVACMode:

View File

@@ -16,7 +16,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -71,7 +70,6 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator

View File

@@ -14,7 +14,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -44,7 +43,6 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity):
_attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ]
_attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator

View File

@@ -13,7 +13,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -50,7 +49,6 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity):
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.FAN_MODE
)
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator

View File

@@ -17,7 +17,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -79,7 +78,6 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator

View File

@@ -15,17 +15,19 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
PRESET_FREEZE = "freeze"
PRESET_NIGHT = "night"
STATE_DEROGATION_ACTIVE = "active"
STATE_DEROGATION_INACTIVE = "inactive"
OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = {
OverkizCommandParam.ACTIVE: HVACMode.HEAT,
OverkizCommandParam.INACTIVE: HVACMode.AUTO,
STATE_DEROGATION_ACTIVE: HVACMode.HEAT,
STATE_DEROGATION_INACTIVE: HVACMode.AUTO,
}
HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()}
@@ -58,8 +60,6 @@ class SomfyThermostat(OverkizEntity, ClimateEntity):
)
_attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ]
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
_attr_translation_key = DOMAIN
# Both min and max temp values have been retrieved from the Somfy Application.
_attr_min_temp = 15.0
_attr_max_temp = 26.0

View File

@@ -1,137 +0,0 @@
"""Support for ValveHeatingTemperatureInterface."""
from __future__ import annotations
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
UnitOfTemperature,
)
from homeassistant.const import ATTR_TEMPERATURE
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
PRESET_MANUAL = "manual"
PRESET_FROST_PROTECTION = "frost_protection"
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
OverkizCommandParam.OPEN: HVACAction.HEATING,
OverkizCommandParam.CLOSED: HVACAction.IDLE,
}
OVERKIZ_TO_PRESET_MODE: dict[str, str] = {
OverkizCommandParam.GEOFENCING_MODE: PRESET_NONE,
OverkizCommandParam.SUDDEN_DROP_MODE: PRESET_NONE,
OverkizCommandParam.AWAY: PRESET_AWAY,
OverkizCommandParam.COMFORT: PRESET_COMFORT,
OverkizCommandParam.ECO: PRESET_ECO,
OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION,
OverkizCommandParam.MANUAL: PRESET_MANUAL,
}
PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()}
TEMPERATURE_SENSOR_DEVICE_INDEX = 2
class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
"""Representation of Valve Heating Temperature Interface device."""
_attr_hvac_mode = HVACMode.HEAT
_attr_hvac_modes = [HVACMode.HEAT]
_attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ]
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
) -> None:
"""Init method."""
super().__init__(device_url, coordinator)
self.temperature_device = self.executor.linked_device(
TEMPERATURE_SENSOR_DEVICE_INDEX
)
self._attr_min_temp = cast(
float, self.executor.select_state(OverkizState.CORE_MIN_SETPOINT)
)
self._attr_max_temp = cast(
float, self.executor.select_state(OverkizState.CORE_MAX_SETPOINT)
)
@property
def hvac_action(self) -> str:
"""Return the current running hvac operation."""
return OVERKIZ_TO_HVAC_ACTION[
cast(str, self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE))
]
@property
def target_temperature(self) -> float:
"""Return the temperature."""
return cast(
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]:
return temperature.value_as_float
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new temperature."""
temperature = kwargs[ATTR_TEMPERATURE]
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
float(temperature),
OverkizCommandParam.FURTHER_NOTICE,
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
return
@property
def preset_mode(self) -> str:
"""Return the current preset mode, e.g., home, away, temp."""
return OVERKIZ_TO_PRESET_MODE[
cast(
str, self.executor.select_state(OverkizState.IO_DEROGATION_HEATING_MODE)
)
]
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
# If we want to switch to manual mode via a preset, we need to pass in a temperature
# Manual mode will be on automatically if an user sets a temperature
if preset_mode == PRESET_MANUAL:
if current_temperature := self.current_temperature:
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
current_temperature,
OverkizCommandParam.FURTHER_NOTICE,
)
else:
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
PRESET_MODE_TO_OVERKIZ[preset_mode],
OverkizCommandParam.FURTHER_NOTICE,
)

View File

@@ -83,7 +83,6 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = {
UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported)
UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported)
UIWidget.TSKALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported)
UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
}
# Map Overkiz camelCase to Home Assistant snake_case for translation

View File

@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.7.6"],
"requirements": ["pyoverkiz==1.7.3"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",

View File

@@ -28,34 +28,6 @@
}
},
"entity": {
"climate": {
"overkiz": {
"state_attributes": {
"preset_mode": {
"state": {
"auto": "Auto",
"comfort-1": "Comfort 1",
"comfort-2": "Comfort 2",
"drying": "Drying",
"external": "External",
"freeze": "Freeze",
"frost_protection": "Frost protection",
"manual": "Manual",
"night": "Night",
"prog": "Prog"
}
},
"fan_mode": {
"state": {
"away": "Away",
"bypass_boost": "Bypass boost",
"home_boost": "Home boost",
"kitchen_boost": "Kitchen boost"
}
}
}
}
},
"select": {
"open_closed_pedestrian": {
"state": {

View File

@@ -8,7 +8,6 @@ import logging
import re
from typing import Any
import async_timeout
from icmplib import NameLookupError, async_ping
import voluptuous as vol
@@ -231,8 +230,9 @@ class PingDataSubProcess(PingData):
close_fds=False, # required for posix_spawn
)
try:
async with async_timeout.timeout(self._count + PING_TIMEOUT):
out_data, out_error = await pinger.communicate()
out_data, out_error = await asyncio.wait_for(
pinger.communicate(), self._count + PING_TIMEOUT
)
if out_data:
_LOGGER.debug(

View File

@@ -17,7 +17,6 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
@@ -304,9 +303,9 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="gas_consumed_interval",
name="Gas consumed interval",
icon="mdi:meter-gas",
native_unit_of_measurement=f"{UnitOfVolume.CUBIC_METERS}/{UnitOfTime.HOURS}",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="gas_consumed_cumulative",

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client
from .const import CONF_COUNTRY, DOMAIN
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
PLATFORMS = [Platform.ALARM_CONTROL_PANEL]
_LOGGER = logging.getLogger(__name__)

View File

@@ -15,7 +15,6 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
@@ -60,14 +59,6 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity):
self._attr_name = f"contract {self.contract}"
self._attr_unique_id = self.contract
self._attr_device_info = DeviceInfo(
name="Prosegur Alarm",
manufacturer="Prosegur",
model="smart",
identifiers={(DOMAIN, self.contract)},
configuration_url="https://smart.prosegur.com",
)
async def async_update(self) -> None:
"""Update alarm status."""

View File

@@ -1,97 +0,0 @@
"""Support for Prosegur cameras."""
from __future__ import annotations
import logging
from pyprosegur.auth import Auth
from pyprosegur.exceptions import ProsegurException
from pyprosegur.installation import Camera as InstallationCamera, Installation
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from . import DOMAIN
from .const import SERVICE_REQUEST_IMAGE
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Prosegur camera platform."""
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_REQUEST_IMAGE,
{},
"async_request_image",
)
_installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id])
async_add_entities(
[
ProsegurCamera(_installation, camera, hass.data[DOMAIN][entry.entry_id])
for camera in _installation.cameras
],
update_before_add=True,
)
class ProsegurCamera(Camera):
"""Representation of a Smart Prosegur Camera."""
def __init__(
self, installation: Installation, camera: InstallationCamera, auth: Auth
) -> None:
"""Initialize Prosegur Camera component."""
Camera.__init__(self)
self._installation = installation
self._camera = camera
self._auth = auth
self._attr_name = camera.description
self._attr_unique_id = f"{self._installation.contract} {camera.id}"
self._attr_device_info = DeviceInfo(
name=self._camera.description,
manufacturer="Prosegur",
model="smart camera",
identifiers={(DOMAIN, self._installation.contract)},
configuration_url="https://smart.prosegur.com",
)
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
try:
_LOGGER.debug("Get image for %s", self._camera.description)
return await self._installation.get_image(self._auth, self._camera.id)
except ProsegurException as err:
_LOGGER.error("Image %s doesn't exist: %s", self._camera.description, err)
return None
async def async_request_image(self):
"""Request new image from the camera."""
try:
_LOGGER.debug("Request image for %s", self._camera.description)
await self._installation.request_image(self._auth, self._camera.id)
except ProsegurException as err:
_LOGGER.error(
"Could not request image from camera %s: %s",
self._camera.description,
err,
)

View File

@@ -3,5 +3,3 @@
DOMAIN = "prosegur"
CONF_COUNTRY = "country"
SERVICE_REQUEST_IMAGE = "request_image"

View File

@@ -1,29 +0,0 @@
"""Diagnostics support for Prosegur."""
from __future__ import annotations
from typing import Any
from pyprosegur.installation import Installation
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
TO_REDACT = {"description", "latitude", "longitude", "contractId", "address"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id])
activity = await installation.activity(hass.data[DOMAIN][entry.entry_id])
return {
"installation": async_redact_data(installation.data, TO_REDACT),
"activity": activity,
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/prosegur",
"iot_class": "cloud_polling",
"loggers": ["pyprosegur"],
"requirements": ["pyprosegur==0.0.8"]
"requirements": ["pyprosegur==0.0.5"]
}

View File

@@ -1,7 +0,0 @@
request_image:
name: Request Camera image
description: Request a new image from a Prosegur Camera
target:
entity:
domain: camera
integration: prosegur

View File

@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONF_NAME,
@@ -46,14 +45,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
name="Down Speed",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_TYPE_UPLOAD_SPEED,
name="Up Speed",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
)

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling",
"loggers": ["aioqsw"],
"requirements": ["aioqsw==0.3.2"]
"requirements": ["aioqsw==0.3.1"]
}

View File

@@ -106,10 +106,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
)
# Fetch initial data so we have data when entities subscribe
try:
# If camera WAN blocked, firmware check fails, do not prevent setup
await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_refresh(),
firmware_coordinator.async_config_entry_first_refresh(),
)
except ConfigEntryNotReady:
await host.stop()

View File

@@ -49,7 +49,7 @@ class ReolinkUpdateEntity(ReolinkBaseCoordinatorEntity, UpdateEntity):
"""Initialize a Netgear device."""
super().__init__(reolink_data, reolink_data.firmware_coordinator)
self._attr_unique_id = f"{self._host.unique_id}"
self._attr_unique_id = f"{self._host.unique_id}_update"
@property
def installed_version(self) -> str | None:

View File

@@ -6,7 +6,6 @@ from collections.abc import Coroutine, Sequence
from datetime import datetime, timedelta
from typing import Any
import async_timeout
from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester
from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable
from async_upnp_client.client_factory import UpnpFactory
@@ -251,8 +250,7 @@ class SamsungTVDevice(MediaPlayerEntity):
# enter it unless we have to (Python 3.11 will have zero cost try)
return
try:
async with async_timeout.timeout(APP_LIST_DELAY):
await self._app_list_event.wait()
await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY)
except asyncio.TimeoutError as err:
# No need to try again
self._app_list_event.set()

View File

@@ -196,30 +196,19 @@ class SensorEntity(Entity):
if self.unique_id is None or self.device_class is None:
return
registry = er.async_get(self.hass)
# Bail out if the entity is not yet registered
if not (
entity_id := registry.async_get_entity_id(
platform.domain, platform.platform_name, self.unique_id
)
):
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._sensor_option_unit_of_measurement = self._get_initial_suggested_unit()
return
registry_entry = registry.async_get(entity_id)
assert registry_entry
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self.registry_entry = registry_entry
self._async_read_entity_options()
# If the sensor has 'unit_of_measurement' in its sensor options, the user has
# overridden the unit.
# If the sensor has 'sensor.private' in its entity options, it already has a
# suggested_unit.
# If the sensor has 'sensor.private' in its entity options, it was added after
# automatic unit conversion was implemented.
registry_unit = registry_entry.unit_of_measurement
if (
(
@@ -241,14 +230,11 @@ class SensorEntity(Entity):
# Set suggested_unit_of_measurement to the old unit to enable automatic
# conversion
self.registry_entry = registry.async_update_entity_options(
registry.async_update_entity_options(
entity_id,
f"{DOMAIN}.private",
{"suggested_unit_of_measurement": registry_unit},
)
# Update _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._async_read_entity_options()
async def async_internal_added_to_hass(self) -> None:
"""Call when the sensor entity is added to hass."""
@@ -319,8 +305,12 @@ class SensorEntity(Entity):
return None
def _get_initial_suggested_unit(self) -> str | UndefinedType:
"""Return the initial unit."""
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
# Unit suggested by the integration
suggested_unit_of_measurement = self.suggested_unit_of_measurement
@@ -331,19 +321,6 @@ class SensorEntity(Entity):
)
if suggested_unit_of_measurement is None:
return UNDEFINED
return suggested_unit_of_measurement
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
suggested_unit_of_measurement = self._get_initial_suggested_unit()
if suggested_unit_of_measurement is UNDEFINED:
return None
return {
@@ -439,7 +416,7 @@ class SensorEntity(Entity):
return self._sensor_option_unit_of_measurement
# Second priority, for non registered entities: unit suggested by integration
if not self.unique_id and self.suggested_unit_of_measurement:
if not self.registry_entry and self.suggested_unit_of_measurement:
return self.suggested_unit_of_measurement
# Third priority: Legacy temperature conversion, which applies

View File

@@ -588,8 +588,8 @@ def _compile_statistics( # noqa: C901
),
entity_id,
new_state,
fstate,
state.last_updated.isoformat(),
fstate,
)
except HomeAssistantError:
continue

View File

@@ -4,7 +4,6 @@ from http import HTTPStatus
import logging
from typing import TYPE_CHECKING
import async_timeout
from pysqueezebox import Server, async_discover
import voluptuous as vol
@@ -131,8 +130,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# no host specified, see if we can discover an unconfigured LMS server
try:
async with async_timeout.timeout(TIMEOUT):
await self._discover()
await asyncio.wait_for(self._discover(), timeout=TIMEOUT)
return await self.async_step_edit()
except asyncio.TimeoutError:
errors["base"] = "no_server_found"

View File

@@ -2,7 +2,7 @@
"domain": "statistics",
"name": "Statistics",
"after_dependencies": ["recorder"],
"codeowners": ["@ThomDietrich"],
"codeowners": ["@fabaff", "@ThomDietrich"],
"documentation": "https://www.home-assistant.io/integrations/statistics",
"iot_class": "local_polling",
"quality_scale": "internal"

View File

@@ -13,23 +13,16 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_import(
self, import_data: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Set up because the user has border routers."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_import(
self, import_data: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})

Some files were not shown because too many files have changed in this diff Show More