Compare commits

..

44 Commits

Author SHA1 Message Date
Paulus Schoutsen fbe1524f6c Bumped version to 2023.3.0b4 2023-02-26 22:37:34 -05:00
J. Nick Koston 95e337277c Avoid starting a bluetooth poll when Home Assistant is stopping (#88819)
* Avoid starting a bluetooth poll when Home Assistant is stopping

* tests
2023-02-26 22:37:26 -05:00
J. Nick Koston 1503674bd6 Prevent integrations from retrying setup once shutdown has started (#88818)
* Prevent integrations from retrying setup once shutdown has started

* coverage
2023-02-26 22:37:25 -05:00
J. Nick Koston ab6bd75b70 Fix flux_led discovery running at shutdown (#88817) 2023-02-26 22:37:24 -05:00
J. Nick Koston 2fff836bd4 Fix lock services not removing entity fields (#88805) 2023-02-26 22:37:23 -05:00
J. Nick Koston d8850758f1 Fix unifiprotect discovery running at shutdown (#88802)
* Fix unifiprotect discovery running at shutdown

Move the discovery start into `async_setup` so we only
start discovery once reguardless of how many config entries
for unifiprotect they have (or how many times they reload).

Always make discovery a background task so it does not get
to block shutdown

* missing decorator
2023-02-26 22:37:22 -05:00
J. Nick Koston 0449856064 Bump yalexs-ble to 2.0.4 (#88798)
changelog: https://github.com/bdraco/yalexs-ble/compare/v2.0.3...v2.0.4
2023-02-26 22:37:21 -05:00
starkillerOG e48089e0c9 Do not block on reolink firmware check fail (#88797)
Do not block on firmware check fail
2023-02-26 22:37:20 -05:00
starkillerOG a7e081f70d Simplify reolink update unique_id (#88794)
simplify unique_id
2023-02-26 22:37:19 -05:00
Paulus Schoutsen fe181425d8 Check circular dependencies (#88778) 2023-02-26 22:37:18 -05:00
Joakim Plate 8c7b29db25 Update nibe library to 2.0.0 (#88769) 2023-02-26 22:37:17 -05:00
J. Nick Koston aaa5bb9f86 Fix checking if a package is installed on py3.11 (#88768)
pkg_resources is abandoned and we need to move away
from using it https://github.com/pypa/pkg_resources

In the mean time we need to keep it working. This fixes
a new exception in py3.11 when a module is not installed
which allows proper fallback to pkg_resources.Requirement.parse
when needed

```
2023-02-25 15:46:21.101 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_protocol.py", line 433, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_app.py", line 504, in _handle
    resp = await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/security_filter.py", line 60, in security_filter_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/forwarded.py", line 100, in forwarded_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/request_context.py", line 28, in request_context_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/ban.py", line 80, in ban_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/auth.py", line 235, in auth_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/view.py", line 146, in handle
    result = await result
             ^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/config/config_entries.py", line 148, in post
    return await super().post(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/components/http/data_validator.py", line 72, in wrapper
    result = await method(view, request, data, *args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/helpers/data_entry_flow.py", line 71, in post
    result = await self._flow_mgr.async_init(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 826, in async_init
    flow, result = await task
                   ^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 844, in _async_init
    flow = await self.async_create_flow(handler, context=context, data=data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 950, in async_create_flow
    await async_process_deps_reqs(self.hass, self._hass_config, integration)
  File "/Users/bdraco/home-assistant/homeassistant/setup.py", line 384, in async_process_deps_reqs
    await requirements.async_get_integration_with_requirements(
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 52, in async_get_integration_with_requirements
    return await manager.async_get_integration_with_requirements(domain)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 171, in async_get_integration_with_requirements
    await self._async_process_integration(integration, done)
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 186, in _async_process_integration
    await self.async_process_requirements(
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 252, in async_process_requirements
    await self._async_process_requirements(name, missing)
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 284, in _async_process_requirements
    installed, failures = await self.hass.async_add_executor_job(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 113, in _install_requirements_if_missing
    if pkg_util.is_installed(req) or _install_with_retry(req, kwargs):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bdraco/home-assistant/homeassistant/util/package.py", line 40, in is_installed
    pkg_resources.get_distribution(package)
  File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 478, in get_distribution
    dist = get_provider(dist)
           ^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 354, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
                                            ~~~~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
``
2023-02-26 22:37:17 -05:00
J. Nick Koston 5b78e0c4ff Restore previous behavior of only waiting for new tasks at shutdown (#88740)
* Restore previous behavior of only waiting for new tasks at shutdown

* cleanup

* do a swap instead

* await canceled tasks

* await canceled tasks

* fix

* not needed since we no longer clear

* log it

* reword

* wait for airvisual

* tests
2023-02-26 22:37:16 -05:00
Franck Nijhof 2063dbf00d Bumped version to 2023.3.0b3 2023-02-25 12:07:47 +01:00
Joakim Sørensen 91a03ab83d Remove homeassistant_hardware after dependency from zha (#88751) 2023-02-25 12:07:25 +01:00
J. Nick Koston ed8f538890 Prevent new discovery flows from being created when stopping (#88743) 2023-02-25 12:07:22 +01:00
J. Nick Koston 6196607c5d Make hass.async_stop an untracked task (#88738) 2023-02-25 12:07:19 +01:00
J. Nick Koston 833ccafb76 Log futures that are blocking shutdown stages (#88736) 2023-02-25 12:07:15 +01:00
mkmer ca539d0a09 Add missing reauth strings to Honeywell (#88733)
Add missing reauth strings
2023-02-25 12:07:12 +01:00
Austin Mroczek 0e3e954000 Bump total_connect_client to v2023.2 (#88729)
* bump total_connect_client to v2023.2

* Trigger Build
2023-02-25 12:07:09 +01:00
avee87 4ef96c76e4 Fix log message in recorder on total_increasing reset (#88710) 2023-02-25 12:07:05 +01:00
Álvaro Fernández Rojas d5b0c1faa0 Update aioqsw v0.3.2 (#88695)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-02-25 12:07:02 +01:00
Arturo 2405908cdd Fix matter light color capabilities bit map (#88693)
* Adds matter light color capabilities bit map

* Fixed matter light hue and saturation test
2023-02-25 12:06:58 +01:00
Paulus Schoutsen b6e50135f5 Bumped version to 2023.3.0b2 2023-02-24 21:41:02 -05:00
Bram Kragten 64197aa5f5 Update frontend to 20230224.0 (#88721) 2023-02-24 21:40:56 -05:00
J. Nick Koston 5a2d7a5dd4 Reduce overhead to save json data to postgresql (#88717)
* Reduce overhead to strip nulls from json

* Reduce overhead to strip nulls from json

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

closes #88232

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

* Remove the serial number if it isn't available

* Use consistent names for the addon and Zigbee radio

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

* Handle `hassio` not existing when listing serial ports

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

* Bump library version to 3.1.0

* move discovery schemas to platforms

* optimize a tiny bit

* simplify even more

* fixed bug in light platform

* fix color control logic

* fix some issues

* Update homeassistant/components/matter/discovery.py

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

* fix some tests

* fix light test

---------

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

* Other errors in flow too

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

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

fix

* Update tests/components/zwave_js/test_api.py

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

* Update tests/components/zwave_js/test_api.py

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

* Update tests/components/zwave_js/test_api.py

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

* Update tests/components/zwave_js/test_api.py

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

* fix types

* Switch back to using Any

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-02-23 14:59:58 -05:00
puddly 45547d226e Disable the ZHA bellows UART thread when connecting to a TCP coordinator (#88202)
Disable the bellows UART thread when connecting to a TCP coordinator
2023-02-23 14:59:56 -05:00
Franck Nijhof cebc6dd096 Bumped version to 2023.3.0b0 2023-02-22 20:44:37 +01:00
157 changed files with 1200 additions and 4123 deletions
+1 -6
View File
@@ -639,10 +639,6 @@ omit =
homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
homeassistant/components/livisi/__init__.py
homeassistant/components/livisi/climate.py
homeassistant/components/livisi/coordinator.py
homeassistant/components/livisi/switch.py
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/logi_circle/__init__.py
homeassistant/components/logi_circle/camera.py
@@ -807,8 +803,7 @@ omit =
homeassistant/components/nuki/sensor.py
homeassistant/components/nx584/alarm_control_panel.py
homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/obihai/connectivity.py
homeassistant/components/obihai/sensor.py
homeassistant/components/obihai/*
homeassistant/components/octoprint/__init__.py
homeassistant/components/oem/climate.py
homeassistant/components/ohmconnect/sensor.py
+3 -3
View File
@@ -31,7 +31,7 @@ env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.4
HA_SHORT_VERSION: 2023.3
DEFAULT_PYTHON: "3.10"
ALL_PYTHON_VERSIONS: "['3.10', '3.11']"
# 10.3 is the oldest supported version
@@ -1073,10 +1073,10 @@ jobs:
ffmpeg \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@v3.3.0
uses: actions/checkout@v3.1.0
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.5.0
uses: actions/setup-python@v4.3.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
-1
View File
@@ -186,7 +186,6 @@ homeassistant.components.ld2410_ble.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*
homeassistant.components.local_ip.*
homeassistant.components.lock.*
+3 -4
View File
@@ -825,8 +825,7 @@ build.json @home-assistant/supervisor
/tests/components/nws/ @MatthewFlamm @kamiyo
/homeassistant/components/nzbget/ @chriscla
/tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney
/tests/components/obihai/ @dshokouhi @ejpenney
/homeassistant/components/obihai/ @dshokouhi
/homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480
@@ -1139,8 +1138,8 @@ build.json @home-assistant/supervisor
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich
/tests/components/statistics/ @ThomDietrich
/homeassistant/components/statistics/ @fabaff @ThomDietrich
/tests/components/statistics/ @fabaff @ThomDietrich
/homeassistant/components/steam_online/ @tkdrob
/tests/components/steam_online/ @tkdrob
/homeassistant/components/steamist/ @bdraco
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from AIOAladdinConnect import AladdinConnectClient
@@ -19,6 +20,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CLIENT_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
@@ -131,6 +134,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(
self, import_data: dict[str, Any] | None = None
) -> FlowResult:
"""Import Aladin Connect config from configuration.yaml."""
return await self.async_step_user(import_data)
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
@@ -2,24 +2,63 @@
from __future__ import annotations
from datetime import timedelta
from typing import Any
import logging
from typing import Any, Final
from AIOAladdinConnect import AladdinConnectClient
import voluptuous as vol
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.components.cover import (
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPENING,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES
from .model import DoorDevice
_LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
SCAN_INTERVAL = timedelta(seconds=300)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up Aladdin Connect devices yaml depreciated."""
_LOGGER.warning(
"Configuring Aladdin Connect through yaml is deprecated. Please remove it from"
" your configuration as it has already been imported to a config entry"
)
await hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
+7 -11
View File
@@ -5,7 +5,6 @@ import asyncio
from http import HTTPStatus
import json
import logging
from typing import cast
import aiohttp
import async_timeout
@@ -16,7 +15,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.significant_change import create_checker
import homeassistant.util.dt as dt_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
@@ -164,10 +162,9 @@ async def async_send_changereport_message(
if response.status == HTTPStatus.ACCEPTED:
return
response_json = json_loads_object(response_text)
response_payload = cast(JsonObjectType, response_json["payload"])
response_json = json.loads(response_text)
if response_payload["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
if invalidate_access_token:
# Invalidate the access token and try again
config.async_invalidate_access_token()
@@ -183,8 +180,8 @@ async def async_send_changereport_message(
_LOGGER.error(
"Error when sending ChangeReport for %s to Alexa: %s: %s",
alexa_entity.entity_id,
response_payload["code"],
response_payload["description"],
response_json["payload"]["code"],
response_json["payload"]["description"],
)
@@ -302,12 +299,11 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
if response.status == HTTPStatus.ACCEPTED:
return
response_json = json_loads_object(response_text)
response_payload = cast(JsonObjectType, response_json["payload"])
response_json = json.loads(response_text)
_LOGGER.error(
"Error when sending DoorbellPress event for %s to Alexa: %s: %s",
alexa_entity.entity_id,
response_payload["code"],
response_payload["description"],
response_json["payload"]["code"],
response_json["payload"]["description"],
)
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/apprise",
"iot_class": "cloud_push",
"loggers": ["apprise"],
"requirements": ["apprise==1.3.0"]
"requirements": ["apprise==1.2.1"]
}
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==2.7.0"]
"requirements": ["bthome-ble==2.5.2"]
}
-10
View File
@@ -119,16 +119,6 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# Gas (m3)
(
BTHomeSensorDeviceClass.GAS,
Units.VOLUME_CUBIC_METERS,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.GAS}_{Units.VOLUME_CUBIC_METERS}",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# Humidity in (percent)
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.2.0"]
"requirements": ["caldav==1.1.1"]
}
@@ -3,7 +3,6 @@
DOMAIN = "conversation"
DEFAULT_EXPOSED_DOMAINS = {
"binary_sensor",
"climate",
"cover",
"fan",
@@ -17,5 +16,3 @@ DEFAULT_EXPOSED_DOMAINS = {
"vacuum",
"water_heater",
}
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
@@ -28,7 +28,7 @@ from homeassistant.helpers import (
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN
from .const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
@@ -479,12 +479,6 @@ class DefaultAgent(AbstractConversationAgent):
for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr_key, attr_value in state.attributes.items():
if attr_key not in DEFAULT_EXPOSED_ATTRIBUTES:
continue
context[attr_key] = attr_value
entity = entities.async_get(state.entity_id)
if entity is not None:
@@ -524,9 +518,6 @@ class DefaultAgent(AbstractConversationAgent):
for alias in area.aliases:
area_names.append((alias, area.id))
_LOGGER.debug("Exposed areas: %s", area_names)
_LOGGER.debug("Exposed entities: %s", entity_names)
self._slot_lists = {
"area": TextSlotList.from_tuples(area_names, allow_template=False),
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
@@ -8,7 +8,6 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import ConditionProtocol, trace_condition_function
from homeassistant.helpers.typing import ConfigType
from . import DeviceAutomationType, async_get_device_automation_platform
@@ -18,13 +17,24 @@ if TYPE_CHECKING:
from homeassistant.helpers import condition
class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
class DeviceAutomationConditionProtocol(Protocol):
"""Define the format of device_condition modules.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config
from ConditionProtocol.
Each module must define either CONDITION_SCHEMA or async_validate_condition_config.
"""
CONDITION_SCHEMA: vol.Schema
async def async_validate_condition_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
def async_condition_from_config(
self, hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Evaluate state based on configuration."""
async def async_get_condition_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
@@ -52,4 +62,4 @@ async def async_condition_from_config(
platform = await async_get_device_automation_platform(
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
)
return trace_condition_function(platform.async_condition_from_config(hass, config))
return platform.async_condition_from_config(hass, config)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env_canada==0.5.29"]
"requirements": ["env_canada==0.5.28"]
}
@@ -341,11 +341,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
is_dev = repo_path is not None
root_path = _frontend_root(repo_path)
if is_dev:
from .dev import async_setup_frontend_dev
async_setup_frontend_dev(hass)
for path, should_cache in (
("service_worker.js", False),
("robots.txt", False),
-60
View File
@@ -1,60 +0,0 @@
"""Development helpers for the frontend."""
import aiohttp
from aiohttp import hdrs, web
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
@callback
def async_setup_frontend_dev(hass: HomeAssistant) -> None:
"""Set up frontend dev views."""
hass.http.register_view( # type: ignore
FrontendDevView(
"http://localhost:8000", aiohttp_client.async_get_clientsession(hass)
)
)
FILTER_RESPONSE_HEADERS = {hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING}
class FrontendDevView(HomeAssistantView):
"""Frontend dev view."""
name = "_dev:frontend"
url = "/_dev_frontend/{path:.*}"
requires_auth = False
extra_urls = ["/__web-dev-server__/{path:.*}"]
def __init__(self, forward_base: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io ingress view."""
self._forward_base = forward_base
self._websession = websession
async def get(self, request: web.Request, path: str) -> web.Response:
"""Frontend routing."""
# To deal with: import * as commonjsHelpers from '/__web-dev-server__/rollup/commonjsHelpers.js
if request.path.startswith("/__web-dev-server__/"):
path = f"__web-dev-server__/{path}"
url = f"{self._forward_base}/{path}"
if request.query_string:
url += f"?{request.query_string}"
async with self._websession.get(
url,
headers=request.headers,
allow_redirects=False,
) as result:
return web.Response(
headers={
hdr: val
for hdr, val in result.headers.items()
if hdr not in FILTER_RESPONSE_HEADERS
},
status=result.status,
body=await result.read(),
)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230227.0"]
"requirements": ["home-assistant-frontend==20230224.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
"iot_class": "cloud_polling",
"requirements": ["odp-amsterdam==5.1.0"]
"requirements": ["odp-amsterdam==5.0.1"]
}
@@ -1,7 +1,6 @@
"""Config flow for HLK-SW16."""
import asyncio
import async_timeout
from hlk_sw16 import create_hlk_sw16_connection
import voluptuous as vol
@@ -36,8 +35,7 @@ async def connect_client(hass, user_input):
reconnect_interval=DEFAULT_RECONNECT_INTERVAL,
keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL,
)
async with async_timeout.timeout(CONNECTION_TIMEOUT):
return await client_aw
return await asyncio.wait_for(client_aw, timeout=CONNECTION_TIMEOUT)
async def validate_input(hass: HomeAssistant, user_input):
@@ -14,7 +14,6 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,
@@ -1,39 +0,0 @@
"""Helper functions for Homematicip Cloud Integration."""
from functools import wraps
import json
import logging
from homeassistant.exceptions import HomeAssistantError
from . import HomematicipGenericEntity
_LOGGER = logging.getLogger(__name__)
def is_error_response(response) -> bool:
"""Response from async call contains errors or not."""
if isinstance(response, dict):
return response.get("errorCode") not in ("", None)
return False
def handle_errors(func):
"""Handle async errors."""
@wraps(func)
async def inner(self: HomematicipGenericEntity) -> None:
"""Handle errors from async call."""
result = await func(self)
if is_error_response(result):
_LOGGER.error(
"Error while execute function %s: %s",
__name__,
json.dumps(result),
)
raise HomeAssistantError(
f"Error while execute function {func.__name__}: {result.get('errorCode')}. See log for more information."
)
return inner
@@ -1,95 +0,0 @@
"""Support for HomematicIP Cloud lock devices."""
from __future__ import annotations
import logging
from typing import Any
from homematicip.aio.device import AsyncDoorLockDrive
from homematicip.base.enums import LockState, MotorState
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity
from .helpers import handle_errors
_LOGGER = logging.getLogger(__name__)
ATTR_AUTO_RELOCK_DELAY = "auto_relock_delay"
ATTR_DOOR_HANDLE_TYPE = "door_handle_type"
ATTR_DOOR_LOCK_DIRECTION = "door_lock_direction"
ATTR_DOOR_LOCK_NEUTRAL_POSITION = "door_lock_neutral_position"
ATTR_DOOR_LOCK_TURNS = "door_lock_turns"
DEVICE_DLD_ATTRIBUTES = {
"autoRelockDelay": ATTR_AUTO_RELOCK_DELAY,
"doorHandleType": ATTR_DOOR_HANDLE_TYPE,
"doorLockDirection": ATTR_DOOR_LOCK_DIRECTION,
"doorLockNeutralPosition": ATTR_DOOR_LOCK_NEUTRAL_POSITION,
"doorLockTurns": ATTR_DOOR_LOCK_TURNS,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the HomematicIP locks from a config entry."""
hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipDoorLockDrive(hap, device)
for device in hap.home.devices
if isinstance(device, AsyncDoorLockDrive)
)
class HomematicipDoorLockDrive(HomematicipGenericEntity, LockEntity):
"""Representation of the HomematicIP DoorLockDrive."""
_attr_supported_features = LockEntityFeature.OPEN
@property
def is_locked(self) -> bool | None:
"""Return true if device is locked."""
return (
self._device.lockState == LockState.LOCKED
and self._device.motorState == MotorState.STOPPED
)
@property
def is_locking(self) -> bool:
"""Return true if device is locking."""
return self._device.motorState == MotorState.CLOSING
@property
def is_unlocking(self) -> bool:
"""Return true if device is unlocking."""
return self._device.motorState == MotorState.OPENING
@handle_errors
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
return await self._device.set_lock_state(LockState.LOCKED)
@handle_errors
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
return await self._device.set_lock_state(LockState.UNLOCKED)
@handle_errors
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
return await self._device.set_lock_state(LockState.OPEN)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
return super().extra_state_attributes | {
attr_key: attr_value
for attr, attr_key in DEVICE_DLD_ATTRIBUTES.items()
if (attr_value := getattr(self._device, attr, None)) is not None
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"iot_class": "cloud_polling",
"loggers": ["somecomfort"],
"requirements": ["aiosomecomfort==0.0.10"]
"requirements": ["aiosomecomfort==0.0.8"]
}
@@ -1,13 +1,22 @@
"""The islamic_prayer_times component."""
from __future__ import annotations
from datetime import timedelta
import logging
from prayer_times_calculator import PrayerTimesCalculator, exceptions
from requests.exceptions import ConnectionError as ConnError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
import homeassistant.util.dt as dt_util
from .const import DOMAIN
from .coordinator import IslamicPrayerDataUpdateCoordinator
from .const import CONF_CALC_METHOD, DATA_UPDATED, DEFAULT_CALC_METHOD, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
@@ -16,32 +25,154 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Islamic Prayer Component."""
coordinator = IslamicPrayerDataUpdateCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, coordinator)
config_entry.async_on_unload(
config_entry.add_update_listener(async_options_updated)
)
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
client = IslamicPrayerClient(hass, config_entry)
hass.data[DOMAIN] = client
await client.async_setup()
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Islamic Prayer entry from config_entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data.pop(DOMAIN)
if coordinator.event_unsub:
coordinator.event_unsub()
return unload_ok
if hass.data[DOMAIN].event_unsub:
hass.data[DOMAIN].event_unsub()
hass.data.pop(DOMAIN)
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Triggered by config entry options updates."""
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN]
if coordinator.event_unsub:
coordinator.event_unsub()
await coordinator.async_request_refresh()
class IslamicPrayerClient:
"""Islamic Prayer Client Object."""
def __init__(self, hass, config_entry):
"""Initialize the Islamic Prayer client."""
self.hass = hass
self.config_entry = config_entry
self.prayer_times_info = {}
self.available = True
self.event_unsub = None
@property
def calc_method(self):
"""Return the calculation method."""
return self.config_entry.options[CONF_CALC_METHOD]
def get_new_prayer_times(self):
"""Fetch prayer times for today."""
calc = PrayerTimesCalculator(
latitude=self.hass.config.latitude,
longitude=self.hass.config.longitude,
calculation_method=self.calc_method,
date=str(dt_util.now().date()),
)
return calc.fetch_prayer_times()
async def async_schedule_future_update(self):
"""Schedule future update for sensors.
Midnight is a calculated time. The specifics of the calculation
depends on the method of the prayer time calculation. This calculated
midnight is the time at which the time to pray the Isha prayers have
expired.
Calculated Midnight: The Islamic midnight.
Traditional Midnight: 12:00AM
Update logic for prayer times:
If the Calculated Midnight is before the traditional midnight then wait
until the traditional midnight to run the update. This way the day
will have changed over and we don't need to do any fancy calculations.
If the Calculated Midnight is after the traditional midnight, then wait
until after the calculated Midnight. We don't want to update the prayer
times too early or else the timings might be incorrect.
Example:
calculated midnight = 11:23PM (before traditional midnight)
Update time: 12:00AM
calculated midnight = 1:35AM (after traditional midnight)
update time: 1:36AM.
"""
_LOGGER.debug("Scheduling next update for Islamic prayer times")
now = dt_util.utcnow()
midnight_dt = self.prayer_times_info["Midnight"]
if now > dt_util.as_utc(midnight_dt):
next_update_at = midnight_dt + timedelta(days=1, minutes=1)
_LOGGER.debug(
"Midnight is after day the changes so schedule update for after"
" Midnight the next day"
)
else:
_LOGGER.debug(
"Midnight is before the day changes so schedule update for the next"
" start of day"
)
next_update_at = dt_util.start_of_local_day(now + timedelta(days=1))
_LOGGER.info("Next update scheduled for: %s", next_update_at)
self.event_unsub = async_track_point_in_time(
self.hass, self.async_update, next_update_at
)
async def async_update(self, *_):
"""Update sensors with new prayer times."""
try:
prayer_times = await self.hass.async_add_executor_job(
self.get_new_prayer_times
)
self.available = True
except (exceptions.InvalidResponseError, ConnError):
self.available = False
_LOGGER.debug("Error retrieving prayer times")
async_call_later(self.hass, 60, self.async_update)
return
for prayer, time in prayer_times.items():
self.prayer_times_info[prayer] = dt_util.parse_datetime(
f"{dt_util.now().date()} {time}"
)
await self.async_schedule_future_update()
_LOGGER.debug("New prayer times retrieved. Updating sensors")
async_dispatcher_send(self.hass, DATA_UPDATED)
async def async_setup(self):
"""Set up the Islamic prayer client."""
await self.async_add_options()
try:
await self.hass.async_add_executor_job(self.get_new_prayer_times)
except (exceptions.InvalidResponseError, ConnError) as err:
raise ConfigEntryNotReady from err
await self.async_update()
self.config_entry.add_update_listener(self.async_options_updated)
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS
)
return True
async def async_add_options(self):
"""Add options for entry."""
if not self.config_entry.options:
data = dict(self.config_entry.data)
calc_method = data.pop(CONF_CALC_METHOD, DEFAULT_CALC_METHOD)
self.hass.config_entries.async_update_entry(
self.config_entry, data=data, options={CONF_CALC_METHOD: calc_method}
)
@staticmethod
async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Triggered by config entry options updates."""
if hass.data[DOMAIN].event_unsub:
hass.data[DOMAIN].event_unsub()
await hass.data[DOMAIN].async_update()
@@ -1,13 +1,10 @@
"""Config flow for Islamic Prayer Times integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME
@@ -25,9 +22,7 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return IslamicPrayerOptionsFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
@@ -45,9 +40,7 @@ class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_init(self, user_input=None):
"""Manage options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
@@ -1,12 +1,23 @@
"""Constants for the Islamic Prayer component."""
from typing import Final
from prayer_times_calculator import PrayerTimesCalculator
DOMAIN: Final = "islamic_prayer_times"
NAME: Final = "Islamic Prayer Times"
DOMAIN = "islamic_prayer_times"
NAME = "Islamic Prayer Times"
PRAYER_TIMES_ICON = "mdi:calendar-clock"
CONF_CALC_METHOD: Final = "calculation_method"
SENSOR_TYPES = {
"Fajr": "prayer",
"Sunrise": "time",
"Dhuhr": "prayer",
"Asr": "prayer",
"Maghrib": "prayer",
"Isha": "prayer",
"Midnight": "time",
}
CONF_CALC_METHOD = "calculation_method"
CALC_METHODS: list[str] = list(PrayerTimesCalculator.CALCULATION_METHODS)
DEFAULT_CALC_METHOD: Final = "isna"
DEFAULT_CALC_METHOD = "isna"
DATA_UPDATED = "Islamic_prayer_data_updated"
@@ -1,121 +0,0 @@
"""Coordinator for the Islamic prayer times integration."""
from __future__ import annotations
from datetime import datetime, timedelta
import logging
from prayer_times_calculator import PrayerTimesCalculator, exceptions
from requests.exceptions import ConnectionError as ConnError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
from .const import CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN
_LOGGER = logging.getLogger(__name__)
class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetime]]):
"""Islamic Prayer Client Object."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the Islamic Prayer client."""
self.event_unsub: CALLBACK_TYPE | None = None
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
)
@property
def calc_method(self) -> str:
"""Return the calculation method."""
return self.config_entry.options.get(CONF_CALC_METHOD, DEFAULT_CALC_METHOD)
def get_new_prayer_times(self) -> dict[str, str]:
"""Fetch prayer times for today."""
calc = PrayerTimesCalculator(
latitude=self.hass.config.latitude,
longitude=self.hass.config.longitude,
calculation_method=self.calc_method,
date=str(dt_util.now().date()),
)
return calc.fetch_prayer_times()
@callback
def async_schedule_future_update(self, midnight_dt: datetime) -> None:
"""Schedule future update for sensors.
Midnight is a calculated time. The specifics of the calculation
depends on the method of the prayer time calculation. This calculated
midnight is the time at which the time to pray the Isha prayers have
expired.
Calculated Midnight: The Islamic midnight.
Traditional Midnight: 12:00AM
Update logic for prayer times:
If the Calculated Midnight is before the traditional midnight then wait
until the traditional midnight to run the update. This way the day
will have changed over and we don't need to do any fancy calculations.
If the Calculated Midnight is after the traditional midnight, then wait
until after the calculated Midnight. We don't want to update the prayer
times too early or else the timings might be incorrect.
Example:
calculated midnight = 11:23PM (before traditional midnight)
Update time: 12:00AM
calculated midnight = 1:35AM (after traditional midnight)
update time: 1:36AM.
"""
_LOGGER.debug("Scheduling next update for Islamic prayer times")
now = dt_util.utcnow()
if now > midnight_dt:
next_update_at = midnight_dt + timedelta(days=1, minutes=1)
_LOGGER.debug(
"Midnight is after the day changes so schedule update for after Midnight the next day"
)
else:
_LOGGER.debug(
"Midnight is before the day changes so schedule update for the next start of day"
)
next_update_at = dt_util.start_of_local_day(now + timedelta(days=1))
_LOGGER.debug("Next update scheduled for: %s", next_update_at)
self.event_unsub = async_track_point_in_time(
self.hass, self.async_request_update, next_update_at
)
async def async_request_update(self, *_) -> None:
"""Request update from coordinator."""
await self.async_request_refresh()
async def _async_update_data(self) -> dict[str, datetime]:
"""Update sensors with new prayer times."""
try:
prayer_times = await self.hass.async_add_executor_job(
self.get_new_prayer_times
)
except (exceptions.InvalidResponseError, ConnError) as err:
async_call_later(self.hass, 60, self.async_request_update)
raise UpdateFailed from err
prayer_times_info: dict[str, datetime] = {}
for prayer, time in prayer_times.items():
if prayer_time := dt_util.parse_datetime(f"{dt_util.now().date()} {time}"):
prayer_times_info[prayer] = dt_util.as_utc(prayer_time)
self.async_schedule_future_update(prayer_times_info["Midnight"])
return prayer_times_info
@@ -1,51 +1,12 @@
"""Platform to retrieve Islamic prayer times information for Home Assistant."""
from datetime import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
import homeassistant.util.dt as dt_util
from . import IslamicPrayerDataUpdateCoordinator
from .const import DOMAIN, NAME
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="Fajr",
name="Fajr prayer",
),
SensorEntityDescription(
key="Sunrise",
name="Sunrise time",
),
SensorEntityDescription(
key="Dhuhr",
name="Dhuhr prayer",
),
SensorEntityDescription(
key="Asr",
name="Asr prayer",
),
SensorEntityDescription(
key="Maghrib",
name="Maghrib prayer",
),
SensorEntityDescription(
key="Isha",
name="Isha prayer",
),
SensorEntityDescription(
key="Midnight",
name="Midnight time",
),
)
from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES
async def async_setup_entry(
@@ -55,38 +16,46 @@ async def async_setup_entry(
) -> None:
"""Set up the Islamic prayer times sensor platform."""
coordinator: IslamicPrayerDataUpdateCoordinator = hass.data[DOMAIN]
client = hass.data[DOMAIN]
async_add_entities(
IslamicPrayerTimeSensor(coordinator, description)
for description in SENSOR_TYPES
)
entities = []
for sensor_type in SENSOR_TYPES:
entities.append(IslamicPrayerTimeSensor(sensor_type, client))
async_add_entities(entities, True)
class IslamicPrayerTimeSensor(
CoordinatorEntity[IslamicPrayerDataUpdateCoordinator], SensorEntity
):
class IslamicPrayerTimeSensor(SensorEntity):
"""Representation of an Islamic prayer time sensor."""
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_has_entity_name = True
_attr_icon = PRAYER_TIMES_ICON
_attr_should_poll = False
def __init__(
self,
coordinator: IslamicPrayerDataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
def __init__(self, sensor_type, client):
"""Initialize the Islamic prayer time sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = description.key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=NAME,
entry_type=DeviceEntryType.SERVICE,
)
self.sensor_type = sensor_type
self.client = client
@property
def native_value(self) -> datetime:
def name(self):
"""Return the name of the sensor."""
return f"{self.sensor_type} {SENSOR_TYPES[self.sensor_type]}"
@property
def unique_id(self):
"""Return the unique id of the entity."""
return self.sensor_type
@property
def native_value(self):
"""Return the state of the sensor."""
return self.coordinator.data[self.entity_description.key]
return self.client.prayer_times_info.get(self.sensor_type).astimezone(
dt_util.UTC
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(self.hass, DATA_UPDATED, self.async_write_ha_state)
)
+7 -49
View File
@@ -8,43 +8,16 @@ from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity
from .services import (
SERVICE_DELETE_USER_CODE_SCHEMA,
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE,
SERVICE_SET_USER_CODE_SCHEMA,
SERVICE_SET_ZWAVE_LOCK_USER_CODE,
)
VALUE_TO_STATE = {0: False, 100: True}
@callback
def async_setup_lock_services(hass: HomeAssistant) -> None:
"""Create lock-specific services for the ISY Integration."""
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_ZWAVE_LOCK_USER_CODE,
SERVICE_SET_USER_CODE_SCHEMA,
"async_set_zwave_lock_user_code",
)
platform.async_register_entity_service(
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE,
SERVICE_DELETE_USER_CODE_SCHEMA,
"async_delete_zwave_lock_user_code",
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@@ -59,7 +32,6 @@ async def async_setup_entry(
entities.append(ISYLockProgramEntity(name, status, actions))
async_add_entities(entities)
async_setup_lock_services(hass)
class ISYLockEntity(ISYNodeEntity, LockEntity):
@@ -75,26 +47,12 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Send the lock command to the ISY device."""
if not await self._node.secure_lock():
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
_LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs: Any) -> None:
"""Send the unlock command to the ISY device."""
if not await self._node.secure_unlock():
raise HomeAssistantError(f"Unable to unlock device {self._node.address}")
async def async_set_zwave_lock_user_code(self, user_num: int, code: int) -> None:
"""Set a user lock code for a Z-Wave Lock."""
if not await self._node.set_zwave_lock_code(user_num, code):
raise HomeAssistantError(
f"Could not set user code {user_num} for {self._node.address}"
)
async def async_delete_zwave_lock_user_code(self, user_num: int) -> None:
"""Delete a user lock code for a Z-Wave Lock."""
if not await self._node.delete_zwave_lock_code(user_num):
raise HomeAssistantError(
f"Could not delete user code {user_num} for {self._node.address}"
)
_LOGGER.error("Unable to lock device")
class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
@@ -108,9 +66,9 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
if not await self._actions.run_then():
raise HomeAssistantError(f"Unable to lock device {self._node.address}")
_LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
if not await self._actions.run_else():
raise HomeAssistantError(f"Unable to unlock device {self._node.address}")
_LOGGER.error("Unable to unlock device")
@@ -24,7 +24,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyisy"],
"requirements": ["pyisy==3.1.14"],
"requirements": ["pyisy==3.1.13"],
"ssdp": [
{
"manufacturer": "Universal Devices Inc.",
@@ -52,14 +52,8 @@ SERVICE_RENAME_NODE = "rename_node"
SERVICE_SET_ON_LEVEL = "set_on_level"
SERVICE_SET_RAMP_RATE = "set_ramp_rate"
# Services valid only for Z-Wave Locks
SERVICE_SET_ZWAVE_LOCK_USER_CODE = "set_zwave_lock_user_code"
SERVICE_DELETE_ZWAVE_LOCK_USER_CODE = "delete_zwave_lock_user_code"
CONF_PARAMETER = "parameter"
CONF_PARAMETERS = "parameters"
CONF_USER_NUM = "user_num"
CONF_CODE = "code"
CONF_VALUE = "value"
CONF_INIT = "init"
CONF_ISY = "isy"
@@ -135,13 +129,6 @@ SERVICE_SET_ZWAVE_PARAMETER_SCHEMA = {
vol.Required(CONF_SIZE): vol.All(vol.Coerce(int), vol.In(VALID_PARAMETER_SIZES)),
}
SERVICE_SET_USER_CODE_SCHEMA = {
vol.Required(CONF_USER_NUM): vol.Coerce(int),
vol.Required(CONF_CODE): vol.Coerce(int),
}
SERVICE_DELETE_USER_CODE_SCHEMA = {vol.Required(CONF_USER_NUM): vol.Coerce(int)}
SERVICE_SET_VARIABLE_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_ADDRESS, CONF_TYPE, CONF_NAME),
vol.Schema(
@@ -118,52 +118,6 @@ set_zwave_parameter:
- "1"
- "2"
- "4"
set_zwave_lock_user_code:
name: Set Z-Wave Lock User Code
description: >-
Set a Z-Wave Lock User Code via the ISY.
target:
entity:
integration: isy994
domain: lock
fields:
user_num:
name: User Number
description: The user slot number on the lock
required: true
example: 8
selector:
number:
min: 1
max: 255
code:
name: Code
description: The code to set for the user.
required: true
example: 33491663
selector:
number:
min: 1
max: 99999999
mode: box
delete_zwave_lock_user_code:
name: Delete Z-Wave Lock User Code
description: >-
Delete a Z-Wave Lock User Code via the ISY.
target:
entity:
integration: isy994
domain: lock
fields:
user_num:
name: User Number
description: The user slot number on the lock
required: true
example: 8
selector:
number:
min: 1
max: 255
rename_node:
name: Rename Node on ISY
description: >-
+1 -1
View File
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["xknx"],
"quality_scale": "platinum",
"requirements": ["xknx==2.6.0"]
"requirements": ["xknx==2.5.0"]
}
+11 -4
View File
@@ -17,9 +17,10 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later, async_track_time_interval
@@ -166,9 +167,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
We do not want the discovery task to block startup.
"""
hass.async_create_background_task(
discovery_manager.async_discovery(), "lifx-discovery"
)
task = asyncio.create_task(discovery_manager.async_discovery())
@callback
def _async_stop(_: Event) -> None:
if not task.done():
task.cancel()
# Task must be shut down when home assistant is closing
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
# Let the system settle a bit before starting discovery
# to reduce the risk we miss devices because the event
+2 -2
View File
@@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
system.on_connected_changed(handle_connected_changed)
async def handle_stop(event: Event) -> None:
async def handle_stop(event) -> None:
await system.close()
entry.async_on_unload(
@@ -76,7 +76,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult:
async def async_step_import(self, import_data):
"""Import litejet config from configuration.yaml."""
return self.async_create_entry(title=import_data[CONF_PORT], data=import_data)
+6 -10
View File
@@ -2,8 +2,6 @@
from __future__ import annotations
from collections.abc import Callable
from datetime import datetime
from typing import cast
from pylitejet import LiteJet
import voluptuous as vol
@@ -44,7 +42,7 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Listen for events based on configuration."""
trigger_data = trigger_info["trigger_data"]
number = cast(int, config[CONF_NUMBER])
number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN)
held_less_than = config.get(CONF_HELD_LESS_THAN)
pressed_time = None
@@ -52,7 +50,7 @@ async def async_attach_trigger(
job = HassJob(action)
@callback
def call_action() -> None:
def call_action():
"""Call action with right context."""
hass.async_run_hass_job(
job,
@@ -74,11 +72,11 @@ async def async_attach_trigger(
# neither: trigger on pressed
@callback
def pressed_more_than_satisfied(now: datetime) -> None:
def pressed_more_than_satisfied(now):
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
call_action()
def pressed() -> None:
def pressed():
"""Handle the press of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
@@ -90,12 +88,10 @@ async def async_attach_trigger(
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
)
def released() -> None:
def released():
"""Handle the release of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
if pressed_time is None:
return
if cancel_pressed_more_than is not None:
cancel_pressed_more_than()
cancel_pressed_more_than = None
@@ -114,7 +110,7 @@ async def async_attach_trigger(
system.on_switch_released(number, released)
@callback
def async_remove() -> None:
def async_remove():
"""Remove all subscriptions used for this trigger."""
system.unsubscribe(pressed)
system.unsubscribe(released)
+2 -3
View File
@@ -8,15 +8,14 @@ from aiolivisi import AioLivisi
from homeassistant import core
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, device_registry as dr
from .const import DOMAIN
from .const import DOMAIN, SWITCH_PLATFORM
from .coordinator import LivisiDataUpdateCoordinator
PLATFORMS: Final = [Platform.CLIMATE, Platform.SWITCH]
PLATFORMS: Final = [SWITCH_PLATFORM]
async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool:
-212
View File
@@ -1,212 +0,0 @@
"""Code to handle a Livisi Virtual Climate Control."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aiolivisi.const import CAPABILITY_MAP
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DOMAIN,
LIVISI_REACHABILITY_CHANGE,
LIVISI_STATE_CHANGE,
LOGGER,
MAX_TEMPERATURE,
MIN_TEMPERATURE,
VRCC_DEVICE_TYPE,
)
from .coordinator import LivisiDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up climate device."""
coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
@callback
def handle_coordinator_update() -> None:
"""Add climate device."""
shc_devices: list[dict[str, Any]] = coordinator.data
entities: list[ClimateEntity] = []
for device in shc_devices:
if (
device["type"] == VRCC_DEVICE_TYPE
and device["id"] not in coordinator.devices
):
livisi_climate: ClimateEntity = create_entity(
config_entry, device, coordinator
)
LOGGER.debug("Include device type: %s", device.get("type"))
coordinator.devices.add(device["id"])
entities.append(livisi_climate)
async_add_entities(entities)
config_entry.async_on_unload(
coordinator.async_add_listener(handle_coordinator_update)
)
def create_entity(
config_entry: ConfigEntry,
device: dict[str, Any],
coordinator: LivisiDataUpdateCoordinator,
) -> ClimateEntity:
"""Create Climate Entity."""
capabilities: Mapping[str, Any] = device[CAPABILITY_MAP]
room_id: str = device["location"]
room_name: str = coordinator.rooms[room_id]
livisi_climate = LivisiClimate(
config_entry,
coordinator,
unique_id=device["id"],
manufacturer=device["manufacturer"],
device_type=device["type"],
target_temperature_capability=capabilities["RoomSetpoint"],
temperature_capability=capabilities["RoomTemperature"],
humidity_capability=capabilities["RoomHumidity"],
room=room_name,
)
return livisi_climate
class LivisiClimate(CoordinatorEntity[LivisiDataUpdateCoordinator], ClimateEntity):
"""Represents the Livisi Climate."""
_attr_hvac_modes = [HVACMode.HEAT]
_attr_hvac_mode = HVACMode.HEAT
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_target_temperature_high = MAX_TEMPERATURE
_attr_target_temperature_low = MIN_TEMPERATURE
def __init__(
self,
config_entry: ConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
unique_id: str,
manufacturer: str,
device_type: str,
target_temperature_capability: str,
temperature_capability: str,
humidity_capability: str,
room: str,
) -> None:
"""Initialize the Livisi Climate."""
self.config_entry = config_entry
self._attr_unique_id = unique_id
self._target_temperature_capability = target_temperature_capability
self._temperature_capability = temperature_capability
self._humidity_capability = humidity_capability
self.aio_livisi = coordinator.aiolivisi
self._attr_available = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
manufacturer=manufacturer,
model=device_type,
name=room,
suggested_area=room,
via_device=(DOMAIN, config_entry.entry_id),
)
super().__init__(coordinator)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
response = await self.aio_livisi.async_vrcc_set_temperature(
self._target_temperature_capability,
kwargs.get(ATTR_TEMPERATURE),
self.coordinator.is_avatar,
)
if response is None:
self._attr_available = False
raise HomeAssistantError(f"Failed to turn off {self._attr_name}")
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Do nothing as LIVISI devices do not support changing the hvac mode."""
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
target_temperature = await self.coordinator.async_get_vrcc_target_temperature(
self._target_temperature_capability
)
temperature = await self.coordinator.async_get_vrcc_temperature(
self._temperature_capability
)
humidity = await self.coordinator.async_get_vrcc_humidity(
self._humidity_capability
)
if temperature is None:
self._attr_current_temperature = None
self._attr_available = False
else:
self._attr_target_temperature = target_temperature
self._attr_current_temperature = temperature
self._attr_current_humidity = humidity
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._target_temperature_capability}",
self.update_target_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._temperature_capability}",
self.update_temperature,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_STATE_CHANGE}_{self._humidity_capability}",
self.update_humidity,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}",
self.update_reachability,
)
)
@callback
def update_target_temperature(self, target_temperature: float) -> None:
"""Update the target temperature of the climate device."""
self._attr_target_temperature = target_temperature
self.async_write_ha_state()
@callback
def update_temperature(self, current_temperature: float) -> None:
"""Update the current temperature of the climate device."""
self._attr_current_temperature = current_temperature
self.async_write_ha_state()
@callback
def update_humidity(self, humidity: int) -> None:
"""Update the humidity temperature of the climate device."""
self._attr_current_humidity = humidity
self.async_write_ha_state()
@callback
def update_reachability(self, is_reachable: bool) -> None:
"""Update the reachability of the climate device."""
self._attr_available = is_reachable
self.async_write_ha_state()
+2 -5
View File
@@ -7,15 +7,12 @@ DOMAIN = "livisi"
CONF_HOST = "host"
CONF_PASSWORD: Final = "password"
AVATAR = "Avatar"
AVATAR_PORT: Final = 9090
CLASSIC_PORT: Final = 8080
DEVICE_POLLING_DELAY: Final = 60
LIVISI_STATE_CHANGE: Final = "livisi_state_change"
LIVISI_REACHABILITY_CHANGE: Final = "livisi_reachability_change"
PSS_DEVICE_TYPE: Final = "PSS"
VRCC_DEVICE_TYPE: Final = "VRCC"
SWITCH_PLATFORM: Final = "switch"
MAX_TEMPERATURE: Final = 30.0
MIN_TEMPERATURE: Final = 6.0
PSS_DEVICE_TYPE: Final = "PSS"
+3 -39
View File
@@ -13,7 +13,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
AVATAR,
AVATAR_PORT,
CLASSIC_PORT,
CONF_HOST,
@@ -70,14 +69,14 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
livisi_connection_data=livisi_connection_data
)
controller_data = await self.aiolivisi.async_get_controller()
if (controller_type := controller_data["controllerType"]) == AVATAR:
if controller_data["controllerType"] == "Avatar":
self.port = AVATAR_PORT
self.is_avatar = True
else:
self.port = CLASSIC_PORT
self.is_avatar = False
self.controller_type = controller_type
self.serial_number = controller_data["serialNumber"]
self.controller_type = controller_data["controllerType"]
async def async_get_devices(self) -> list[dict[str, Any]]:
"""Set the discovered devices list."""
@@ -85,7 +84,7 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
async def async_get_pss_state(self, capability: str) -> bool | None:
"""Set the PSS state."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
response: dict[str, Any] = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
@@ -93,35 +92,6 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
on_state = response["onState"]
return on_state["value"]
async def async_get_vrcc_target_temperature(self, capability: str) -> float | None:
"""Get the target temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
if self.is_avatar:
return response["setpointTemperature"]["value"]
return response["pointTemperature"]["value"]
async def async_get_vrcc_temperature(self, capability: str) -> float | None:
"""Get the temperature of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["temperature"]["value"]
async def async_get_vrcc_humidity(self, capability: str) -> int | None:
"""Get the humidity of the climate device."""
response: dict[str, Any] | None = await self.aiolivisi.async_get_device_state(
capability[1:]
)
if response is None:
return None
return response["humidity"]["value"]
async def async_set_all_rooms(self) -> None:
"""Set the room list."""
response: list[dict[str, Any]] = await self.aiolivisi.async_get_all_rooms()
@@ -138,12 +108,6 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.onState,
)
if event_data.vrccData is not None:
async_dispatcher_send(
self.hass,
f"{LIVISI_STATE_CHANGE}_{event_data.source}",
event_data.vrccData,
)
if event_data.isReachable is not None:
async_dispatcher_send(
self.hass,
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/livisi",
"iot_class": "local_polling",
"requirements": ["aiolivisi==0.0.16"]
"requirements": ["aiolivisi==0.0.15"]
}
@@ -1,18 +1 @@
"""The Obihai integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -1,73 +0,0 @@
"""Config flow to configure the Obihai integration."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .connectivity import validate_auth
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(
CONF_USERNAME,
default=DEFAULT_USERNAME,
): str,
vol.Optional(
CONF_PASSWORD,
default=DEFAULT_PASSWORD,
): str,
}
)
class ObihaiFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Obihai."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
if await self.hass.async_add_executor_job(
validate_auth,
user_input[CONF_HOST],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
):
return self.async_create_entry(
title=user_input[CONF_HOST],
data=user_input,
)
errors["base"] = "cannot_connect"
data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input)
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=data_schema,
)
# DEPRECATED
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Handle a flow initialized by importing a config."""
self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]})
return self.async_create_entry(
title=config.get(CONF_NAME, config[CONF_HOST]),
data={
CONF_HOST: config[CONF_HOST],
CONF_PASSWORD: config[CONF_PASSWORD],
CONF_USERNAME: config[CONF_USERNAME],
},
)
@@ -1,67 +0,0 @@
"""Support for Obihai Connectivity."""
from __future__ import annotations
from pyobihai import PyObihai
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, LOGGER
def get_pyobihai(
host: str,
username: str,
password: str,
) -> PyObihai:
"""Retrieve an authenticated PyObihai."""
return PyObihai(host, username, password)
def validate_auth(
host: str,
username: str,
password: str,
) -> bool:
"""Test if the given setting works as expected."""
obi = get_pyobihai(host, username, password)
login = obi.check_account()
if not login:
LOGGER.debug("Invalid credentials")
return False
return True
class ObihaiConnection:
"""Contains a list of Obihai Sensors."""
def __init__(
self,
host: str,
username: str = DEFAULT_USERNAME,
password: str = DEFAULT_PASSWORD,
) -> None:
"""Store configuration."""
self.sensors: list = []
self.host = host
self.username = username
self.password = password
self.serial: list = []
self.services: list = []
self.line_services: list = []
self.call_direction: list = []
self.pyobihai: PyObihai = None
def update(self) -> bool:
"""Validate connection and retrieve a list of sensors."""
if not self.pyobihai:
self.pyobihai = get_pyobihai(self.host, self.username, self.password)
if not self.pyobihai.check_account():
return False
self.serial = self.pyobihai.get_device_serial()
self.services = self.pyobihai.get_state()
self.line_services = self.pyobihai.get_line_state()
self.call_direction = self.pyobihai.get_call_direction()
return True
-15
View File
@@ -1,15 +0,0 @@
"""Constants for the Obihai integration."""
import logging
from typing import Final
from homeassistant.const import Platform
DOMAIN: Final = "obihai"
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
OBIHAI = "Obihai"
LOGGER = logging.getLogger(__package__)
PLATFORMS: Final = [Platform.SENSOR]
@@ -1,8 +1,7 @@
{
"domain": "obihai",
"name": "Obihai",
"codeowners": ["@dshokouhi", "@ejpenney"],
"config_flow": true,
"codeowners": ["@dshokouhi"],
"documentation": "https://www.home-assistant.io/integrations/obihai",
"iot_class": "local_polling",
"loggers": ["pyobihai"],
+36 -49
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
from datetime import timedelta
import logging
from pyobihai import PyObihai
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -10,19 +12,20 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .connectivity import ObihaiConnection
from .const import DEFAULT_PASSWORD, DEFAULT_USERNAME, DOMAIN, OBIHAI
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
OBIHAI = "Obihai"
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@@ -32,58 +35,46 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
# DEPRECATED
async def async_setup_platform(
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Obihai sensor platform."""
issue_registry.async_create_issue(
hass,
DOMAIN,
"manual_migration",
breaks_in_ha_version="2023.6.0",
is_fixable=False,
severity=issue_registry.IssueSeverity.ERROR,
translation_key="manual_migration",
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
host = config[CONF_HOST]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Obihai sensor entries."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
host = entry.data[CONF_HOST]
requester = ObihaiConnection(host, username, password)
await hass.async_add_executor_job(requester.update)
sensors = []
for key in requester.services:
sensors.append(ObihaiServiceSensors(requester.pyobihai, requester.serial, key))
if requester.line_services is not None:
for key in requester.line_services:
sensors.append(
ObihaiServiceSensors(requester.pyobihai, requester.serial, key)
)
pyobihai = PyObihai(host, username, password)
for key in requester.call_direction:
sensors.append(ObihaiServiceSensors(requester.pyobihai, requester.serial, key))
login = pyobihai.check_account()
if not login:
_LOGGER.error("Invalid credentials")
return
async_add_entities(sensors, update_before_add=True)
serial = pyobihai.get_device_serial()
services = pyobihai.get_state()
line_services = pyobihai.get_line_state()
call_direction = pyobihai.get_call_direction()
for key in services:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
if line_services is not None:
for key in line_services:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
for key in call_direction:
sensors.append(ObihaiServiceSensors(pyobihai, serial, key))
add_entities(sensors)
class ObihaiServiceSensors(SensorEntity):
@@ -157,10 +148,6 @@ class ObihaiServiceSensors(SensorEntity):
def update(self) -> None:
"""Update the sensor."""
if not self._pyobihai.check_account():
self._state = None
return
services = self._pyobihai.get_state()
if self._service_name in services:
@@ -1,25 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"issues": {
"manual_migration": {
"title": "Manual migration required for Obihai",
"description": "Configuration of the Obihai platform in YAML is deprecated and will be removed in Home Assistant 2023.6; Your existing configuration has been imported into the UI automatically and can be safely removed from your configuration.yaml file."
}
}
}
@@ -3,7 +3,6 @@ import asyncio
from datetime import date, datetime
import logging
import async_timeout
import pyotgw
import pyotgw.vars as gw_vars
from serial import SerialException
@@ -113,8 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
config_entry.add_update_listener(options_updated)
try:
async with async_timeout.timeout(CONNECTION_TIMEOUT):
await gateway.connect_and_subscribe()
await asyncio.wait_for(
gateway.connect_and_subscribe(),
timeout=CONNECTION_TIMEOUT,
)
except (asyncio.TimeoutError, ConnectionError, SerialException) as ex:
await gateway.cleanup()
raise ConfigEntryNotReady(
@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import async_timeout
import pyotgw
from pyotgw import vars as gw_vars
from serial import SerialException
@@ -69,8 +68,10 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
try:
async with async_timeout.timeout(CONNECTION_TIMEOUT):
await test_connection()
await asyncio.wait_for(
test_connection(),
timeout=CONNECTION_TIMEOUT,
)
except asyncio.TimeoutError:
return self._show_form({"base": "timeout_connect"})
except (ConnectionError, SerialException):
-12
View File
@@ -46,23 +46,11 @@ class OTBRData:
url: str
api: python_otbr_api.OTBR
@_handle_otbr_error
async def set_enabled(self, enabled: bool) -> None:
"""Enable or disable the router."""
return await self.api.set_enabled(enabled)
@_handle_otbr_error
async def get_active_dataset_tlvs(self) -> bytes | None:
"""Get current active operational dataset in TLVS format, or None."""
return await self.api.get_active_dataset_tlvs()
@_handle_otbr_error
async def create_active_dataset(
self, dataset: python_otbr_api.OperationalDataSet
) -> None:
"""Create an active operational dataset."""
return await self.api.create_active_dataset(dataset)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Open Thread Border Router component."""
@@ -1,8 +1,6 @@
"""Websocket API for OTBR."""
from typing import TYPE_CHECKING
import python_otbr_api
from homeassistant.components.websocket_api import (
ActiveConnection,
async_register_command,
@@ -22,7 +20,6 @@ if TYPE_CHECKING:
def async_setup(hass: HomeAssistant) -> None:
"""Set up the OTBR Websocket API."""
async_register_command(hass, websocket_info)
async_register_command(hass, websocket_create_network)
@websocket_command(
@@ -54,42 +51,3 @@ async def websocket_info(
"active_dataset_tlvs": dataset.hex() if dataset else None,
},
)
@websocket_command(
{
"type": "otbr/create_network",
}
)
@async_response
async def websocket_create_network(
hass: HomeAssistant, connection: ActiveConnection, msg: dict
) -> None:
"""Create a new Thread network."""
if DOMAIN not in hass.data:
connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
return
data: OTBRData = hass.data[DOMAIN]
try:
await data.set_enabled(False)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
try:
await data.create_active_dataset(
python_otbr_api.OperationalDataSet(network_name="home-assistant")
)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "create_active_dataset_failed", str(exc))
return
try:
await data.set_enabled(True)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
connection.send_result(msg["id"])
@@ -10,7 +10,6 @@ from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl
from .somfy_thermostat import SomfyThermostat
from .valve_heating_temperature_interface import ValveHeatingTemperatureInterface
WIDGET_TO_CLIMATE_ENTITY = {
UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater,
@@ -22,5 +21,4 @@ WIDGET_TO_CLIMATE_ENTITY = {
UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: AtlanticPassAPCHeatingZone,
UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl,
UIWidget.SOMFY_THERMOSTAT: SomfyThermostat,
UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: ValveHeatingTemperatureInterface,
}
@@ -15,7 +15,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import UnitOfTemperature
from ..const import DOMAIN
from ..entity import OverkizEntity
PRESET_COMFORT1 = "comfort-1"
@@ -48,7 +47,6 @@ class AtlanticElectricalHeater(OverkizEntity, ClimateEntity):
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
_attr_supported_features = ClimateEntityFeature.PRESET_MODE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
@property
def hvac_mode(self) -> HVACMode:
@@ -16,7 +16,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -71,7 +70,6 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
@@ -14,7 +14,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -44,7 +43,6 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity):
_attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ]
_attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
@@ -13,7 +13,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -50,7 +49,6 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity):
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.FAN_MODE
)
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
@@ -17,7 +17,6 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
@@ -79,7 +78,6 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
@@ -15,17 +15,19 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
PRESET_FREEZE = "freeze"
PRESET_NIGHT = "night"
STATE_DEROGATION_ACTIVE = "active"
STATE_DEROGATION_INACTIVE = "inactive"
OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = {
OverkizCommandParam.ACTIVE: HVACMode.HEAT,
OverkizCommandParam.INACTIVE: HVACMode.AUTO,
STATE_DEROGATION_ACTIVE: HVACMode.HEAT,
STATE_DEROGATION_INACTIVE: HVACMode.AUTO,
}
HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()}
@@ -58,8 +60,6 @@ class SomfyThermostat(OverkizEntity, ClimateEntity):
)
_attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ]
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
_attr_translation_key = DOMAIN
# Both min and max temp values have been retrieved from the Somfy Application.
_attr_min_temp = 15.0
_attr_max_temp = 26.0
@@ -1,137 +0,0 @@
"""Support for ValveHeatingTemperatureInterface."""
from __future__ import annotations
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
UnitOfTemperature,
)
from homeassistant.const import ATTR_TEMPERATURE
from ..const import DOMAIN
from ..coordinator import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
PRESET_MANUAL = "manual"
PRESET_FROST_PROTECTION = "frost_protection"
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
OverkizCommandParam.OPEN: HVACAction.HEATING,
OverkizCommandParam.CLOSED: HVACAction.IDLE,
}
OVERKIZ_TO_PRESET_MODE: dict[str, str] = {
OverkizCommandParam.GEOFENCING_MODE: PRESET_NONE,
OverkizCommandParam.SUDDEN_DROP_MODE: PRESET_NONE,
OverkizCommandParam.AWAY: PRESET_AWAY,
OverkizCommandParam.COMFORT: PRESET_COMFORT,
OverkizCommandParam.ECO: PRESET_ECO,
OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION,
OverkizCommandParam.MANUAL: PRESET_MANUAL,
}
PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()}
TEMPERATURE_SENSOR_DEVICE_INDEX = 2
class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
"""Representation of Valve Heating Temperature Interface device."""
_attr_hvac_mode = HVACMode.HEAT
_attr_hvac_modes = [HVACMode.HEAT]
_attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ]
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = DOMAIN
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
) -> None:
"""Init method."""
super().__init__(device_url, coordinator)
self.temperature_device = self.executor.linked_device(
TEMPERATURE_SENSOR_DEVICE_INDEX
)
self._attr_min_temp = cast(
float, self.executor.select_state(OverkizState.CORE_MIN_SETPOINT)
)
self._attr_max_temp = cast(
float, self.executor.select_state(OverkizState.CORE_MAX_SETPOINT)
)
@property
def hvac_action(self) -> str:
"""Return the current running hvac operation."""
return OVERKIZ_TO_HVAC_ACTION[
cast(str, self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE))
]
@property
def target_temperature(self) -> float:
"""Return the temperature."""
return cast(
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]:
return temperature.value_as_float
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new temperature."""
temperature = kwargs[ATTR_TEMPERATURE]
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
float(temperature),
OverkizCommandParam.FURTHER_NOTICE,
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
return
@property
def preset_mode(self) -> str:
"""Return the current preset mode, e.g., home, away, temp."""
return OVERKIZ_TO_PRESET_MODE[
cast(
str, self.executor.select_state(OverkizState.IO_DEROGATION_HEATING_MODE)
)
]
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
# If we want to switch to manual mode via a preset, we need to pass in a temperature
# Manual mode will be on automatically if an user sets a temperature
if preset_mode == PRESET_MANUAL:
if current_temperature := self.current_temperature:
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
current_temperature,
OverkizCommandParam.FURTHER_NOTICE,
)
else:
await self.executor.async_execute_command(
OverkizCommand.SET_DEROGATION,
PRESET_MODE_TO_OVERKIZ[preset_mode],
OverkizCommandParam.FURTHER_NOTICE,
)
@@ -83,7 +83,6 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = {
UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported)
UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported)
UIWidget.TSKALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported)
UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
}
# Map Overkiz camelCase to Home Assistant snake_case for translation
@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.7.6"],
"requirements": ["pyoverkiz==1.7.3"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",
@@ -28,34 +28,6 @@
}
},
"entity": {
"climate": {
"overkiz": {
"state_attributes": {
"preset_mode": {
"state": {
"auto": "Auto",
"comfort-1": "Comfort 1",
"comfort-2": "Comfort 2",
"drying": "Drying",
"external": "External",
"freeze": "Freeze",
"frost_protection": "Frost protection",
"manual": "Manual",
"night": "Night",
"prog": "Prog"
}
},
"fan_mode": {
"state": {
"away": "Away",
"bypass_boost": "Bypass boost",
"home_boost": "Home boost",
"kitchen_boost": "Kitchen boost"
}
}
}
}
},
"select": {
"open_closed_pedestrian": {
"state": {
@@ -8,7 +8,6 @@ import logging
import re
from typing import Any
import async_timeout
from icmplib import NameLookupError, async_ping
import voluptuous as vol
@@ -231,8 +230,9 @@ class PingDataSubProcess(PingData):
close_fds=False, # required for posix_spawn
)
try:
async with async_timeout.timeout(self._count + PING_TIMEOUT):
out_data, out_error = await pinger.communicate()
out_data, out_error = await asyncio.wait_for(
pinger.communicate(), self._count + PING_TIMEOUT
)
if out_data:
_LOGGER.debug(
+3 -4
View File
@@ -17,7 +17,6 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
@@ -304,9 +303,9 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="gas_consumed_interval",
name="Gas consumed interval",
icon="mdi:meter-gas",
native_unit_of_measurement=f"{UnitOfVolume.CUBIC_METERS}/{UnitOfTime.HOURS}",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="gas_consumed_cumulative",
@@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client
from .const import CONF_COUNTRY, DOMAIN
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
PLATFORMS = [Platform.ALARM_CONTROL_PANEL]
_LOGGER = logging.getLogger(__name__)
@@ -15,7 +15,6 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN
@@ -60,14 +59,6 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity):
self._attr_name = f"contract {self.contract}"
self._attr_unique_id = self.contract
self._attr_device_info = DeviceInfo(
name="Prosegur Alarm",
manufacturer="Prosegur",
model="smart",
identifiers={(DOMAIN, self.contract)},
configuration_url="https://smart.prosegur.com",
)
async def async_update(self) -> None:
"""Update alarm status."""
@@ -1,97 +0,0 @@
"""Support for Prosegur cameras."""
from __future__ import annotations
import logging
from pyprosegur.auth import Auth
from pyprosegur.exceptions import ProsegurException
from pyprosegur.installation import Camera as InstallationCamera, Installation
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from . import DOMAIN
from .const import SERVICE_REQUEST_IMAGE
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Prosegur camera platform."""
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_REQUEST_IMAGE,
{},
"async_request_image",
)
_installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id])
async_add_entities(
[
ProsegurCamera(_installation, camera, hass.data[DOMAIN][entry.entry_id])
for camera in _installation.cameras
],
update_before_add=True,
)
class ProsegurCamera(Camera):
"""Representation of a Smart Prosegur Camera."""
def __init__(
self, installation: Installation, camera: InstallationCamera, auth: Auth
) -> None:
"""Initialize Prosegur Camera component."""
Camera.__init__(self)
self._installation = installation
self._camera = camera
self._auth = auth
self._attr_name = camera.description
self._attr_unique_id = f"{self._installation.contract} {camera.id}"
self._attr_device_info = DeviceInfo(
name=self._camera.description,
manufacturer="Prosegur",
model="smart camera",
identifiers={(DOMAIN, self._installation.contract)},
configuration_url="https://smart.prosegur.com",
)
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
try:
_LOGGER.debug("Get image for %s", self._camera.description)
return await self._installation.get_image(self._auth, self._camera.id)
except ProsegurException as err:
_LOGGER.error("Image %s doesn't exist: %s", self._camera.description, err)
return None
async def async_request_image(self):
"""Request new image from the camera."""
try:
_LOGGER.debug("Request image for %s", self._camera.description)
await self._installation.request_image(self._auth, self._camera.id)
except ProsegurException as err:
_LOGGER.error(
"Could not request image from camera %s: %s",
self._camera.description,
err,
)
@@ -3,5 +3,3 @@
DOMAIN = "prosegur"
CONF_COUNTRY = "country"
SERVICE_REQUEST_IMAGE = "request_image"
@@ -1,29 +0,0 @@
"""Diagnostics support for Prosegur."""
from __future__ import annotations
from typing import Any
from pyprosegur.installation import Installation
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
TO_REDACT = {"description", "latitude", "longitude", "contractId", "address"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
installation = await Installation.retrieve(hass.data[DOMAIN][entry.entry_id])
activity = await installation.activity(hass.data[DOMAIN][entry.entry_id])
return {
"installation": async_redact_data(installation.data, TO_REDACT),
"activity": activity,
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/prosegur",
"iot_class": "cloud_polling",
"loggers": ["pyprosegur"],
"requirements": ["pyprosegur==0.0.8"]
"requirements": ["pyprosegur==0.0.5"]
}
@@ -1,7 +0,0 @@
request_image:
name: Request Camera image
description: Request a new image from a Prosegur Camera
target:
entity:
domain: camera
integration: prosegur
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONF_NAME,
@@ -46,14 +45,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
name="Down Speed",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_TYPE_UPLOAD_SPEED,
name="Up Speed",
device_class=SensorDeviceClass.DATA_RATE,
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
)
@@ -6,7 +6,6 @@ from collections.abc import Coroutine, Sequence
from datetime import datetime, timedelta
from typing import Any
import async_timeout
from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester
from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable
from async_upnp_client.client_factory import UpnpFactory
@@ -251,8 +250,7 @@ class SamsungTVDevice(MediaPlayerEntity):
# enter it unless we have to (Python 3.11 will have zero cost try)
return
try:
async with async_timeout.timeout(APP_LIST_DELAY):
await self._app_list_event.wait()
await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY)
except asyncio.TimeoutError as err:
# No need to try again
self._app_list_event.set()
+10 -33
View File
@@ -196,30 +196,19 @@ class SensorEntity(Entity):
if self.unique_id is None or self.device_class is None:
return
registry = er.async_get(self.hass)
# Bail out if the entity is not yet registered
if not (
entity_id := registry.async_get_entity_id(
platform.domain, platform.platform_name, self.unique_id
)
):
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._sensor_option_unit_of_measurement = self._get_initial_suggested_unit()
return
registry_entry = registry.async_get(entity_id)
assert registry_entry
# Prime _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self.registry_entry = registry_entry
self._async_read_entity_options()
# If the sensor has 'unit_of_measurement' in its sensor options, the user has
# overridden the unit.
# If the sensor has 'sensor.private' in its entity options, it already has a
# suggested_unit.
# If the sensor has 'sensor.private' in its entity options, it was added after
# automatic unit conversion was implemented.
registry_unit = registry_entry.unit_of_measurement
if (
(
@@ -241,14 +230,11 @@ class SensorEntity(Entity):
# Set suggested_unit_of_measurement to the old unit to enable automatic
# conversion
self.registry_entry = registry.async_update_entity_options(
registry.async_update_entity_options(
entity_id,
f"{DOMAIN}.private",
{"suggested_unit_of_measurement": registry_unit},
)
# Update _sensor_option_unit_of_measurement to ensure the correct unit
# is stored in the entity registry.
self._async_read_entity_options()
async def async_internal_added_to_hass(self) -> None:
"""Call when the sensor entity is added to hass."""
@@ -319,8 +305,12 @@ class SensorEntity(Entity):
return None
def _get_initial_suggested_unit(self) -> str | UndefinedType:
"""Return the initial unit."""
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
# Unit suggested by the integration
suggested_unit_of_measurement = self.suggested_unit_of_measurement
@@ -331,19 +321,6 @@ class SensorEntity(Entity):
)
if suggested_unit_of_measurement is None:
return UNDEFINED
return suggested_unit_of_measurement
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options.
These will be stored in the entity registry the first time the entity is seen,
and then never updated.
"""
suggested_unit_of_measurement = self._get_initial_suggested_unit()
if suggested_unit_of_measurement is UNDEFINED:
return None
return {
@@ -439,7 +416,7 @@ class SensorEntity(Entity):
return self._sensor_option_unit_of_measurement
# Second priority, for non registered entities: unit suggested by integration
if not self.unique_id and self.suggested_unit_of_measurement:
if not self.registry_entry and self.suggested_unit_of_measurement:
return self.suggested_unit_of_measurement
# Third priority: Legacy temperature conversion, which applies
@@ -4,7 +4,6 @@ from http import HTTPStatus
import logging
from typing import TYPE_CHECKING
import async_timeout
from pysqueezebox import Server, async_discover
import voluptuous as vol
@@ -131,8 +130,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# no host specified, see if we can discover an unconfigured LMS server
try:
async with async_timeout.timeout(TIMEOUT):
await self._discover()
await asyncio.wait_for(self._discover(), timeout=TIMEOUT)
return await self.async_step_edit()
except asyncio.TimeoutError:
errors["base"] = "no_server_found"
@@ -2,7 +2,7 @@
"domain": "statistics",
"name": "Statistics",
"after_dependencies": ["recorder"],
"codeowners": ["@ThomDietrich"],
"codeowners": ["@fabaff", "@ThomDietrich"],
"documentation": "https://www.home-assistant.io/integrations/statistics",
"iot_class": "local_polling",
"quality_scale": "internal"
+7 -14
View File
@@ -13,23 +13,16 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_import(
self, import_data: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Set up because the user has border routers."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
async def async_step_import(
self, import_data: dict[str, str] | None = None
) -> FlowResult:
"""Set up by import from async_setup."""
await self._async_handle_discovery_without_unique_id()
return self.async_create_entry(title="Thread", data={})
+16 -16
View File
@@ -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 new UniFi entity on event."""
async_add_unifi_entity([obj_id])
"""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)
api_handler.subscribe(async_create_entity, ItemEvent.ADDED)
@@ -45,9 +45,7 @@ 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,
@@ -80,7 +78,6 @@ 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,
@@ -96,7 +93,6 @@ 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,
@@ -123,7 +119,6 @@ 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,
@@ -141,7 +136,17 @@ async def async_setup_platform(
"""Set up the universal media players."""
await async_setup_reload_service(hass, "universal", ["media_player"])
player = UniversalMediaPlayer(hass, config)
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),
)
async_add_entities([player])
@@ -153,25 +158,30 @@ class UniversalMediaPlayer(MediaPlayerEntity):
def __init__(
self,
hass,
config,
name,
children,
commands,
attributes,
unique_id=None,
device_class=None,
state_template=None,
):
"""Initialize the Universal media device."""
self.hass = hass
self._name = config.get(CONF_NAME)
self._children = config.get(CONF_CHILDREN)
self._cmds = config.get(CONF_COMMANDS)
self._name = name
self._children = children
self._cmds = commands
self._attrs = {}
for key, val in config.get(CONF_ATTRS).items():
for key, val in attributes.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 = 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)
self._state_template = state_template
self._device_class = device_class
self._attr_unique_id = unique_id
async def async_added_to_hass(self) -> None:
"""Subscribe to children and template state changes."""
@@ -292,11 +302,6 @@ 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.
@@ -492,9 +497,6 @@ 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
@@ -626,20 +628,6 @@ 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
+1 -3
View File
@@ -4,7 +4,6 @@ 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
@@ -71,8 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
async with async_timeout.timeout(10):
await device_discovered_event.wait()
await asyncio.wait_for(device_discovered_event.wait(), timeout=10)
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err
finally:
+11 -9
View File
@@ -5,28 +5,24 @@ from typing import cast
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
TriggerProtocol,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .triggers import turn_on
from .triggers import TriggersPlatformModule, turn_on
TRIGGERS = {
"turn_on": turn_on,
}
def _get_trigger_platform(config: ConfigType) -> TriggerProtocol:
def _get_trigger_platform(config: ConfigType) -> TriggersPlatformModule:
"""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(TriggerProtocol, TRIGGERS[platform_split[1]])
return cast(TriggersPlatformModule, TRIGGERS[platform_split[1]])
async def async_validate_trigger_config(
@@ -45,4 +41,10 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach trigger of specified platform."""
platform = _get_trigger_platform(config)
return await platform.async_attach_trigger(hass, config, action, trigger_info)
assert hasattr(platform, "async_attach_trigger")
return cast(
CALLBACK_TYPE,
await getattr(platform, "async_attach_trigger")(
hass, config, action, trigger_info
),
)
@@ -1 +1,12 @@
"""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
+14 -8
View File
@@ -2,12 +2,18 @@
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
@@ -73,13 +79,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": config_entry.as_dict(),
"application_state": shallow_asdict(gateway.application_controller.state),
"versions": {
"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"),
"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,
},
},
KEYS_TO_REDACT,
+3 -3
View File
@@ -20,15 +20,15 @@
"zigpy_znp"
],
"requirements": [
"bellows==0.34.9",
"bellows==0.34.7",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.93",
"zigpy-deconz==0.19.2",
"zigpy==0.53.2",
"zigpy==0.53.0",
"zigpy-xbee==0.16.2",
"zigpy-zigate==0.10.3",
"zigpy-znp==0.9.3"
"zigpy-znp==0.9.2"
],
"usb": [
{
+1 -2
View File
@@ -463,8 +463,7 @@ class ConfigEntry:
await self._async_process_on_unload()
return
# pylint: disable-next=broad-except
except (asyncio.CancelledError, SystemExit, Exception):
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Error setting up entry %s for %s", self.title, integration.domain
)
+2 -2
View File
@@ -7,8 +7,8 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0.dev0"
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0b4"
__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)
-1
View File
@@ -293,7 +293,6 @@ FLOWS = {
"nut",
"nws",
"nzbget",
"obihai",
"octoprint",
"omnilogic",
"oncue",
+1 -1
View File
@@ -3777,7 +3777,7 @@
"obihai": {
"name": "Obihai",
"integration_type": "hub",
"config_flow": true,
"config_flow": false,
"iot_class": "local_polling"
},
"octoprint": {
+23 -70
View File
@@ -7,13 +7,15 @@ 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, Protocol, cast
from typing import Any, 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,
@@ -53,7 +55,6 @@ 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
@@ -76,44 +77,12 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
FROM_CONFIG_FORMAT = "{}_from_config"
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
_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,
}
_LOGGER = logging.getLogger(__name__)
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]
@@ -183,27 +152,6 @@ 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,
@@ -212,18 +160,15 @@ async def async_from_config(
Should be run on the event loop.
"""
factory: Any = None
platform = await _async_get_condition_platform(hass, config)
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 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:
break
if factory:
break
else:
factory = platform.async_condition_from_config
if factory is None:
raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}')
# Check if condition is not enabled
if not config.get(CONF_ENABLED, True):
@@ -983,6 +928,14 @@ 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:
@@ -1038,10 +991,10 @@ async def async_validate_condition_config(
config["conditions"] = conditions
return config
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"):
if condition == "device":
return await device_condition.async_validate_condition_config(hass, config)
if condition in ("numeric_state", "state"):
validator = cast(
Callable[[HomeAssistant, ConfigType], ConfigType],
getattr(sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition)),
+22 -47
View File
@@ -79,27 +79,27 @@ class Selector(Generic[_T]):
return {"selector": {self.selector_type: self.config}}
ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration that provided the entity
vol.Optional("integration"): str,
# Domain the entity belongs to
vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
vol.Optional("domain"): vol.Any(str, [str]),
# Device class of the entity
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]),
vol.Optional("device_class"): str,
}
)
class EntityFilterSelectorConfig(TypedDict, total=False):
class SingleEntitySelectorConfig(TypedDict, total=False):
"""Class to represent a single entity selector config."""
integration: str
domain: str | list[str]
device_class: str | list[str]
device_class: str
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration linked to it with a config entry
vol.Optional("integration"): str,
@@ -108,21 +108,18 @@ DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
# Model of device
vol.Optional("model"): str,
# Device has to contain entities matching this selector
vol.Optional("entity"): vol.All(
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
),
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
}
)
class DeviceFilterSelectorConfig(TypedDict, total=False):
class SingleDeviceSelectorConfig(TypedDict, total=False):
"""Class to represent a single device selector config."""
integration: str
manufacturer: str
model: str
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
entity: SingleEntitySelectorConfig
class ActionSelectorConfig(TypedDict):
@@ -179,8 +176,8 @@ class AddonSelector(Selector[AddonSelectorConfig]):
class AreaSelectorConfig(TypedDict, total=False):
"""Class to represent an area selector config."""
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
multiple: bool
@@ -192,14 +189,8 @@ class AreaSelector(Selector[AreaSelectorConfig]):
CONFIG_SCHEMA = vol.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("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
vol.Optional("multiple", default=False): cv.boolean,
}
)
@@ -408,7 +399,7 @@ class DeviceSelectorConfig(TypedDict, total=False):
integration: str
manufacturer: str
model: str
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
entity: SingleEntitySelectorConfig
multiple: bool
@@ -418,14 +409,8 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
selector_type = "device"
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],
),
},
CONFIG_SCHEMA = SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA.extend(
{vol.Optional("multiple", default=False): cv.boolean}
)
def __init__(self, config: DeviceSelectorConfig | None = None) -> None:
@@ -472,7 +457,7 @@ class DurationSelector(Selector[DurationSelectorConfig]):
return cast(dict[str, float], data)
class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
class EntitySelectorConfig(SingleEntitySelectorConfig, total=False):
"""Class to represent an entity selector config."""
exclude_entities: list[str]
@@ -486,15 +471,11 @@ class EntitySelector(Selector[EntitySelectorConfig]):
selector_type = "entity"
CONFIG_SCHEMA = ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = SINGLE_ENTITY_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],
),
}
)
@@ -803,8 +784,8 @@ class SelectSelector(Selector[SelectSelectorConfig]):
class TargetSelectorConfig(TypedDict, total=False):
"""Class to represent a target selector config."""
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
class StateSelectorConfig(TypedDict, total=False):
@@ -851,14 +832,8 @@ class TargetSelector(Selector[TargetSelectorConfig]):
CONFIG_SCHEMA = vol.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("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
}
)
-1
View File
@@ -2063,7 +2063,6 @@ 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
+1 -1
View File
@@ -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==20230227.0
home-assistant-frontend==20230224.0
home-assistant-intents==2023.2.22
httpx==0.23.3
ifaddr==0.1.7
+1 -2
View File
@@ -264,8 +264,7 @@ async def _async_setup_component(
SLOW_SETUP_MAX_WAIT,
)
return False
# pylint: disable-next=broad-except
except (asyncio.CancelledError, SystemExit, Exception):
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
async_notify_setup_error(hass, domain, integration.documentation)
return False
-10
View File
@@ -1622,16 +1622,6 @@ 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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.4.0.dev0"
version = "2023.3.0b4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+15 -15
View File
@@ -202,7 +202,7 @@ aiolifx_effects==0.3.1
aiolifx_themes==0.4.0
# homeassistant.components.livisi
aiolivisi==0.0.16
aiolivisi==0.0.15
# 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.10
aiosomecomfort==0.0.8
# homeassistant.components.steamist
aiosteamist==0.3.2
@@ -342,7 +342,7 @@ anthemav==1.4.1
apcaccess==0.0.13
# homeassistant.components.apprise
apprise==1.3.0
apprise==1.2.1
# 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.9
bellows==0.34.7
# 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.7.0
bthome-ble==2.5.2
# 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.2.0
caldav==1.1.1
# 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.29
env_canada==0.5.28
# 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==20230227.0
home-assistant-frontend==20230224.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.1.0
odp-amsterdam==5.0.1
# 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.14
pyisy==3.1.13
# 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.6
pyoverkiz==1.7.3
# 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.8
pyprosegur==0.0.5
# 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.6.0
xknx==2.5.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.3
zigpy-znp==0.9.2
# homeassistant.components.zha
zigpy==0.53.2
zigpy==0.53.0
# homeassistant.components.zoneminder
zm-py==0.5.2
+1 -1
View File
@@ -13,7 +13,7 @@ coverage==7.1.0
freezegun==1.2.2
mock-open==1.4.0
mypy==1.0.1
pre-commit==3.1.0
pre-commit==3.0.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