mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 06:07:56 +00:00
Compare commits
99 Commits
2023.3.0b4
...
frontend-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
651312fb42 | ||
|
|
7b3cab1bfe | ||
|
|
c096ef3fce | ||
|
|
9fed4472f1 | ||
|
|
7a5a882687 | ||
|
|
73c7ee4326 | ||
|
|
79f96fe900 | ||
|
|
7cc8712a0c | ||
|
|
0e8d28dab0 | ||
|
|
fd87748b99 | ||
|
|
00954dfc1f | ||
|
|
e95944bf9f | ||
|
|
ac70612ec5 | ||
|
|
7419a92a1b | ||
|
|
ff4de8cd06 | ||
|
|
bdb9994b7e | ||
|
|
2dcc2f88cc | ||
|
|
db1dd16ab0 | ||
|
|
2c2489284b | ||
|
|
198ebaff6e | ||
|
|
5cc9e7fedd | ||
|
|
76819fbb23 | ||
|
|
aeb6c4f078 | ||
|
|
b25f6e3ffc | ||
|
|
b542f6b3ac | ||
|
|
a8d587bc53 | ||
|
|
fe8f3602ff | ||
|
|
735000475a | ||
|
|
ae3e8746f7 | ||
|
|
10bf910f88 | ||
|
|
b7846de311 | ||
|
|
66b33e1090 | ||
|
|
4fd7ca503f | ||
|
|
33466cdddd | ||
|
|
0d25eef19c | ||
|
|
b5223e1196 | ||
|
|
1d1c553d9b | ||
|
|
f8934175cb | ||
|
|
4898d22960 | ||
|
|
480a495239 | ||
|
|
d219e7c8b1 | ||
|
|
c8fc2dc440 | ||
|
|
9be3f86a4c | ||
|
|
bea81d3f63 | ||
|
|
0f01866508 | ||
|
|
588b51bdfa | ||
|
|
0fb41bdffe | ||
|
|
c9dfa15ed6 | ||
|
|
e00ff54869 | ||
|
|
7c23de469e | ||
|
|
490a0908d4 | ||
|
|
327edabb64 | ||
|
|
b4a3a663cf | ||
|
|
1519a78567 | ||
|
|
57360a7528 | ||
|
|
7b61d3763b | ||
|
|
0f204d6502 | ||
|
|
0a3a8c4b3c | ||
|
|
091305fc57 | ||
|
|
3499d60401 | ||
|
|
f18c0bf626 | ||
|
|
f52a5f6965 | ||
|
|
1edef73c9a | ||
|
|
5a365788b5 | ||
|
|
a60fd18386 | ||
|
|
0223058d25 | ||
|
|
7b2e743a6b | ||
|
|
69a3738bdb | ||
|
|
e69091c6db | ||
|
|
ee7dfdae30 | ||
|
|
fdc06c2fc2 | ||
|
|
ba929dfc79 | ||
|
|
753c790a25 | ||
|
|
ee8f746808 | ||
|
|
84823d2fcf | ||
|
|
0ae2fdc08b | ||
|
|
d90ee85118 | ||
|
|
2f826a6f86 | ||
|
|
af49b98475 | ||
|
|
9575cd9161 | ||
|
|
f0b029c363 | ||
|
|
a71487a42b | ||
|
|
d5f1713498 | ||
|
|
301144993c | ||
|
|
e0601530a0 | ||
|
|
e1e0400b16 | ||
|
|
5739782877 | ||
|
|
6112793b19 | ||
|
|
f8314fe007 | ||
|
|
dac3c7179f | ||
|
|
6511b3f355 | ||
|
|
6474297d1f | ||
|
|
27ebee1501 | ||
|
|
23b52025f9 | ||
|
|
87dc692a20 | ||
|
|
473db48943 | ||
|
|
aa3657e071 | ||
|
|
2a819f23c1 | ||
|
|
c6ff79aa0e |
@@ -639,6 +639,10 @@ 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
|
||||
@@ -803,7 +807,8 @@ omit =
|
||||
homeassistant/components/nuki/sensor.py
|
||||
homeassistant/components/nx584/alarm_control_panel.py
|
||||
homeassistant/components/oasa_telematics/sensor.py
|
||||
homeassistant/components/obihai/*
|
||||
homeassistant/components/obihai/connectivity.py
|
||||
homeassistant/components/obihai/sensor.py
|
||||
homeassistant/components/octoprint/__init__.py
|
||||
homeassistant/components/oem/climate.py
|
||||
homeassistant/components/ohmconnect/sensor.py
|
||||
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -31,7 +31,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 4
|
||||
HA_SHORT_VERSION: 2023.3
|
||||
HA_SHORT_VERSION: 2023.4
|
||||
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.1.0
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.3.0
|
||||
uses: actions/setup-python@v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
@@ -186,6 +186,7 @@ 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.*
|
||||
|
||||
@@ -825,7 +825,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/nws/ @MatthewFlamm @kamiyo
|
||||
/homeassistant/components/nzbget/ @chriscla
|
||||
/tests/components/nzbget/ @chriscla
|
||||
/homeassistant/components/obihai/ @dshokouhi
|
||||
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
||||
/tests/components/obihai/ @dshokouhi @ejpenney
|
||||
/homeassistant/components/octoprint/ @rfleming71
|
||||
/tests/components/octoprint/ @rfleming71
|
||||
/homeassistant/components/ohmconnect/ @robbiet480
|
||||
@@ -1138,8 +1139,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/starlink/ @boswelja
|
||||
/tests/components/starlink/ @boswelja
|
||||
/homeassistant/components/statistics/ @fabaff @ThomDietrich
|
||||
/tests/components/statistics/ @fabaff @ThomDietrich
|
||||
/homeassistant/components/statistics/ @ThomDietrich
|
||||
/tests/components/statistics/ @ThomDietrich
|
||||
/homeassistant/components/steam_online/ @tkdrob
|
||||
/tests/components/steam_online/ @tkdrob
|
||||
/homeassistant/components/steamist/ @bdraco
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from AIOAladdinConnect import AladdinConnectClient
|
||||
@@ -20,8 +19,6 @@ 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,
|
||||
@@ -134,12 +131,6 @@ 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."""
|
||||
|
||||
@@ -2,63 +2,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
from typing import Any
|
||||
|
||||
from AIOAladdinConnect import AladdinConnectClient
|
||||
import voluptuous as vol
|
||||
|
||||
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.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import 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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@@ -15,6 +16,7 @@ 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
|
||||
@@ -162,9 +164,10 @@ async def async_send_changereport_message(
|
||||
if response.status == HTTPStatus.ACCEPTED:
|
||||
return
|
||||
|
||||
response_json = json.loads(response_text)
|
||||
response_json = json_loads_object(response_text)
|
||||
response_payload = cast(JsonObjectType, response_json["payload"])
|
||||
|
||||
if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
|
||||
if response_payload["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
|
||||
if invalidate_access_token:
|
||||
# Invalidate the access token and try again
|
||||
config.async_invalidate_access_token()
|
||||
@@ -180,8 +183,8 @@ async def async_send_changereport_message(
|
||||
_LOGGER.error(
|
||||
"Error when sending ChangeReport for %s to Alexa: %s: %s",
|
||||
alexa_entity.entity_id,
|
||||
response_json["payload"]["code"],
|
||||
response_json["payload"]["description"],
|
||||
response_payload["code"],
|
||||
response_payload["description"],
|
||||
)
|
||||
|
||||
|
||||
@@ -299,11 +302,12 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
if response.status == HTTPStatus.ACCEPTED:
|
||||
return
|
||||
|
||||
response_json = json.loads(response_text)
|
||||
response_json = json_loads_object(response_text)
|
||||
response_payload = cast(JsonObjectType, response_json["payload"])
|
||||
|
||||
_LOGGER.error(
|
||||
"Error when sending DoorbellPress event for %s to Alexa: %s: %s",
|
||||
alexa_entity.entity_id,
|
||||
response_json["payload"]["code"],
|
||||
response_json["payload"]["description"],
|
||||
response_payload["code"],
|
||||
response_payload["description"],
|
||||
)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apprise",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["apprise"],
|
||||
"requirements": ["apprise==1.2.1"]
|
||||
"requirements": ["apprise==1.3.0"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==2.5.2"]
|
||||
"requirements": ["bthome-ble==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -119,6 +119,16 @@ 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}",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.1.1"]
|
||||
"requirements": ["caldav==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
DOMAIN = "conversation"
|
||||
|
||||
DEFAULT_EXPOSED_DOMAINS = {
|
||||
"binary_sensor",
|
||||
"climate",
|
||||
"cover",
|
||||
"fan",
|
||||
@@ -16,3 +17,5 @@ DEFAULT_EXPOSED_DOMAINS = {
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
}
|
||||
|
||||
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
|
||||
|
||||
@@ -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_DOMAINS, DOMAIN
|
||||
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
|
||||
@@ -479,6 +479,12 @@ 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:
|
||||
@@ -518,6 +524,9 @@ 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),
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
@@ -17,24 +18,13 @@ if TYPE_CHECKING:
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
|
||||
class DeviceAutomationConditionProtocol(Protocol):
|
||||
class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
|
||||
"""Define the format of device_condition modules.
|
||||
|
||||
Each module must define either CONDITION_SCHEMA or async_validate_condition_config.
|
||||
Each module must define either CONDITION_SCHEMA or async_validate_condition_config
|
||||
from ConditionProtocol.
|
||||
"""
|
||||
|
||||
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]:
|
||||
@@ -62,4 +52,4 @@ async def async_condition_from_config(
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
return platform.async_condition_from_config(hass, config)
|
||||
return trace_condition_function(platform.async_condition_from_config(hass, config))
|
||||
|
||||
@@ -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.28"]
|
||||
"requirements": ["env_canada==0.5.29"]
|
||||
}
|
||||
|
||||
@@ -341,6 +341,11 @@ 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),
|
||||
|
||||
60
homeassistant/components/frontend/dev.py
Normal file
60
homeassistant/components/frontend/dev.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""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(),
|
||||
)
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20230224.0"]
|
||||
"requirements": ["home-assistant-frontend==20230227.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==5.0.1"]
|
||||
"requirements": ["odp-amsterdam==5.1.0"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow for HLK-SW16."""
|
||||
import asyncio
|
||||
|
||||
import async_timeout
|
||||
from hlk_sw16 import create_hlk_sw16_connection
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -35,7 +36,8 @@ async def connect_client(hass, user_input):
|
||||
reconnect_interval=DEFAULT_RECONNECT_INTERVAL,
|
||||
keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL,
|
||||
)
|
||||
return await asyncio.wait_for(client_aw, timeout=CONNECTION_TIMEOUT)
|
||||
async with async_timeout.timeout(CONNECTION_TIMEOUT):
|
||||
return await client_aw
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, user_input):
|
||||
|
||||
@@ -14,6 +14,7 @@ PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WEATHER,
|
||||
|
||||
39
homeassistant/components/homematicip_cloud/helpers.py
Normal file
39
homeassistant/components/homematicip_cloud/helpers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""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
|
||||
95
homeassistant/components/homematicip_cloud/lock.py
Normal file
95
homeassistant/components/homematicip_cloud/lock.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""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
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/honeywell",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["somecomfort"],
|
||||
"requirements": ["aiosomecomfort==0.0.8"]
|
||||
"requirements": ["aiosomecomfort==0.0.10"]
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
"""The islamic_prayer_times component."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from prayer_times_calculator import PrayerTimesCalculator, exceptions
|
||||
from requests.exceptions import ConnectionError as ConnError
|
||||
from __future__ import annotations
|
||||
|
||||
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 CONF_CALC_METHOD, DATA_UPDATED, DEFAULT_CALC_METHOD, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN
|
||||
from .coordinator import IslamicPrayerDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -25,154 +16,32 @@ 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."""
|
||||
client = IslamicPrayerClient(hass, config_entry)
|
||||
hass.data[DOMAIN] = client
|
||||
await client.async_setup()
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload Islamic Prayer entry from config_entry."""
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
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()
|
||||
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()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""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
|
||||
|
||||
@@ -22,7 +25,9 @@ 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=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
@@ -40,7 +45,9 @@ class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
"""Constants for the Islamic Prayer component."""
|
||||
from typing import Final
|
||||
|
||||
from prayer_times_calculator import PrayerTimesCalculator
|
||||
|
||||
DOMAIN = "islamic_prayer_times"
|
||||
NAME = "Islamic Prayer Times"
|
||||
PRAYER_TIMES_ICON = "mdi:calendar-clock"
|
||||
DOMAIN: Final = "islamic_prayer_times"
|
||||
NAME: Final = "Islamic Prayer Times"
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"Fajr": "prayer",
|
||||
"Sunrise": "time",
|
||||
"Dhuhr": "prayer",
|
||||
"Asr": "prayer",
|
||||
"Maghrib": "prayer",
|
||||
"Isha": "prayer",
|
||||
"Midnight": "time",
|
||||
}
|
||||
|
||||
CONF_CALC_METHOD = "calculation_method"
|
||||
CONF_CALC_METHOD: Final = "calculation_method"
|
||||
|
||||
CALC_METHODS: list[str] = list(PrayerTimesCalculator.CALCULATION_METHODS)
|
||||
DEFAULT_CALC_METHOD = "isna"
|
||||
|
||||
DATA_UPDATED = "Islamic_prayer_data_updated"
|
||||
DEFAULT_CALC_METHOD: Final = "isna"
|
||||
|
||||
121
homeassistant/components/islamic_prayer_times/coordinator.py
Normal file
121
homeassistant/components/islamic_prayer_times/coordinator.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""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
|
||||
@@ -1,12 +1,51 @@
|
||||
"""Platform to retrieve Islamic prayer times information for Home Assistant."""
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES
|
||||
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",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -16,46 +55,38 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Islamic prayer times sensor platform."""
|
||||
|
||||
client = hass.data[DOMAIN]
|
||||
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN]
|
||||
|
||||
entities = []
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
entities.append(IslamicPrayerTimeSensor(sensor_type, client))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
async_add_entities(
|
||||
IslamicPrayerTimeSensor(coordinator, description)
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class IslamicPrayerTimeSensor(SensorEntity):
|
||||
class IslamicPrayerTimeSensor(
|
||||
CoordinatorEntity[IslamicPrayerDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Representation of an Islamic prayer time sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_icon = PRAYER_TIMES_ICON
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, sensor_type, client):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IslamicPrayerDataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Islamic prayer time sensor."""
|
||||
self.sensor_type = sensor_type
|
||||
self.client = client
|
||||
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,
|
||||
)
|
||||
|
||||
@property
|
||||
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):
|
||||
def native_value(self) -> datetime:
|
||||
"""Return the state of the sensor."""
|
||||
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)
|
||||
)
|
||||
return self.coordinator.data[self.entity_description.key]
|
||||
|
||||
@@ -8,16 +8,43 @@ 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
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import 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:
|
||||
@@ -32,6 +59,7 @@ async def async_setup_entry(
|
||||
entities.append(ISYLockProgramEntity(name, status, actions))
|
||||
|
||||
async_add_entities(entities)
|
||||
async_setup_lock_services(hass)
|
||||
|
||||
|
||||
class ISYLockEntity(ISYNodeEntity, LockEntity):
|
||||
@@ -47,12 +75,26 @@ 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():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Send the unlock command to the ISY device."""
|
||||
if not await self._node.secure_unlock():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
|
||||
@@ -66,9 +108,9 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the device."""
|
||||
if not await self._actions.run_then():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the device."""
|
||||
if not await self._actions.run_else():
|
||||
_LOGGER.error("Unable to unlock device")
|
||||
raise HomeAssistantError(f"Unable to unlock device {self._node.address}")
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.1.13"],
|
||||
"requirements": ["pyisy==3.1.14"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
||||
@@ -52,8 +52,14 @@ 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"
|
||||
@@ -129,6 +135,13 @@ 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(
|
||||
|
||||
@@ -118,6 +118,52 @@ 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: >-
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["xknx"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["xknx==2.5.0"]
|
||||
"requirements": ["xknx==2.6.0"]
|
||||
}
|
||||
|
||||
@@ -17,10 +17,9 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, 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
|
||||
@@ -167,15 +166,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
We do not want the discovery task to block startup.
|
||||
"""
|
||||
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)
|
||||
hass.async_create_background_task(
|
||||
discovery_manager.async_discovery(), "lifx-discovery"
|
||||
)
|
||||
|
||||
# Let the system settle a bit before starting discovery
|
||||
# to reduce the risk we miss devices because the event
|
||||
|
||||
@@ -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 HomeAssistant
|
||||
from homeassistant.core import Event, 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) -> None:
|
||||
async def handle_stop(event: Event) -> None:
|
||||
await system.close()
|
||||
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -76,7 +76,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult:
|
||||
"""Import litejet config from configuration.yaml."""
|
||||
return self.async_create_entry(title=import_data[CONF_PORT], data=import_data)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
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
|
||||
@@ -42,7 +44,7 @@ async def async_attach_trigger(
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Listen for events based on configuration."""
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
number = config.get(CONF_NUMBER)
|
||||
number = cast(int, config[CONF_NUMBER])
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
held_less_than = config.get(CONF_HELD_LESS_THAN)
|
||||
pressed_time = None
|
||||
@@ -50,7 +52,7 @@ async def async_attach_trigger(
|
||||
job = HassJob(action)
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
def call_action() -> None:
|
||||
"""Call action with right context."""
|
||||
hass.async_run_hass_job(
|
||||
job,
|
||||
@@ -72,11 +74,11 @@ async def async_attach_trigger(
|
||||
# neither: trigger on pressed
|
||||
|
||||
@callback
|
||||
def pressed_more_than_satisfied(now):
|
||||
def pressed_more_than_satisfied(now: datetime) -> None:
|
||||
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
|
||||
call_action()
|
||||
|
||||
def pressed():
|
||||
def pressed() -> None:
|
||||
"""Handle the press of the LiteJet switch's button."""
|
||||
nonlocal cancel_pressed_more_than, pressed_time
|
||||
nonlocal held_less_than, held_more_than
|
||||
@@ -88,10 +90,12 @@ async def async_attach_trigger(
|
||||
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
|
||||
)
|
||||
|
||||
def released():
|
||||
def released() -> None:
|
||||
"""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
|
||||
@@ -110,7 +114,7 @@ async def async_attach_trigger(
|
||||
system.on_switch_released(number, released)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
def async_remove() -> None:
|
||||
"""Remove all subscriptions used for this trigger."""
|
||||
system.unsubscribe(pressed)
|
||||
system.unsubscribe(released)
|
||||
|
||||
@@ -8,14 +8,15 @@ 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, SWITCH_PLATFORM
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LivisiDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: Final = [SWITCH_PLATFORM]
|
||||
PLATFORMS: Final = [Platform.CLIMATE, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
212
homeassistant/components/livisi/climate.py
Normal file
212
homeassistant/components/livisi/climate.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""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()
|
||||
@@ -7,12 +7,15 @@ 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"
|
||||
|
||||
SWITCH_PLATFORM: Final = "switch"
|
||||
|
||||
PSS_DEVICE_TYPE: Final = "PSS"
|
||||
VRCC_DEVICE_TYPE: Final = "VRCC"
|
||||
|
||||
MAX_TEMPERATURE: Final = 30.0
|
||||
MIN_TEMPERATURE: Final = 6.0
|
||||
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
@@ -69,14 +70,14 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
livisi_connection_data=livisi_connection_data
|
||||
)
|
||||
controller_data = await self.aiolivisi.async_get_controller()
|
||||
if controller_data["controllerType"] == "Avatar":
|
||||
if (controller_type := 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."""
|
||||
@@ -84,7 +85,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] = await self.aiolivisi.async_get_device_state(
|
||||
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
|
||||
capability[1:]
|
||||
)
|
||||
if response is None:
|
||||
@@ -92,6 +93,35 @@ 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()
|
||||
@@ -108,6 +138,12 @@ 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,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/livisi",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiolivisi==0.0.15"]
|
||||
"requirements": ["aiolivisi==0.0.16"]
|
||||
}
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
"""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)
|
||||
|
||||
73
homeassistant/components/obihai/config_flow.py
Normal file
73
homeassistant/components/obihai/config_flow.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""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],
|
||||
},
|
||||
)
|
||||
67
homeassistant/components/obihai/connectivity.py
Normal file
67
homeassistant/components/obihai/connectivity.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""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
|
||||
15
homeassistant/components/obihai/const.py
Normal file
15
homeassistant/components/obihai/const.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""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]
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"domain": "obihai",
|
||||
"name": "Obihai",
|
||||
"codeowners": ["@dshokouhi"],
|
||||
"codeowners": ["@dshokouhi", "@ejpenney"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/obihai",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyobihai"],
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyobihai import PyObihai
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -12,20 +10,19 @@ 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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .connectivity import ObihaiConnection
|
||||
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, DOMAIN, OBIHAI
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
OBIHAI = "Obihai"
|
||||
DEFAULT_USERNAME = "admin"
|
||||
DEFAULT_PASSWORD = "admin"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -35,46 +32,58 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
# DEPRECATED
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
async_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",
|
||||
)
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
host = config[CONF_HOST]
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
pyobihai = PyObihai(host, username, password)
|
||||
if requester.line_services is not None:
|
||||
for key in requester.line_services:
|
||||
sensors.append(
|
||||
ObihaiServiceSensors(requester.pyobihai, requester.serial, key)
|
||||
)
|
||||
|
||||
login = pyobihai.check_account()
|
||||
if not login:
|
||||
_LOGGER.error("Invalid credentials")
|
||||
return
|
||||
for key in requester.call_direction:
|
||||
sensors.append(ObihaiServiceSensors(requester.pyobihai, requester.serial, key))
|
||||
|
||||
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)
|
||||
async_add_entities(sensors, update_before_add=True)
|
||||
|
||||
|
||||
class ObihaiServiceSensors(SensorEntity):
|
||||
@@ -148,6 +157,10 @@ 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:
|
||||
|
||||
25
homeassistant/components/obihai/strings.json
Normal file
25
homeassistant/components/obihai/strings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import asyncio
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
import pyotgw
|
||||
import pyotgw.vars as gw_vars
|
||||
from serial import SerialException
|
||||
@@ -112,10 +113,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
config_entry.add_update_listener(options_updated)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
gateway.connect_and_subscribe(),
|
||||
timeout=CONNECTION_TIMEOUT,
|
||||
)
|
||||
async with async_timeout.timeout(CONNECTION_TIMEOUT):
|
||||
await gateway.connect_and_subscribe()
|
||||
except (asyncio.TimeoutError, ConnectionError, SerialException) as ex:
|
||||
await gateway.cleanup()
|
||||
raise ConfigEntryNotReady(
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import async_timeout
|
||||
import pyotgw
|
||||
from pyotgw import vars as gw_vars
|
||||
from serial import SerialException
|
||||
@@ -68,10 +69,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
test_connection(),
|
||||
timeout=CONNECTION_TIMEOUT,
|
||||
)
|
||||
async with async_timeout.timeout(CONNECTION_TIMEOUT):
|
||||
await test_connection()
|
||||
except asyncio.TimeoutError:
|
||||
return self._show_form({"base": "timeout_connect"})
|
||||
except (ConnectionError, SerialException):
|
||||
|
||||
@@ -46,11 +46,23 @@ 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."""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Websocket API for OTBR."""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import python_otbr_api
|
||||
|
||||
from homeassistant.components.websocket_api import (
|
||||
ActiveConnection,
|
||||
async_register_command,
|
||||
@@ -20,6 +22,7 @@ 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(
|
||||
@@ -51,3 +54,42 @@ 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"])
|
||||
|
||||
@@ -10,6 +10,7 @@ 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,
|
||||
@@ -21,4 +22,5 @@ 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,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
PRESET_COMFORT1 = "comfort-1"
|
||||
@@ -47,6 +48,7 @@ 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:
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
@@ -70,6 +71,7 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)
|
||||
_attr_translation_key = DOMAIN
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
@@ -43,6 +44,7 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
@@ -49,6 +50,7 @@ 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
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
|
||||
from ..const import DOMAIN
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
@@ -78,6 +79,7 @@ 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
|
||||
|
||||
@@ -15,19 +15,17 @@ 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] = {
|
||||
STATE_DEROGATION_ACTIVE: HVACMode.HEAT,
|
||||
STATE_DEROGATION_INACTIVE: HVACMode.AUTO,
|
||||
OverkizCommandParam.ACTIVE: HVACMode.HEAT,
|
||||
OverkizCommandParam.INACTIVE: HVACMode.AUTO,
|
||||
}
|
||||
HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()}
|
||||
|
||||
@@ -60,6 +58,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -83,6 +83,7 @@ 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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.7.3"],
|
||||
"requirements": ["pyoverkiz==1.7.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -28,6 +28,34 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import async_timeout
|
||||
from icmplib import NameLookupError, async_ping
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -230,9 +231,8 @@ class PingDataSubProcess(PingData):
|
||||
close_fds=False, # required for posix_spawn
|
||||
)
|
||||
try:
|
||||
out_data, out_error = await asyncio.wait_for(
|
||||
pinger.communicate(), self._count + PING_TIMEOUT
|
||||
)
|
||||
async with async_timeout.timeout(self._count + PING_TIMEOUT):
|
||||
out_data, out_error = await pinger.communicate()
|
||||
|
||||
if out_data:
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -303,9 +304,9 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="gas_consumed_interval",
|
||||
name="Gas consumed interval",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:meter-gas",
|
||||
native_unit_of_measurement=f"{UnitOfVolume.CUBIC_METERS}/{UnitOfTime.HOURS}",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="gas_consumed_cumulative",
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_COUNTRY, DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL]
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
@@ -59,6 +60,14 @@ 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."""
|
||||
|
||||
|
||||
97
homeassistant/components/prosegur/camera.py
Normal file
97
homeassistant/components/prosegur/camera.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -3,3 +3,5 @@
|
||||
DOMAIN = "prosegur"
|
||||
|
||||
CONF_COUNTRY = "country"
|
||||
|
||||
SERVICE_REQUEST_IMAGE = "request_image"
|
||||
|
||||
29
homeassistant/components/prosegur/diagnostics.py
Normal file
29
homeassistant/components/prosegur/diagnostics.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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,
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/prosegur",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyprosegur"],
|
||||
"requirements": ["pyprosegur==0.0.5"]
|
||||
"requirements": ["pyprosegur==0.0.8"]
|
||||
}
|
||||
|
||||
7
homeassistant/components/prosegur/services.yaml
Normal file
7
homeassistant/components/prosegur/services.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
request_image:
|
||||
name: Request Camera image
|
||||
description: Request a new image from a Prosegur Camera
|
||||
target:
|
||||
entity:
|
||||
domain: camera
|
||||
integration: prosegur
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
@@ -45,12 +46,14 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -250,7 +251,8 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
# enter it unless we have to (Python 3.11 will have zero cost try)
|
||||
return
|
||||
try:
|
||||
await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY)
|
||||
async with async_timeout.timeout(APP_LIST_DELAY):
|
||||
await self._app_list_event.wait()
|
||||
except asyncio.TimeoutError as err:
|
||||
# No need to try again
|
||||
self._app_list_event.set()
|
||||
|
||||
@@ -196,19 +196,30 @@ 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 was added after
|
||||
# automatic unit conversion was implemented.
|
||||
# If the sensor has 'sensor.private' in its entity options, it already has a
|
||||
# suggested_unit.
|
||||
registry_unit = registry_entry.unit_of_measurement
|
||||
if (
|
||||
(
|
||||
@@ -230,11 +241,14 @@ class SensorEntity(Entity):
|
||||
|
||||
# Set suggested_unit_of_measurement to the old unit to enable automatic
|
||||
# conversion
|
||||
registry.async_update_entity_options(
|
||||
self.registry_entry = 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."""
|
||||
@@ -305,12 +319,8 @@ class SensorEntity(Entity):
|
||||
|
||||
return None
|
||||
|
||||
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.
|
||||
"""
|
||||
def _get_initial_suggested_unit(self) -> str | UndefinedType:
|
||||
"""Return the initial unit."""
|
||||
# Unit suggested by the integration
|
||||
suggested_unit_of_measurement = self.suggested_unit_of_measurement
|
||||
|
||||
@@ -321,6 +331,19 @@ 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 {
|
||||
@@ -416,7 +439,7 @@ class SensorEntity(Entity):
|
||||
return self._sensor_option_unit_of_measurement
|
||||
|
||||
# Second priority, for non registered entities: unit suggested by integration
|
||||
if not self.registry_entry and self.suggested_unit_of_measurement:
|
||||
if not self.unique_id and self.suggested_unit_of_measurement:
|
||||
return self.suggested_unit_of_measurement
|
||||
|
||||
# Third priority: Legacy temperature conversion, which applies
|
||||
|
||||
@@ -4,6 +4,7 @@ from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import async_timeout
|
||||
from pysqueezebox import Server, async_discover
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -130,7 +131,8 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# no host specified, see if we can discover an unconfigured LMS server
|
||||
try:
|
||||
await asyncio.wait_for(self._discover(), timeout=TIMEOUT)
|
||||
async with async_timeout.timeout(TIMEOUT):
|
||||
await self._discover()
|
||||
return await self.async_step_edit()
|
||||
except asyncio.TimeoutError:
|
||||
errors["base"] = "no_server_found"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "statistics",
|
||||
"name": "Statistics",
|
||||
"after_dependencies": ["recorder"],
|
||||
"codeowners": ["@fabaff", "@ThomDietrich"],
|
||||
"codeowners": ["@ThomDietrich"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/statistics",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
|
||||
@@ -13,16 +13,23 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
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={})
|
||||
|
||||
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={})
|
||||
|
||||
@@ -198,26 +198,26 @@ class UniFiController:
|
||||
@callback
|
||||
def async_load_entities(description: UnifiEntityDescription) -> None:
|
||||
"""Load and subscribe to UniFi endpoints."""
|
||||
entities: list[UnifiEntity] = []
|
||||
api_handler = description.api_handler_fn(self.api)
|
||||
|
||||
@callback
|
||||
def async_add_unifi_entity(obj_ids: list[str]) -> None:
|
||||
"""Add UniFi entity."""
|
||||
async_add_entities(
|
||||
[
|
||||
unifi_platform_entity(obj_id, self, description)
|
||||
for obj_id in obj_ids
|
||||
if description.allowed_fn(self, obj_id)
|
||||
if description.supported_fn(self, obj_id)
|
||||
]
|
||||
)
|
||||
|
||||
async_add_unifi_entity(list(api_handler))
|
||||
|
||||
@callback
|
||||
def async_create_entity(event: ItemEvent, obj_id: str) -> None:
|
||||
"""Create UniFi entity."""
|
||||
if not description.allowed_fn(
|
||||
self, obj_id
|
||||
) or not description.supported_fn(self, obj_id):
|
||||
return
|
||||
|
||||
entity = unifi_platform_entity(obj_id, self, description)
|
||||
if event == ItemEvent.ADDED:
|
||||
async_add_entities([entity])
|
||||
return
|
||||
entities.append(entity)
|
||||
|
||||
for obj_id in api_handler:
|
||||
async_create_entity(ItemEvent.CHANGED, obj_id)
|
||||
async_add_entities(entities)
|
||||
"""Create new UniFi entity on event."""
|
||||
async_add_unifi_entity([obj_id])
|
||||
|
||||
api_handler.subscribe(async_create_entity, ItemEvent.ADDED)
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
@@ -78,6 +80,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
TrackTemplate,
|
||||
@@ -93,6 +96,7 @@ ATTR_ACTIVE_CHILD = "active_child"
|
||||
CONF_ATTRS = "attributes"
|
||||
CONF_CHILDREN = "children"
|
||||
CONF_COMMANDS = "commands"
|
||||
CONF_BROWSE_MEDIA_ENTITY = "browse_media_entity"
|
||||
|
||||
STATES_ORDER = [
|
||||
STATE_UNKNOWN,
|
||||
@@ -119,6 +123,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_ATTRS, default={}): vol.Or(
|
||||
cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA
|
||||
),
|
||||
vol.Optional(CONF_BROWSE_MEDIA_ENTITY): cv.string,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
|
||||
@@ -136,17 +141,7 @@ async def async_setup_platform(
|
||||
"""Set up the universal media players."""
|
||||
await async_setup_reload_service(hass, "universal", ["media_player"])
|
||||
|
||||
player = UniversalMediaPlayer(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_CHILDREN),
|
||||
config.get(CONF_COMMANDS),
|
||||
config.get(CONF_ATTRS),
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE_CLASS),
|
||||
config.get(CONF_STATE_TEMPLATE),
|
||||
)
|
||||
|
||||
player = UniversalMediaPlayer(hass, config)
|
||||
async_add_entities([player])
|
||||
|
||||
|
||||
@@ -158,30 +153,25 @@ class UniversalMediaPlayer(MediaPlayerEntity):
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
name,
|
||||
children,
|
||||
commands,
|
||||
attributes,
|
||||
unique_id=None,
|
||||
device_class=None,
|
||||
state_template=None,
|
||||
config,
|
||||
):
|
||||
"""Initialize the Universal media device."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._children = children
|
||||
self._cmds = commands
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._children = config.get(CONF_CHILDREN)
|
||||
self._cmds = config.get(CONF_COMMANDS)
|
||||
self._attrs = {}
|
||||
for key, val in attributes.items():
|
||||
for key, val in config.get(CONF_ATTRS).items():
|
||||
attr = list(map(str.strip, val.split("|", 1)))
|
||||
if len(attr) == 1:
|
||||
attr.append(None)
|
||||
self._attrs[key] = attr
|
||||
self._child_state = None
|
||||
self._state_template_result = None
|
||||
self._state_template = state_template
|
||||
self._device_class = device_class
|
||||
self._attr_unique_id = unique_id
|
||||
self._state_template = config.get(CONF_STATE_TEMPLATE)
|
||||
self._device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._browse_media_entity = config.get(CONF_BROWSE_MEDIA_ENTITY)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to children and template state changes."""
|
||||
@@ -302,6 +292,11 @@ class UniversalMediaPlayer(MediaPlayerEntity):
|
||||
"""Return the name of universal player."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return True if unable to access real state of the entity."""
|
||||
return self._child_attr(ATTR_ASSUMED_STATE)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state of media player.
|
||||
@@ -497,6 +492,9 @@ class UniversalMediaPlayer(MediaPlayerEntity):
|
||||
if SERVICE_PLAY_MEDIA in self._cmds:
|
||||
flags |= MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
|
||||
if self._browse_media_entity:
|
||||
flags |= MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
|
||||
if SERVICE_CLEAR_PLAYLIST in self._cmds:
|
||||
flags |= MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||
|
||||
@@ -628,6 +626,20 @@ class UniversalMediaPlayer(MediaPlayerEntity):
|
||||
# Delegate to turn_on or turn_off by default
|
||||
await super().async_toggle()
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""Return a BrowseMedia instance."""
|
||||
entity_id = self._browse_media_entity
|
||||
if not entity_id and self._child_state:
|
||||
entity_id = self._child_state.entity_id
|
||||
component: EntityComponent[MediaPlayerEntity] = self.hass.data[DOMAIN]
|
||||
if entity_id and (entity := component.get_entity(entity_id)):
|
||||
return await entity.async_browse_media(media_content_type, media_content_id)
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update state in HA."""
|
||||
self._child_state = None
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import async_timeout
|
||||
from async_upnp_client.exceptions import UpnpConnectionError
|
||||
|
||||
from homeassistant.components import ssdp
|
||||
@@ -70,7 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(device_discovered_event.wait(), timeout=10)
|
||||
async with async_timeout.timeout(10):
|
||||
await device_discovered_event.wait()
|
||||
except asyncio.TimeoutError as err:
|
||||
raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err
|
||||
finally:
|
||||
|
||||
@@ -5,24 +5,28 @@ from typing import cast
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.trigger import (
|
||||
TriggerActionType,
|
||||
TriggerInfo,
|
||||
TriggerProtocol,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .triggers import TriggersPlatformModule, turn_on
|
||||
from .triggers import turn_on
|
||||
|
||||
TRIGGERS = {
|
||||
"turn_on": turn_on,
|
||||
}
|
||||
|
||||
|
||||
def _get_trigger_platform(config: ConfigType) -> TriggersPlatformModule:
|
||||
def _get_trigger_platform(config: ConfigType) -> TriggerProtocol:
|
||||
"""Return trigger platform."""
|
||||
platform_split = config[CONF_PLATFORM].split(".", maxsplit=1)
|
||||
if len(platform_split) < 2 or platform_split[1] not in TRIGGERS:
|
||||
raise ValueError(
|
||||
f"Unknown webOS Smart TV trigger platform {config[CONF_PLATFORM]}"
|
||||
)
|
||||
return cast(TriggersPlatformModule, TRIGGERS[platform_split[1]])
|
||||
return cast(TriggerProtocol, TRIGGERS[platform_split[1]])
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
@@ -41,10 +45,4 @@ async def async_attach_trigger(
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach trigger of specified platform."""
|
||||
platform = _get_trigger_platform(config)
|
||||
assert hasattr(platform, "async_attach_trigger")
|
||||
return cast(
|
||||
CALLBACK_TYPE,
|
||||
await getattr(platform, "async_attach_trigger")(
|
||||
hass, config, action, trigger_info
|
||||
),
|
||||
)
|
||||
return await platform.async_attach_trigger(hass, config, action, trigger_info)
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
"""webOS Smart TV triggers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
class TriggersPlatformModule(Protocol):
|
||||
"""Protocol type for the triggers platform."""
|
||||
|
||||
TRIGGER_SCHEMA: vol.Schema
|
||||
|
||||
@@ -2,18 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from importlib.metadata import version
|
||||
from typing import Any
|
||||
|
||||
import bellows
|
||||
import pkg_resources
|
||||
import zigpy
|
||||
from zigpy.config import CONF_NWK_EXTENDED_PAN_ID
|
||||
from zigpy.profiles import PROFILES
|
||||
from zigpy.zcl import Cluster
|
||||
import zigpy_deconz
|
||||
import zigpy_xbee
|
||||
import zigpy_zigate
|
||||
import zigpy_znp
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -79,13 +73,13 @@ async def async_get_config_entry_diagnostics(
|
||||
"config_entry": config_entry.as_dict(),
|
||||
"application_state": shallow_asdict(gateway.application_controller.state),
|
||||
"versions": {
|
||||
"bellows": bellows.__version__,
|
||||
"zigpy": zigpy.__version__,
|
||||
"zigpy_deconz": zigpy_deconz.__version__,
|
||||
"zigpy_xbee": zigpy_xbee.__version__,
|
||||
"zigpy_znp": zigpy_znp.__version__,
|
||||
"zigpy_zigate": zigpy_zigate.__version__,
|
||||
"zhaquirks": pkg_resources.get_distribution("zha-quirks").version,
|
||||
"bellows": version("bellows"),
|
||||
"zigpy": version("zigpy"),
|
||||
"zigpy_deconz": version("zigpy-deconz"),
|
||||
"zigpy_xbee": version("zigpy-xbee"),
|
||||
"zigpy_znp": version("zigpy_znp"),
|
||||
"zigpy_zigate": version("zigpy-zigate"),
|
||||
"zhaquirks": version("zha-quirks"),
|
||||
},
|
||||
},
|
||||
KEYS_TO_REDACT,
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
"zigpy_znp"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.34.7",
|
||||
"bellows==0.34.9",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.93",
|
||||
"zigpy-deconz==0.19.2",
|
||||
"zigpy==0.53.0",
|
||||
"zigpy==0.53.2",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.2"
|
||||
"zigpy-znp==0.9.3"
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
|
||||
@@ -463,7 +463,8 @@ class ConfigEntry:
|
||||
|
||||
await self._async_process_on_unload()
|
||||
return
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# pylint: disable-next=broad-except
|
||||
except (asyncio.CancelledError, SystemExit, Exception):
|
||||
_LOGGER.exception(
|
||||
"Error setting up entry %s for %s", self.title, integration.domain
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ from .backports.enum import StrEnum
|
||||
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0b4"
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)
|
||||
|
||||
@@ -293,6 +293,7 @@ FLOWS = {
|
||||
"nut",
|
||||
"nws",
|
||||
"nzbget",
|
||||
"obihai",
|
||||
"octoprint",
|
||||
"omnilogic",
|
||||
"oncue",
|
||||
|
||||
@@ -3777,7 +3777,7 @@
|
||||
"obihai": {
|
||||
"name": "Obihai",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"octoprint": {
|
||||
|
||||
@@ -7,15 +7,13 @@ from collections.abc import Callable, Container, Generator
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zone as zone_cmp
|
||||
from homeassistant.components.device_automation import condition as device_condition
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -55,6 +53,7 @@ from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
TemplateError,
|
||||
)
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -77,12 +76,44 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
||||
FROM_CONFIG_FORMAT = "{}_from_config"
|
||||
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_PLATFORM_ALIASES = {
|
||||
"and": None,
|
||||
"device": "device_automation",
|
||||
"not": None,
|
||||
"numeric_state": None,
|
||||
"or": None,
|
||||
"state": None,
|
||||
"sun": None,
|
||||
"template": None,
|
||||
"time": None,
|
||||
"trigger": None,
|
||||
"zone": None,
|
||||
}
|
||||
|
||||
INPUT_ENTITY_ID = re.compile(
|
||||
r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(?<!_)$"
|
||||
)
|
||||
|
||||
|
||||
class ConditionProtocol(Protocol):
|
||||
"""Define the format of device_condition modules.
|
||||
|
||||
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
|
||||
) -> ConditionCheckerType:
|
||||
"""Evaluate state based on configuration."""
|
||||
|
||||
|
||||
ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool | None]
|
||||
|
||||
|
||||
@@ -152,6 +183,27 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke
|
||||
return wrapper
|
||||
|
||||
|
||||
async def _async_get_condition_platform(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConditionProtocol | None:
|
||||
platform = config[CONF_CONDITION]
|
||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||
if platform is None:
|
||||
return None
|
||||
try:
|
||||
integration = await async_get_integration(hass, platform)
|
||||
except IntegrationNotFound:
|
||||
raise HomeAssistantError(
|
||||
f'Invalid condition "{platform}" specified {config}'
|
||||
) from None
|
||||
try:
|
||||
return integration.get_platform("condition")
|
||||
except ImportError:
|
||||
raise HomeAssistantError(
|
||||
f"Integration '{platform}' does not provide condition support"
|
||||
) from None
|
||||
|
||||
|
||||
async def async_from_config(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@@ -160,15 +212,18 @@ async def async_from_config(
|
||||
|
||||
Should be run on the event loop.
|
||||
"""
|
||||
condition = config.get(CONF_CONDITION)
|
||||
for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT):
|
||||
factory = getattr(sys.modules[__name__], fmt.format(condition), None)
|
||||
factory: Any = None
|
||||
platform = await _async_get_condition_platform(hass, config)
|
||||
|
||||
if factory:
|
||||
break
|
||||
if platform is None:
|
||||
condition = config.get(CONF_CONDITION)
|
||||
for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT):
|
||||
factory = getattr(sys.modules[__name__], fmt.format(condition), None)
|
||||
|
||||
if factory is None:
|
||||
raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}')
|
||||
if factory:
|
||||
break
|
||||
else:
|
||||
factory = platform.async_condition_from_config
|
||||
|
||||
# Check if condition is not enabled
|
||||
if not config.get(CONF_ENABLED, True):
|
||||
@@ -928,14 +983,6 @@ def zone_from_config(config: ConfigType) -> ConditionCheckerType:
|
||||
return if_in_zone
|
||||
|
||||
|
||||
async def async_device_from_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
checker = await device_condition.async_condition_from_config(hass, config)
|
||||
return trace_condition_function(checker)
|
||||
|
||||
|
||||
async def async_trigger_from_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConditionCheckerType:
|
||||
@@ -991,10 +1038,10 @@ async def async_validate_condition_config(
|
||||
config["conditions"] = conditions
|
||||
return config
|
||||
|
||||
if condition == "device":
|
||||
return await device_condition.async_validate_condition_config(hass, config)
|
||||
|
||||
if condition in ("numeric_state", "state"):
|
||||
platform = await _async_get_condition_platform(hass, config)
|
||||
if platform is not None and hasattr(platform, "async_validate_condition_config"):
|
||||
return await platform.async_validate_condition_config(hass, config)
|
||||
if platform is None and condition in ("numeric_state", "state"):
|
||||
validator = cast(
|
||||
Callable[[HomeAssistant, ConfigType], ConfigType],
|
||||
getattr(sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition)),
|
||||
|
||||
@@ -79,27 +79,27 @@ class Selector(Generic[_T]):
|
||||
return {"selector": {self.selector_type: self.config}}
|
||||
|
||||
|
||||
SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Integration that provided the entity
|
||||
vol.Optional("integration"): str,
|
||||
# Domain the entity belongs to
|
||||
vol.Optional("domain"): vol.Any(str, [str]),
|
||||
vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
|
||||
# Device class of the entity
|
||||
vol.Optional("device_class"): str,
|
||||
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SingleEntitySelectorConfig(TypedDict, total=False):
|
||||
class EntityFilterSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent a single entity selector config."""
|
||||
|
||||
integration: str
|
||||
domain: str | list[str]
|
||||
device_class: str
|
||||
device_class: str | list[str]
|
||||
|
||||
|
||||
SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Integration linked to it with a config entry
|
||||
vol.Optional("integration"): str,
|
||||
@@ -108,18 +108,21 @@ SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
||||
# Model of device
|
||||
vol.Optional("model"): str,
|
||||
# Device has to contain entities matching this selector
|
||||
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
|
||||
vol.Optional("entity"): vol.All(
|
||||
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SingleDeviceSelectorConfig(TypedDict, total=False):
|
||||
class DeviceFilterSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent a single device selector config."""
|
||||
|
||||
integration: str
|
||||
manufacturer: str
|
||||
model: str
|
||||
entity: SingleEntitySelectorConfig
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
|
||||
|
||||
class ActionSelectorConfig(TypedDict):
|
||||
@@ -176,8 +179,8 @@ class AddonSelector(Selector[AddonSelectorConfig]):
|
||||
class AreaSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent an area selector config."""
|
||||
|
||||
entity: SingleEntitySelectorConfig
|
||||
device: SingleDeviceSelectorConfig
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
multiple: bool
|
||||
|
||||
|
||||
@@ -189,8 +192,14 @@ class AreaSelector(Selector[AreaSelectorConfig]):
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
|
||||
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
|
||||
vol.Optional("entity"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("device"): vol.All(
|
||||
cv.ensure_list,
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -399,7 +408,7 @@ class DeviceSelectorConfig(TypedDict, total=False):
|
||||
integration: str
|
||||
manufacturer: str
|
||||
model: str
|
||||
entity: SingleEntitySelectorConfig
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
multiple: bool
|
||||
|
||||
|
||||
@@ -409,8 +418,14 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
|
||||
|
||||
selector_type = "device"
|
||||
|
||||
CONFIG_SCHEMA = SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{vol.Optional("multiple", default=False): cv.boolean}
|
||||
CONFIG_SCHEMA = DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("filter"): vol.All(
|
||||
cv.ensure_list,
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, config: DeviceSelectorConfig | None = None) -> None:
|
||||
@@ -457,7 +472,7 @@ class DurationSelector(Selector[DurationSelectorConfig]):
|
||||
return cast(dict[str, float], data)
|
||||
|
||||
|
||||
class EntitySelectorConfig(SingleEntitySelectorConfig, total=False):
|
||||
class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
|
||||
"""Class to represent an entity selector config."""
|
||||
|
||||
exclude_entities: list[str]
|
||||
@@ -471,11 +486,15 @@ class EntitySelector(Selector[EntitySelectorConfig]):
|
||||
|
||||
selector_type = "entity"
|
||||
|
||||
CONFIG_SCHEMA = SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
CONFIG_SCHEMA = ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional("exclude_entities"): [str],
|
||||
vol.Optional("include_entities"): [str],
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("filter"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -784,8 +803,8 @@ class SelectSelector(Selector[SelectSelectorConfig]):
|
||||
class TargetSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent a target selector config."""
|
||||
|
||||
entity: SingleEntitySelectorConfig
|
||||
device: SingleDeviceSelectorConfig
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
|
||||
|
||||
class StateSelectorConfig(TypedDict, total=False):
|
||||
@@ -832,8 +851,14 @@ class TargetSelector(Selector[TargetSelectorConfig]):
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
|
||||
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
|
||||
vol.Optional("entity"): vol.All(
|
||||
cv.ensure_list,
|
||||
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("device"): vol.All(
|
||||
cv.ensure_list,
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2063,6 +2063,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.template_cache: weakref.WeakValueDictionary[
|
||||
str | jinja2.nodes.Template, CodeType | str | None
|
||||
] = weakref.WeakValueDictionary()
|
||||
self.add_extension("jinja2.ext.loopcontrols")
|
||||
self.filters["round"] = forgiving_round
|
||||
self.filters["multiply"] = multiply
|
||||
self.filters["log"] = logarithm
|
||||
|
||||
@@ -23,7 +23,7 @@ fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
hassil==1.0.5
|
||||
home-assistant-bluetooth==1.9.3
|
||||
home-assistant-frontend==20230224.0
|
||||
home-assistant-frontend==20230227.0
|
||||
home-assistant-intents==2023.2.22
|
||||
httpx==0.23.3
|
||||
ifaddr==0.1.7
|
||||
|
||||
@@ -264,7 +264,8 @@ async def _async_setup_component(
|
||||
SLOW_SETUP_MAX_WAIT,
|
||||
)
|
||||
return False
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# pylint: disable-next=broad-except
|
||||
except (asyncio.CancelledError, SystemExit, Exception):
|
||||
_LOGGER.exception("Error during setup of component %s", domain)
|
||||
async_notify_setup_error(hass, domain, integration.documentation)
|
||||
return False
|
||||
|
||||
10
mypy.ini
10
mypy.ini
@@ -1622,6 +1622,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.litejet.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.litterrobot.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.3.0b4"
|
||||
version = "2023.4.0.dev0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -202,7 +202,7 @@ aiolifx_effects==0.3.1
|
||||
aiolifx_themes==0.4.0
|
||||
|
||||
# homeassistant.components.livisi
|
||||
aiolivisi==0.0.15
|
||||
aiolivisi==0.0.16
|
||||
|
||||
# homeassistant.components.lookin
|
||||
aiolookin==1.0.0
|
||||
@@ -276,7 +276,7 @@ aioskybell==22.7.0
|
||||
aioslimproto==2.1.1
|
||||
|
||||
# homeassistant.components.honeywell
|
||||
aiosomecomfort==0.0.8
|
||||
aiosomecomfort==0.0.10
|
||||
|
||||
# homeassistant.components.steamist
|
||||
aiosteamist==0.3.2
|
||||
@@ -342,7 +342,7 @@ anthemav==1.4.1
|
||||
apcaccess==0.0.13
|
||||
|
||||
# homeassistant.components.apprise
|
||||
apprise==1.2.1
|
||||
apprise==1.3.0
|
||||
|
||||
# homeassistant.components.aprs
|
||||
aprslib==0.7.0
|
||||
@@ -422,7 +422,7 @@ beautifulsoup4==4.11.1
|
||||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.7
|
||||
bellows==0.34.9
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.12.1
|
||||
@@ -492,7 +492,7 @@ brunt==1.2.0
|
||||
bt_proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.5.2
|
||||
bthome-ble==2.7.0
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -504,7 +504,7 @@ btsmarthub_devicelist==0.2.3
|
||||
buienradar==1.0.5
|
||||
|
||||
# homeassistant.components.caldav
|
||||
caldav==1.1.1
|
||||
caldav==1.2.0
|
||||
|
||||
# homeassistant.components.circuit
|
||||
circuit-webhook==1.0.1
|
||||
@@ -661,7 +661,7 @@ enocean==0.50
|
||||
enturclient==0.2.4
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env_canada==0.5.28
|
||||
env_canada==0.5.29
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
envoy_reader==0.20.1
|
||||
@@ -907,7 +907,7 @@ hole==0.8.0
|
||||
holidays==0.18.0
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230224.0
|
||||
home-assistant-frontend==20230227.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.2.22
|
||||
@@ -1248,7 +1248,7 @@ oauth2client==4.1.3
|
||||
objgraph==3.5.0
|
||||
|
||||
# homeassistant.components.garages_amsterdam
|
||||
odp-amsterdam==5.0.1
|
||||
odp-amsterdam==5.1.0
|
||||
|
||||
# homeassistant.components.oem
|
||||
oemthermostat==1.1.1
|
||||
@@ -1708,7 +1708,7 @@ pyirishrail==0.0.2
|
||||
pyiss==1.0.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==3.1.13
|
||||
pyisy==3.1.14
|
||||
|
||||
# homeassistant.components.itach
|
||||
pyitachip2ir==0.0.7
|
||||
@@ -1857,7 +1857,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.7.3
|
||||
pyoverkiz==1.7.6
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@@ -1884,7 +1884,7 @@ pypoint==2.3.0
|
||||
pyprof2calltree==1.4.5
|
||||
|
||||
# homeassistant.components.prosegur
|
||||
pyprosegur==0.0.5
|
||||
pyprosegur==0.0.8
|
||||
|
||||
# homeassistant.components.prusalink
|
||||
pyprusalink==1.1.0
|
||||
@@ -2653,7 +2653,7 @@ xboxapi==2.0.1
|
||||
xiaomi-ble==0.16.4
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.5.0
|
||||
xknx==2.6.0
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@@ -2724,10 +2724,10 @@ zigpy-xbee==0.16.2
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.2
|
||||
zigpy-znp==0.9.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.53.0
|
||||
zigpy==0.53.2
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
@@ -13,7 +13,7 @@ coverage==7.1.0
|
||||
freezegun==1.2.2
|
||||
mock-open==1.4.0
|
||||
mypy==1.0.1
|
||||
pre-commit==3.0.0
|
||||
pre-commit==3.1.0
|
||||
pydantic==1.10.5
|
||||
pylint==2.16.0
|
||||
pylint-per-file-ignores==1.1.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user