Compare commits

..

57 Commits

Author SHA1 Message Date
Paulus Schoutsen
72c0526d87 Bumped version to 2023.3.0b5 2023-02-27 20:58:22 -05:00
Matthias Alphart
9ed4e01e94 Update xknx to 2.6.0 (#88864) 2023-02-27 20:58:11 -05:00
Paul Bottein
dcf1ecfeb5 Update frontend to 20230227.0 (#88857) 2023-02-27 20:58:10 -05:00
Klaas Schoute
b72224ceff Bump odp-amsterdam to v5.1.0 (#88847) 2023-02-27 20:58:09 -05:00
Erik Montnemery
96ad5c9666 Add thread user flow (#88842) 2023-02-27 20:58:09 -05:00
Erik Montnemery
00b59c142a Fix sensor unit conversion bug (#88825)
* Fix sensor unit conversion bug

* Ensure the correct unit is stored in the entity registry
2023-02-27 20:58:08 -05:00
Michael Davie
b054c81e13 Bump env_canada to 0.5.29 (#88821) 2023-02-27 20:58:07 -05:00
puddly
b0cbcad440 Bump ZHA dependencies (#88799)
* Bump ZHA dependencies

* Use `importlib.metadata.version` to get package versions
2023-02-27 20:58:06 -05:00
stickpin
bafe552af6 Upgrade caldav to 1.2.0 (#88791) 2023-02-27 20:58:05 -05:00
stickpin
d399855e50 Upgrade caldav to 1.1.3 (#88681)
* Update caldav to 1.1.3

* update caldav to 1.1.3

* update caldav to 1.1.3

---------

Co-authored-by: Allen Porter <allen@thebends.org>
2023-02-27 20:58:03 -05:00
mkmer
d26f430766 Bump aiosomecomfort to 0.0.10 (#88766) 2023-02-27 20:56:46 -05:00
Erik Montnemery
f2e4943a53 Catch CancelledError when setting up components (#88635)
* Catch CancelledError when setting up components

* Catch CancelledError when setting up components

* Also catch SystemExit
2023-02-27 20:56:45 -05:00
Bouwe Westerdijk
6512cd901f Correct Plugwise gas_consumed_interval sensor (#87449)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2023-02-27 20:56:45 -05:00
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
141 changed files with 1134 additions and 3886 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1 @@
"""The Obihai integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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

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:

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
),
)

View File

@@ -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

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 = "0b5"
__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)

View File

@@ -293,7 +293,6 @@ FLOWS = {
"nut",
"nws",
"nzbget",
"obihai",
"octoprint",
"omnilogic",
"oncue",

View File

@@ -3777,7 +3777,7 @@
"obihai": {
"name": "Obihai",
"integration_type": "hub",
"config_flow": true,
"config_flow": false,
"iot_class": "local_polling"
},
"octoprint": {

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)),

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,
}
)

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

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.4.0.dev0"
version = "2023.3.0b5"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

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
@@ -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
@@ -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
@@ -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

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

View File

@@ -183,7 +183,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
@@ -311,7 +311,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
@@ -399,7 +399,7 @@ brother==2.2.0
brunt==1.2.0
# homeassistant.components.bthome
bthome-ble==2.7.0
bthome-ble==2.5.2
# homeassistant.components.buienradar
buienradar==1.0.5
@@ -1227,7 +1227,7 @@ pyiqvia==2022.04.0
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.1.14
pyisy==3.1.13
# homeassistant.components.kaleidescape
pykaleidescape==1.0.1
@@ -1322,9 +1322,6 @@ pynx584==0.5
# homeassistant.components.nzbget
pynzbgetapi==0.2.0
# homeassistant.components.obihai
pyobihai==1.3.2
# homeassistant.components.octoprint
pyoctoprintapi==0.1.11
@@ -1343,7 +1340,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
@@ -1367,7 +1364,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

View File

@@ -151,13 +151,13 @@ def _custom_tasks(template, info: Info) -> None:
)
elif template == "config_flow_helper":
info.update_manifest(config_flow=True, integration_type="helper")
info.update_manifest(config_flow=True)
info.update_strings(
config={
"step": {
"user": {
"description": "New NEW_NAME Sensor",
"data": {"entity_id": "Input sensor", "name": "Name"},
"data": {"entity": "Input sensor", "name": "Name"},
},
},
},
@@ -165,7 +165,7 @@ def _custom_tasks(template, info: Info) -> None:
"step": {
"init": {
"data": {
"entity_id": "[%key:component::NEW_DOMAIN::config::step::user::description%]"
"entity": "[%key:component::NEW_DOMAIN::config::step::user::description%]"
},
},
},

View File

@@ -1,7 +1,4 @@
"""Configuration for Abode tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from jaraco.abode.helpers import urls as URL
import pytest
@@ -9,15 +6,6 @@ from tests.common import load_fixture
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.abode.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(autouse=True)
def requests_mock_fixture(requests_mock) -> None:
"""Fixture to provide a requests mocker."""

View File

@@ -6,7 +6,6 @@ from jaraco.abode.exceptions import (
AuthenticationException as AbodeAuthenticationException,
)
from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED
import pytest
from requests.exceptions import ConnectTimeout
from homeassistant import data_entry_flow
@@ -18,8 +17,6 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_show_form(hass: HomeAssistant) -> None:
"""Test that the form is served with no input."""

View File

@@ -131,6 +131,35 @@ async def test_form_already_configured(
assert result2["reason"] == "already_configured"
async def test_import_flow_success(
hass: HomeAssistant, mock_aladdinconnect_api: MagicMock
) -> None:
"""Test a successful import of yaml."""
with patch(
"homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient",
return_value=mock_aladdinconnect_api,
), patch(
"homeassistant.components.aladdin_connect.async_setup_entry", return_value=True
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_USERNAME: "test-user",
CONF_PASSWORD: "test-password",
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Aladdin Connect"
assert result2["data"] == {
CONF_USERNAME: "test-user",
CONF_PASSWORD: "test-password",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_flow(
hass: HomeAssistant, mock_aladdinconnect_api: MagicMock
) -> None:

View File

@@ -1,12 +1,16 @@
"""Test the Aladdin Connect Cover."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.aladdin_connect.const import DOMAIN
from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PASSWORD,
CONF_USERNAME,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
STATE_CLOSED,
@@ -192,3 +196,36 @@ async def test_cover_operation(
await hass.async_block_till_done()
assert hass.states.get("cover.home").state == STATE_UNKNOWN
async def test_yaml_import(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
mock_aladdinconnect_api: MagicMock,
) -> None:
"""Test setup YAML import."""
assert COVER_DOMAIN not in hass.config.components
with patch(
"homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient",
return_value=mock_aladdinconnect_api,
):
await async_setup_component(
hass,
COVER_DOMAIN,
{
COVER_DOMAIN: {
"platform": DOMAIN,
"username": "test-user",
"password": "test-password",
}
},
)
await hass.async_block_till_done()
assert hass.config_entries.async_entries(DOMAIN)
assert "Configuring Aladdin Connect through yaml is deprecated" in caplog.text
assert hass.config_entries.async_entries(DOMAIN)
config_data = hass.config_entries.async_entries(DOMAIN)[0].data
assert config_data[CONF_USERNAME] == "test-user"
assert config_data[CONF_PASSWORD] == "test-password"

View File

@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.apcupsd import DOMAIN
from homeassistant.components.apcupsd import DOMAIN, APCUPSdData
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@@ -31,20 +31,20 @@ async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> No
async def test_multiple_integrations(hass: HomeAssistant) -> None:
"""Test successful setup for multiple entries."""
# Load two integrations from two mock hosts.
status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"}
status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"}
entries = (
await init_integration(hass, host="test1", status=status1),
await init_integration(hass, host="test2", status=status2),
await init_integration(hass, host="test1", status=MOCK_STATUS),
await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS),
)
assert len(hass.config_entries.async_entries(DOMAIN)) == 2
assert all(entry.state is ConfigEntryState.LOADED for entry in entries)
# Data dict should contain different API objects.
assert len(hass.data[DOMAIN]) == len(entries)
for entry in entries:
assert entry.entry_id in hass.data[DOMAIN]
assert isinstance(hass.data[DOMAIN][entry.entry_id], APCUPSdData)
state1 = hass.states.get("sensor.ups_load")
state2 = hass.states.get("sensor.ups_load_2")
assert state1 is not None and state2 is not None
assert state1.state != state2.state
assert (
hass.data[DOMAIN][entries[0].entry_id] != hass.data[DOMAIN][entries[1].entry_id]
)
async def test_connection_error(hass: HomeAssistant) -> None:
@@ -83,14 +83,19 @@ async def test_unload_remove(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entries[0].state is ConfigEntryState.NOT_LOADED
assert entries[1].state is ConfigEntryState.LOADED
assert len(hass.data[DOMAIN]) == 1
# Unload the second entry.
assert await hass.config_entries.async_unload(entries[1].entry_id)
await hass.async_block_till_done()
assert all(entry.state is ConfigEntryState.NOT_LOADED for entry in entries)
# We should never leave any garbage in the data dict.
assert len(hass.data[DOMAIN]) == 0
# Remove both entries.
for entry in entries:
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
await hass.async_block_till_done()
state = hass.states.get(entry.entry_id)
assert state is None

View File

@@ -1,224 +0,0 @@
# serializer version: 1
# name: test_extract_blueprint_from_community_topic
OrderedDict({
'remote': OrderedDict({
'name': 'Remote',
'description': 'IKEA remote to use',
'selector': dict({
'device': OrderedDict({
'integration': 'zha',
'manufacturer': 'IKEA of Sweden',
'model': 'TRADFRI remote control',
'multiple': False,
}),
}),
}),
'light': OrderedDict({
'name': 'Light(s)',
'description': 'The light(s) to control',
'selector': dict({
'target': OrderedDict({
'entity': list([
OrderedDict({
'domain': list([
'light',
]),
}),
]),
}),
}),
}),
'force_brightness': OrderedDict({
'name': 'Force turn on brightness',
'description': '''
Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.
''',
'default': False,
'selector': dict({
'boolean': dict({
}),
}),
}),
'brightness': OrderedDict({
'name': 'Brightness',
'description': 'Brightness of the light(s) when turning on',
'default': 50,
'selector': dict({
'number': OrderedDict({
'min': 0.0,
'max': 100.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': '%',
}),
}),
}),
'button_left_short': OrderedDict({
'name': 'Left button - short press',
'description': 'Action to run on short left button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_left_long': OrderedDict({
'name': 'Left button - long press',
'description': 'Action to run on long left button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_right_short': OrderedDict({
'name': 'Right button - short press',
'description': 'Action to run on short right button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_right_long': OrderedDict({
'name': 'Right button - long press',
'description': 'Action to run on long right button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
})
# ---
# name: test_fetch_blueprint_from_community_url
OrderedDict({
'remote': OrderedDict({
'name': 'Remote',
'description': 'IKEA remote to use',
'selector': dict({
'device': OrderedDict({
'integration': 'zha',
'manufacturer': 'IKEA of Sweden',
'model': 'TRADFRI remote control',
'multiple': False,
}),
}),
}),
'light': OrderedDict({
'name': 'Light(s)',
'description': 'The light(s) to control',
'selector': dict({
'target': OrderedDict({
'entity': list([
OrderedDict({
'domain': list([
'light',
]),
}),
]),
}),
}),
}),
'force_brightness': OrderedDict({
'name': 'Force turn on brightness',
'description': '''
Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.
''',
'default': False,
'selector': dict({
'boolean': dict({
}),
}),
}),
'brightness': OrderedDict({
'name': 'Brightness',
'description': 'Brightness of the light(s) when turning on',
'default': 50,
'selector': dict({
'number': OrderedDict({
'min': 0.0,
'max': 100.0,
'mode': 'slider',
'step': 1.0,
'unit_of_measurement': '%',
}),
}),
}),
'button_left_short': OrderedDict({
'name': 'Left button - short press',
'description': 'Action to run on short left button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_left_long': OrderedDict({
'name': 'Left button - long press',
'description': 'Action to run on long left button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_right_short': OrderedDict({
'name': 'Right button - short press',
'description': 'Action to run on short right button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
'button_right_long': OrderedDict({
'name': 'Right button - long press',
'description': 'Action to run on long right button press',
'default': NodeListClass([
]),
'selector': dict({
'action': dict({
}),
}),
}),
})
# ---
# name: test_fetch_blueprint_from_github_gist_url
OrderedDict({
'motion_entity': OrderedDict({
'name': 'Motion Sensor',
'selector': dict({
'entity': OrderedDict({
'domain': list([
'binary_sensor',
]),
'device_class': list([
'motion',
]),
'multiple': False,
}),
}),
}),
'light_entity': OrderedDict({
'name': 'Light',
'selector': dict({
'entity': OrderedDict({
'domain': list([
'light',
]),
'multiple': False,
}),
}),
}),
})
# ---

View File

@@ -18,6 +18,74 @@ def community_post():
return load_fixture("blueprint/community_post.json")
COMMUNITY_POST_INPUTS = {
"remote": {
"name": "Remote",
"description": "IKEA remote to use",
"selector": {
"device": {
"integration": "zha",
"manufacturer": "IKEA of Sweden",
"model": "TRADFRI remote control",
"multiple": False,
}
},
},
"light": {
"name": "Light(s)",
"description": "The light(s) to control",
"selector": {"target": {"entity": {"domain": "light"}}},
},
"force_brightness": {
"name": "Force turn on brightness",
"description": (
'Force the brightness to the set level below, when the "on" button on the'
" remote is pushed and lights turn on.\n"
),
"default": False,
"selector": {"boolean": {}},
},
"brightness": {
"name": "Brightness",
"description": "Brightness of the light(s) when turning on",
"default": 50,
"selector": {
"number": {
"min": 0.0,
"max": 100.0,
"mode": "slider",
"step": 1.0,
"unit_of_measurement": "%",
}
},
},
"button_left_short": {
"name": "Left button - short press",
"description": "Action to run on short left button press",
"default": [],
"selector": {"action": {}},
},
"button_left_long": {
"name": "Left button - long press",
"description": "Action to run on long left button press",
"default": [],
"selector": {"action": {}},
},
"button_right_short": {
"name": "Right button - short press",
"description": "Action to run on short right button press",
"default": [],
"selector": {"action": {}},
},
"button_right_long": {
"name": "Right button - long press",
"description": "Action to run on long right button press",
"default": [],
"selector": {"action": {}},
},
}
def test_get_community_post_import_url() -> None:
"""Test variations of generating import forum url."""
assert (
@@ -52,14 +120,14 @@ def test_get_github_import_url() -> None:
)
def test_extract_blueprint_from_community_topic(community_post, snapshot) -> None:
def test_extract_blueprint_from_community_topic(community_post) -> None:
"""Test extracting blueprint."""
imported_blueprint = importer._extract_blueprint_from_community_topic(
"http://example.com", json.loads(community_post)
)
assert imported_blueprint is not None
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.inputs == snapshot
assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS
def test_extract_blueprint_from_community_topic_invalid_yaml() -> None:
@@ -93,7 +161,7 @@ def test_extract_blueprint_from_community_topic_wrong_lang() -> None:
async def test_fetch_blueprint_from_community_url(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, community_post, snapshot
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, community_post
) -> None:
"""Test fetching blueprint from url."""
aioclient_mock.get(
@@ -104,7 +172,7 @@ async def test_fetch_blueprint_from_community_url(
)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.inputs == snapshot
assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS
assert (
imported_blueprint.suggested_filename
== "frenck/zha-ikea-five-button-remote-for-lights"
@@ -147,7 +215,7 @@ async def test_fetch_blueprint_from_github_url(
async def test_fetch_blueprint_from_github_gist_url(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, snapshot
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test fetching blueprint from url."""
aioclient_mock.get(
@@ -159,6 +227,21 @@ async def test_fetch_blueprint_from_github_gist_url(
imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.inputs == snapshot
assert imported_blueprint.blueprint.inputs == {
"motion_entity": {
"name": "Motion Sensor",
"selector": {
"entity": {
"domain": "binary_sensor",
"device_class": "motion",
"multiple": False,
}
},
},
"light_entity": {
"name": "Light",
"selector": {"entity": {"domain": "light", "multiple": False}},
},
}
assert imported_blueprint.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url

View File

@@ -1,14 +0,0 @@
"""Configuration for brunt tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.brunt.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

View File

@@ -1,5 +1,5 @@
"""Test the Brunt config flow."""
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
from aiohttp.client_exceptions import ServerDisconnectedError
@@ -14,10 +14,8 @@ from tests.common import MockConfigEntry
CONFIG = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
@@ -28,7 +26,10 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
with patch(
"homeassistant.components.brunt.config_flow.BruntClientAsync.async_login",
return_value=None,
):
), patch(
"homeassistant.components.brunt.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,

View File

@@ -844,23 +844,6 @@ async def test_v1_sensors(
},
],
),
(
"A4:C1:38:8D:18:B2",
make_bthome_v2_adv(
"A4:C1:38:8D:18:B2",
b"\x40\x4b\x13\x8a\x14",
),
None,
[
{
"sensor_entity": "sensor.test_device_18b2_gas",
"friendly_name": "Test Device 18B2 Gas",
"unit_of_measurement": "",
"state_class": "total_increasing",
"expected_state": "1346.067",
},
],
),
(
"A4:C1:38:8D:18:B2",
make_bthome_v2_adv(

View File

@@ -1,14 +0,0 @@
"""Configuration for cert_expiry tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.cert_expiry.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

View File

@@ -3,8 +3,6 @@ import socket
import ssl
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
@@ -15,8 +13,6 @@ from .helpers import future_timestamp
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
async def test_user(hass: HomeAssistant) -> None:
"""Test user config."""
@@ -38,6 +34,9 @@ async def test_user(hass: HomeAssistant) -> None:
assert result["data"][CONF_PORT] == PORT
assert result["result"].unique_id == f"{HOST}:{PORT}"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_user_with_bad_cert(hass: HomeAssistant) -> None:
"""Test user config with bad certificate."""
@@ -61,6 +60,9 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None:
assert result["data"][CONF_PORT] == PORT
assert result["result"].unique_id == f"{HOST}:{PORT}"
with patch("homeassistant.components.cert_expiry.sensor.async_setup_entry"):
await hass.async_block_till_done()
async def test_import_host_only(hass: HomeAssistant) -> None:
"""Test import with host only."""

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