forked from home-assistant/core
Compare commits
218 Commits
gj-20250610-05
..
rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 25d1480f2a | |||
| 2175754a1f | |||
| df5f253146 | |||
| a017d9415b | |||
| e89c3b1e92 | |||
| d4ffeedc87 | |||
| cb74b2663f | |||
| 4ec711bd63 | |||
| e81c8ce44d | |||
| c2cf348255 | |||
| e048a3da38 | |||
| 7cf3116f5b | |||
| 5cd7ea06ad | |||
| 52c62b31fd | |||
| b2bb0aeb64 | |||
| f0fc87e2b6 | |||
| e7a88e99f9 | |||
| c3e3a36b4c | |||
| 54d8d71de5 | |||
| fb4c77d43b | |||
| cada2f84a9 | |||
| dc4627f413 | |||
| 02524b8b9b | |||
| 60b8230ecc | |||
| 75e6f23a82 | |||
| 1f221712a2 | |||
| 43797c03cc | |||
| 89637a618e | |||
| fd605e0abe | |||
| e73bcc73b5 | |||
| c02707a90f | |||
| 232f853d68 | |||
| 91e296a0c8 | |||
| bcedb06862 | |||
| 2ab32220ed | |||
| 273ccb3929 | |||
| caaa4d5f35 | |||
| 0cf1fd1d41 | |||
| 5ee39df330 | |||
| cc972d20f6 | |||
| e0f32cfd54 | |||
| 6384c800c3 | |||
| 82de2ed8e1 | |||
| af72d1854f | |||
| 0cff7cbccd | |||
| 6f4e16eed1 | |||
| 66be2f9240 | |||
| b6c8718ae4 | |||
| c8b70cc0fb | |||
| 6d1f621e55 | |||
| 671a33b31c | |||
| 7afc469306 | |||
| 8fd52248b7 | |||
| 69ba2aab11 | |||
| f1df6dcda5 | |||
| 43e16bb913 | |||
| 4147211f94 | |||
| 63e49c5d3c | |||
| 35580c0849 | |||
| 8949a595fe | |||
| bf8ef0a767 | |||
| 39962a3f48 | |||
| 4964621014 | |||
| 18e1a26da1 | |||
| 1d91ca5716 | |||
| 1040646610 | |||
| fcd71931e7 | |||
| bdbb74aff1 | |||
| 6f4029983a | |||
| b2d25b1883 | |||
| ba19d4f043 | |||
| b222fe5afa | |||
| f945defa2b | |||
| 4f0e4bc1ca | |||
| 41abc8404d | |||
| 2b08c4c344 | |||
| 97d91ddddb | |||
| ec30b12fd1 | |||
| 9997fc11b1 | |||
| c6ff0e6492 | |||
| a3220ecae6 | |||
| 218864d08c | |||
| 3d0d70ece6 | |||
| f629731930 | |||
| e7a7b2417b | |||
| 0b24a9abc3 | |||
| ca77b5210f | |||
| 0874f1c350 | |||
| d89b99f42b | |||
| 7bd6ec68a8 | |||
| bfe2eeb833 | |||
| e97ab1fe3c | |||
| b3ee2a8885 | |||
| 80b09e3212 | |||
| 0eb3714abc | |||
| 7991977443 | |||
| 5e5431c9f9 | |||
| 1fc05d1a30 | |||
| 21833e7c31 | |||
| 79daeb23a9 | |||
| 761c2578fb | |||
| 4d3145e559 | |||
| 91e29a3bf1 | |||
| f6a4486c65 | |||
| fc8b512931 | |||
| e5dd15da82 | |||
| e4140d71ab | |||
| 8312780c47 | |||
| 5accc3dec2 | |||
| d875989866 | |||
| 38c92a2338 | |||
| ce76b5db16 | |||
| dfc4889d45 | |||
| 41431282ee | |||
| 5821b2f03c | |||
| 78d2bf736c | |||
| 6c098c3e0a | |||
| bfb140d2e9 | |||
| f71a1a7a89 | |||
| e8aab39620 | |||
| 1d578d8563 | |||
| abfd443541 | |||
| 81cbb6e5cf | |||
| 010c5cab87 | |||
| 415858119a | |||
| 1838a731d6 | |||
| 1e304fad65 | |||
| 999c9b3dc5 | |||
| e15edbd54b | |||
| e5cb77d168 | |||
| cf521d4c7c | |||
| 6f09474193 | |||
| 7626933352 | |||
| 9e1d8c2fc6 | |||
| 6defed2915 | |||
| d729eed7c2 | |||
| f280032dcf | |||
| 7e85137012 | |||
| 88f2c3abd3 | |||
| 1a21e01f85 | |||
| d302e817c8 | |||
| 1e1b0424d7 | |||
| 03f028b7e2 | |||
| b1d35de8e4 | |||
| ea6b9e5260 | |||
| 06d869aaa5 | |||
| 907cebdd6d | |||
| 745902bc7e | |||
| ef0b3c9f9c | |||
| 532c077ddf | |||
| cd905a6593 | |||
| d0bf9d9bfb | |||
| ddc79a631d | |||
| 6015f60db4 | |||
| a6608bd7ea | |||
| fb2d8c6406 | |||
| c84ffb54d2 | |||
| 306bbdc697 | |||
| 9879ecad85 | |||
| f0fcef5744 | |||
| aa8a6058b5 | |||
| 48103bd244 | |||
| 600ac17a5f | |||
| d46f28792c | |||
| 0f7379c941 | |||
| 4317fad798 | |||
| 5cfccb7e1d | |||
| 097eecd78a | |||
| 64b4642c49 | |||
| 0e87d14ca8 | |||
| 4d22b35a9f | |||
| 26586b4514 | |||
| 95fb2a7d7f | |||
| fa66ea31d3 | |||
| e0d3b819e5 | |||
| 17a0b4f3d0 | |||
| d0d228d9f4 | |||
| 309acb961b | |||
| 12f8ebb3ea | |||
| 612861061c | |||
| 83af5ec36b | |||
| 74102d0319 | |||
| fbd05a0fcf | |||
| a53c786fe0 | |||
| eb2728e5b9 | |||
| 3f17223387 | |||
| 74104cf107 | |||
| 13b4879723 | |||
| f1ec0b2c59 | |||
| 6d44daf599 | |||
| 644a6f5569 | |||
| fb83396522 | |||
| e825bd0bdb | |||
| 61823ec7e2 | |||
| cd133cbbe3 | |||
| 0e7a1bb76c | |||
| f86bf69ebc | |||
| adddf330fd | |||
| 10adb57b83 | |||
| 3160fe9abc | |||
| 6adb27d173 | |||
| 6e6aae2ea3 | |||
| 41a140d16c | |||
| 8880ab6498 | |||
| 389becc4f6 | |||
| 923530972a | |||
| b84850df9f | |||
| 9e7dc1d11d | |||
| 2830ed6147 | |||
| bfa919d078 | |||
| f09c28e61f | |||
| bfdba7713e | |||
| d6cadc1e3f | |||
| 20a6a3f195 | |||
| f60de45b52 | |||
| 77031d1ae4 | |||
| 9483a88ee1 | |||
| 3438a4f063 |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.7"
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
- name: Run ruff
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
|
||||
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
|
||||
env:
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.19
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.19
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
rev: v1.35.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
||||
+1
-1
@@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_devices.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
|
||||
Vendored
+1
-1
@@ -45,7 +45,7 @@
|
||||
{
|
||||
"label": "Ruff",
|
||||
"type": "shell",
|
||||
"command": "pre-commit run ruff-check --all-files",
|
||||
"command": "pre-commit run ruff --all-files",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
||||
Generated
+2
-2
@@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/amazon_devices/ @chemelli74
|
||||
/tests/components/amazon_devices/ @chemelli74
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
|
||||
@@ -171,6 +171,8 @@ FRONTEND_INTEGRATIONS = {
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# The substages preceding it should also have no timeout, until we ensure that the recorder
|
||||
# is not accidentally promoted as a dependency of any of the integrations in them.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
@@ -927,11 +929,7 @@ async def _async_set_up_integrations(
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
timeout,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message=f"Bootstrap stage {name} timeout",
|
||||
):
|
||||
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
@@ -943,11 +941,7 @@ async def _async_set_up_integrations(
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
WRAP_UP_TIMEOUT,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message="Bootstrap startup wrap up timeout",
|
||||
):
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Amazon",
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"amazon_devices",
|
||||
"alexa_devices",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
|
||||
@@ -14,24 +14,30 @@ from jaraco.abode.exceptions import (
|
||||
)
|
||||
from jaraco.abode.helpers.timeline import Groups as GROUPS
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
from .services import async_setup_services
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
@@ -39,12 +45,22 @@ ATTR_EVENT_CODE = "event_code"
|
||||
ATTR_EVENT_NAME = "event_name"
|
||||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_UTC = "event_utc"
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_USER_NAME = "user_name"
|
||||
ATTR_APP_TYPE = "app_type"
|
||||
ATTR_EVENT_BY = "event_by"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -69,7 +85,7 @@ class AbodeSystem:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
async_setup_services(hass)
|
||||
setup_hass_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
@@ -122,6 +138,60 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
def setup_hass_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
def change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
def capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
def trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Support for the Abode Security System."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jaraco.abode.exceptions import Exception as AbodeException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def _change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
call.hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
|
||||
def _capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def _trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER_AUTOMATION,
|
||||
_trigger_automation,
|
||||
schema=AUTOMATION_SCHEMA,
|
||||
)
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import CONNECTION_TYPE, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
|
||||
@@ -41,30 +41,7 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
try:
|
||||
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
|
||||
rooms = await self.adax_data_handler.fetch_rooms_info() or []
|
||||
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
|
||||
else:
|
||||
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
|
||||
rooms = []
|
||||
|
||||
if not rooms:
|
||||
_LOGGER.debug(
|
||||
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
|
||||
)
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
|
||||
|
||||
if not rooms:
|
||||
raise UpdateFailed("No rooms available from Adax API")
|
||||
|
||||
except OSError as e:
|
||||
raise UpdateFailed(f"Error communicating with API: {e}") from e
|
||||
|
||||
for room in rooms:
|
||||
room["energyWh"] = int(room.get("energyWh", 0))
|
||||
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Support for Adax energy sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AdaxConfigEntry
|
||||
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax energy sensors with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) != LOCAL:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
|
||||
# Create individual energy sensors for each device
|
||||
async_add_entities(
|
||||
AdaxEnergySensor(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
"""Representation of an Adax energy sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "energy"
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
|
||||
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_suggested_display_precision = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the energy sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
room = coordinator.data[device_id]
|
||||
|
||||
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=room["name"],
|
||||
manufacturer="Adax",
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available and "energyWh" in self.coordinator.data[self._device_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the native value of the sensor."""
|
||||
return int(self.coordinator.data[self._device_id]["energyWh"])
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
|
||||
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
manufacturer="Agent",
|
||||
model="Camera",
|
||||
name=f"{device.client.name} {device.name}",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.4.6"]
|
||||
"requirements": ["aioairq==0.4.4"]
|
||||
}
|
||||
|
||||
@@ -5,22 +5,23 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
@@ -31,8 +32,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
async def _update_method() -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""The Airthings integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.airthings = airthings
|
||||
|
||||
async def _update_method(self) -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await self.airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,9 +27,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirthingsConfigEntry
|
||||
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
@@ -56,12 +54,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@@ -148,7 +140,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class AirthingsHeaterEnergySensor(
|
||||
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
||||
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
|
||||
):
|
||||
"""Representation of a Airthings Sensor device."""
|
||||
|
||||
@@ -157,7 +149,7 @@ class AirthingsHeaterEnergySensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirthingsDataUpdateCoordinator,
|
||||
coordinator: AirthingsDataCoordinatorType,
|
||||
airthings_device: AirthingsDevice,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.3.0"]
|
||||
"requirements": ["airtouch5py==0.2.11"]
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Amazon Devices integration."""
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -13,7 +13,7 @@ PLATFORMS = [
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Amazon Devices platform."""
|
||||
"""Set up Alexa Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Amazon Devices binary sensor entity description."""
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices binary sensors based on a config entry."""
|
||||
"""Set up Alexa Devices binary sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Amazon Devices integration."""
|
||||
"""Config flow for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Amazon Devices."""
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
"""Amazon Devices constants."""
|
||||
"""Alexa Devices constants."""
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "amazon_devices"
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Support for Amazon Devices."""
|
||||
"""Support for Alexa Devices."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Base coordinator for Amazon Devices."""
|
||||
"""Base coordinator for Alexa Devices."""
|
||||
|
||||
config_entry: AmazonConfigEntry
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Diagnostics support for Amazon Devices integration."""
|
||||
"""Diagnostics support for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||
@@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
|
||||
|
||||
|
||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "alexa_devices",
|
||||
"name": "Alexa Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.2"]
|
||||
}
|
||||
+6
-2
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||
|
||||
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -20,8 +21,9 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Amazon Devices notify entity description."""
|
||||
"""Alexa Devices notify entity description."""
|
||||
|
||||
is_supported: Callable[[AmazonDevice], bool] = lambda _device: True
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
|
||||
@@ -31,6 +33,7 @@ NOTIFY: Final = (
|
||||
key="speak",
|
||||
translation_key="speak",
|
||||
subkey="AUDIO_PLAYER",
|
||||
is_supported=lambda _device: _device.device_family != SPEAKER_GROUP_FAMILY,
|
||||
method=lambda api, device, message: api.call_alexa_speak(device, message),
|
||||
),
|
||||
AmazonNotifyEntityDescription(
|
||||
@@ -49,7 +52,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices notification entity based on a config entry."""
|
||||
"""Set up Alexa Devices notification entity based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@@ -58,6 +61,7 @@ async def async_setup_entry(
|
||||
for sensor_desc in NOTIFY
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
and sensor_desc.is_supported(coordinator.data[serial_num])
|
||||
)
|
||||
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ rules:
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Network information not relevant
|
||||
discovery: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
+6
-6
@@ -12,16 +12,16 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::amazon_devices::common::data_country%]",
|
||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::amazon_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::amazon_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::amazon_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
+2
-2
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Amazon Devices switch entity description."""
|
||||
"""Alexa Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
@@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices switches based on a config entry."""
|
||||
"""Set up Alexa Devices switches based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"domain": "amazon_devices",
|
||||
"name": "Amazon Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
@@ -16,7 +16,10 @@ from amcrest import AmcrestError, ApiWrapper, LoginError
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_BINARY_SENSORS,
|
||||
CONF_HOST,
|
||||
@@ -27,17 +30,21 @@ from homeassistant.const import (
|
||||
CONF_SENSORS,
|
||||
CONF_SWITCHES,
|
||||
CONF_USERNAME,
|
||||
ENTITY_MATCH_ALL,
|
||||
ENTITY_MATCH_NONE,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
|
||||
from .camera import STREAM_SOURCE_LIST
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .const import (
|
||||
CAMERAS,
|
||||
COMM_RETRIES,
|
||||
@@ -51,7 +58,6 @@ from .const import (
|
||||
)
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_KEYS
|
||||
from .services import async_setup_services
|
||||
from .switch import SWITCH_KEYS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -449,7 +455,47 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if not hass.data[DATA_AMCREST][DEVICES]:
|
||||
return False
|
||||
|
||||
async_setup_services(hass)
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .camera import CAMERA_SERVICES
|
||||
from .const import CAMERAS, DATA_AMCREST, DOMAIN
|
||||
from .helpers import service_signal
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Amcrest IP Camera services."""
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -225,8 +225,7 @@ class Analytics:
|
||||
LOGGER.error(err)
|
||||
return
|
||||
|
||||
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
||||
|
||||
configuration_set = set(yaml_configuration)
|
||||
er_platforms = {
|
||||
entity.platform
|
||||
for entity in ent_reg.entities.values()
|
||||
@@ -371,13 +370,3 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
"""Extract domains from the YAML configuration."""
|
||||
domains = set(yaml_configuration)
|
||||
for platforms in conf_util.extract_platform_integrations(
|
||||
yaml_configuration, BASE_PLATFORMS
|
||||
).values():
|
||||
domains.update(platforms)
|
||||
return domains
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
"requirements": ["apsystems-ez1==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class ArubaDeviceScanner(DeviceScanner):
|
||||
def get_aruba_data(self) -> dict[str, dict[str, str]] | None:
|
||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||
|
||||
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
|
||||
connect = f"ssh {self.username}@{self.host}"
|
||||
ssh: pexpect.spawn[str] = pexpect.spawn(connect, encoding="utf-8")
|
||||
query = ssh.expect(
|
||||
[
|
||||
|
||||
@@ -1207,15 +1207,6 @@ class PipelineRun:
|
||||
|
||||
self._streamed_response_text = True
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.INTENT_PROGRESS,
|
||||
{
|
||||
"tts_start_streaming": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
async def tts_input_stream_generator() -> AsyncGenerator[str]:
|
||||
"""Yield TTS input stream."""
|
||||
while (tts_input := await tts_input_stream.get()) is not None:
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -33,7 +32,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
"""Representation of an ATAG water heater."""
|
||||
|
||||
_attr_operation_list = OPERATION_LIST
|
||||
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
|
||||
from .config import AxisConfig
|
||||
from .entity_loader import AxisEntityLoader
|
||||
from .event_source import AxisEventSource
|
||||
@@ -79,7 +79,7 @@ class AxisHub:
|
||||
config_entry_id=self.config.entry.entry_id,
|
||||
configuration_url=self.api.config.url,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
identifiers={(AXIS_DOMAIN, self.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=f"{self.config.model} {self.product_type}",
|
||||
name=self.config.name,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==2.1.0"]
|
||||
"requirements": ["python-bsblan==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import socket
|
||||
|
||||
@@ -27,18 +26,8 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type CloudflareConfigEntry = ConfigEntry[CloudflareRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CloudflareRuntimeData:
|
||||
"""Runtime data for Cloudflare config entry."""
|
||||
|
||||
client: pycfdns.Client
|
||||
dns_zone: pycfdns.ZoneModel
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Cloudflare from a config entry."""
|
||||
session = async_get_clientsession(hass)
|
||||
client = pycfdns.Client(
|
||||
@@ -56,12 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
|
||||
except pycfdns.ComunicationException as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
entry.runtime_data = CloudflareRuntimeData(client, dns_zone)
|
||||
|
||||
async def update_records(now: datetime) -> None:
|
||||
async def update_records(now):
|
||||
"""Set up recurring update."""
|
||||
try:
|
||||
await _async_update_cloudflare(hass, entry)
|
||||
await _async_update_cloudflare(
|
||||
hass, client, dns_zone, entry.data[CONF_RECORDS]
|
||||
)
|
||||
except (
|
||||
pycfdns.AuthenticationException,
|
||||
pycfdns.ComunicationException,
|
||||
@@ -71,7 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
|
||||
async def update_records_service(call: ServiceCall) -> None:
|
||||
"""Set up service for manual trigger."""
|
||||
try:
|
||||
await _async_update_cloudflare(hass, entry)
|
||||
await _async_update_cloudflare(
|
||||
hass, client, dns_zone, entry.data[CONF_RECORDS]
|
||||
)
|
||||
except (
|
||||
pycfdns.AuthenticationException,
|
||||
pycfdns.ComunicationException,
|
||||
@@ -88,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Cloudflare config entry."""
|
||||
|
||||
return True
|
||||
@@ -96,12 +87,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry)
|
||||
|
||||
async def _async_update_cloudflare(
|
||||
hass: HomeAssistant,
|
||||
entry: CloudflareConfigEntry,
|
||||
client: pycfdns.Client,
|
||||
dns_zone: pycfdns.ZoneModel,
|
||||
target_records: list[str],
|
||||
) -> None:
|
||||
client = entry.runtime_data.client
|
||||
dns_zone = entry.runtime_data.dns_zone
|
||||
target_records: list[str] = entry.data[CONF_RECORDS]
|
||||
|
||||
_LOGGER.debug("Starting update for zone %s", dns_zone["name"])
|
||||
|
||||
records = await client.list_dns_records(zone_id=dns_zone["id"], type="A")
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.3.0"]
|
||||
"requirements": ["numpy==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_GESTURE, DOMAIN
|
||||
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
|
||||
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
|
||||
from .device_trigger import (
|
||||
CONF_BOTH_BUTTONS,
|
||||
@@ -200,6 +200,6 @@ def async_describe_events(
|
||||
}
|
||||
|
||||
async_describe_event(
|
||||
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
)
|
||||
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SOURCE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Derivative from a config entry."""
|
||||
@@ -29,19 +25,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
get_helper_entity_id=lambda: entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, entry.entry_id
|
||||
),
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_SOURCE]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.4.0"]
|
||||
"requirements": ["aiodns==3.5.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -35,7 +34,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
|
||||
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
|
||||
def sort_ips(ips: list, querytype: str) -> list:
|
||||
"""Join IPs into a single string."""
|
||||
|
||||
if querytype == "AAAA":
|
||||
@@ -90,7 +89,7 @@ class WanIpSensor(SensorEntity):
|
||||
self.hostname = hostname
|
||||
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
|
||||
self.resolver.nameservers = [resolver]
|
||||
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
|
||||
self.querytype = "AAAA" if ipv6 else "A"
|
||||
self._retries = DEFAULT_RETRIES
|
||||
self._attr_extra_state_attributes = {
|
||||
"resolver": resolver,
|
||||
@@ -107,7 +106,7 @@ class WanIpSensor(SensorEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
try:
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import _LOGGER, CONF_DOWNLOAD_DIR
|
||||
from .services import async_setup_services
|
||||
from .services import register_services
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -25,6 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
async_setup_services(hass)
|
||||
register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -141,7 +141,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
"""Register the services for the downloader component."""
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -26,11 +26,12 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from .const import (
|
||||
@@ -61,7 +62,6 @@ from .discovery import (
|
||||
async_update_entry_from_discovery,
|
||||
)
|
||||
from .models import ELKM1Data
|
||||
from .services import async_setup_services
|
||||
|
||||
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
|
||||
|
||||
@@ -79,6 +79,19 @@ PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def hostname_from_url(url: str) -> str:
|
||||
"""Return the hostname from a url."""
|
||||
@@ -166,7 +179,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Elk M1 platform."""
|
||||
async_setup_services(hass)
|
||||
_create_elk_services(hass)
|
||||
|
||||
async def _async_discovery(*_: Any) -> None:
|
||||
async_trigger_discovery(
|
||||
@@ -313,6 +326,17 @@ def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -
|
||||
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -366,3 +390,39 @@ async def async_wait_for_elk_to_sync(
|
||||
_LOGGER.debug("Received %s event", name)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import ELKM1Data
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(service.hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_word(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_phrase(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).set_time(dt_util.now())
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -9,9 +10,14 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
OPTION_DISABLE_KEEP_ALIVE,
|
||||
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
|
||||
|
||||
@@ -19,8 +25,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
"""Set up Enphase Envoy from a config entry."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
session = async_create_clientsession(hass, verify_ssl=False)
|
||||
envoy = Envoy(host, session)
|
||||
options = entry.options
|
||||
envoy = (
|
||||
Envoy(
|
||||
host,
|
||||
httpx.AsyncClient(
|
||||
verify=False, limits=httpx.Limits(max_keepalive_connections=0)
|
||||
),
|
||||
)
|
||||
if options.get(
|
||||
OPTION_DISABLE_KEEP_ALIVE, OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE
|
||||
)
|
||||
else Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
)
|
||||
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
@@ -63,7 +63,7 @@ async def validate_input(
|
||||
description_placeholders: dict[str, str],
|
||||
) -> Envoy:
|
||||
"""Validate the user input allows us to connect."""
|
||||
envoy = Envoy(host, async_get_clientsession(hass, verify_ssl=False))
|
||||
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
try:
|
||||
await envoy.setup()
|
||||
await envoy.authenticate(username=username, password=password)
|
||||
|
||||
@@ -6,7 +6,6 @@ import copy
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from attr import asdict
|
||||
from pyenphase.envoy import Envoy
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
@@ -70,14 +69,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
|
||||
for end_point in end_points:
|
||||
try:
|
||||
response: ClientResponse = await envoy.request(end_point)
|
||||
fixture_data[end_point] = (
|
||||
(await response.text()).replace("\n", "").replace(serial, CLEAN_TEXT)
|
||||
response = await envoy.request(end_point)
|
||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||
serial, CLEAN_TEXT
|
||||
)
|
||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||
{
|
||||
"headers": dict(response.headers.items()),
|
||||
"code": response.status,
|
||||
"code": response.status_code,
|
||||
}
|
||||
)
|
||||
except EnvoyError as err:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiohttp import ClientError
|
||||
from httpx import HTTPError
|
||||
from pyenphase import EnvoyData
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseUpdateCoordinator
|
||||
|
||||
ACTIONERRORS = (EnvoyError, ClientError)
|
||||
ACTIONERRORS = (EnvoyError, HTTPError)
|
||||
|
||||
|
||||
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.0.1"],
|
||||
"requirements": ["pyenphase==1.26.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -71,11 +71,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
_attr_name = "DHW controller"
|
||||
_attr_icon = "mdi:thermometer-lines"
|
||||
_attr_operation_list = list(HA_STATE_TO_EVO)
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
_evo_device: evo.HotWater
|
||||
@@ -96,6 +91,9 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
self._attr_precision = (
|
||||
PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
|
||||
@@ -11,25 +11,32 @@ from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONTENT_TYPE_MULTIPART,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
from homeassistant.util.system_info import is_official_image
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
|
||||
DATA_FFMPEG = "ffmpeg"
|
||||
|
||||
@@ -56,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the FFmpeg component."""
|
||||
@@ -65,7 +74,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
await manager.async_get_version()
|
||||
|
||||
async_setup_services(hass)
|
||||
# Register service
|
||||
async def async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.data[DATA_FFMPEG] = manager
|
||||
return True
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def _async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register FFmpeg services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.3"]
|
||||
}
|
||||
|
||||
@@ -5,11 +5,18 @@ import voluptuous as vol
|
||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "generic_hygrostat"
|
||||
@@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.options[CONF_HUMIDIFIER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||
# but not the humidity sensor because the generic_hygrostat adds itself to the
|
||||
# humidifier's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HUMIDIFIER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"""The generic_thermostat component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
from .const import CONF_HEATER, PLATFORMS
|
||||
from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.entry_id,
|
||||
entry.options[CONF_HEATER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HEATER: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||
# not the temperature sensor because the generic_hygrostat adds itself to the
|
||||
# heater's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HEATER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HEATER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import aiohttp
|
||||
from gassist_text import TextAssistant
|
||||
from google.oauth2.credentials import Credentials
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery, intent
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -23,9 +31,21 @@ from .helpers import (
|
||||
GoogleAssistantSDKConfigEntry,
|
||||
GoogleAssistantSDKRuntimeData,
|
||||
InMemoryStorage,
|
||||
async_send_text_commands,
|
||||
best_matching_language_code,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
|
||||
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
|
||||
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
|
||||
),
|
||||
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
|
||||
},
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -38,8 +58,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -63,6 +81,8 @@ async def async_setup_entry(
|
||||
mem_storage = InMemoryStorage(hass)
|
||||
hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage))
|
||||
|
||||
await async_setup_service(hass)
|
||||
|
||||
entry.runtime_data = GoogleAssistantSDKRuntimeData(
|
||||
session=session, mem_storage=mem_storage
|
||||
)
|
||||
@@ -85,6 +105,36 @@ async def async_unload_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Assistant SDK."""
|
||||
|
||||
async def send_text_command(call: ServiceCall) -> ServiceResponse:
|
||||
"""Send a text command to Google Assistant SDK."""
|
||||
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
||||
media_players: list[str] | None = call.data.get(
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
|
||||
)
|
||||
command_response_list = await async_send_text_commands(
|
||||
hass, commands, media_players
|
||||
)
|
||||
if call.return_response:
|
||||
return {
|
||||
"responses": [
|
||||
dataclasses.asdict(command_response)
|
||||
for command_response in command_response_list
|
||||
]
|
||||
}
|
||||
return None
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TEXT_COMMAND,
|
||||
send_text_command,
|
||||
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
|
||||
class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
||||
"""Google Assistant SDK conversation agent."""
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Support for Google Assistant SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
from .helpers import async_send_text_commands
|
||||
|
||||
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
|
||||
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
|
||||
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
|
||||
),
|
||||
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _send_text_command(call: ServiceCall) -> ServiceResponse:
|
||||
"""Send a text command to Google Assistant SDK."""
|
||||
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
||||
media_players: list[str] | None = call.data.get(
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
|
||||
)
|
||||
command_response_list = await async_send_text_commands(
|
||||
call.hass, commands, media_players
|
||||
)
|
||||
if call.return_response:
|
||||
return {
|
||||
"responses": [
|
||||
dataclasses.asdict(command_response)
|
||||
for command_response in command_response_list
|
||||
]
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Assistant SDK."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TEXT_COMMAND,
|
||||
_send_text_command,
|
||||
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
@@ -24,11 +24,9 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail integration."""
|
||||
"""Set up the Google Mail platform."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -54,6 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -7,26 +7,17 @@ from google_photos_library_api.api import GooglePhotosLibraryApi
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .services import async_register_services
|
||||
|
||||
__all__ = ["DOMAIN"]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Google Photos integration."""
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -57,6 +48,8 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register Google Photos services."""
|
||||
|
||||
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
|
||||
@@ -152,10 +152,11 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
}
|
||||
return None
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
@@ -2,33 +2,48 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
|
||||
from .const import DEFAULT_ACCESS, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate the Google Sheets component."""
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -52,6 +67,8 @@ async def async_setup_entry(
|
||||
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
|
||||
entry.runtime_data = session
|
||||
|
||||
await async_setup_service(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -64,4 +81,55 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for service_name in hass.services.async_services_for_domain(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
async def append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Support for Google Sheets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import GoogleSheetsConfigEntry
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
|
||||
async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
_async_append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
|
||||
from .entity import GroupEntity
|
||||
|
||||
DEFAULT_NAME = "Sensor Group"
|
||||
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return state_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_state_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return device_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_device_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if device_class:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
else:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_no_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
|
||||
@@ -9,8 +9,10 @@ from functools import partial
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import aiofiles
|
||||
from aiohasupervisor import SupervisorError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -37,6 +39,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery_flow,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.deprecation import (
|
||||
@@ -51,6 +54,7 @@ from homeassistant.helpers.hassio import (
|
||||
get_supervisor_ip as _get_supervisor_ip,
|
||||
is_hassio as _is_hassio,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service_info.hassio import (
|
||||
HassioServiceInfo as _HassioServiceInfo,
|
||||
)
|
||||
@@ -109,7 +113,7 @@ from .coordinator import (
|
||||
get_core_info, # noqa: F401
|
||||
get_core_stats, # noqa: F401
|
||||
get_host_info, # noqa: F401
|
||||
get_info, # noqa: F401
|
||||
get_info,
|
||||
get_issues_info, # noqa: F401
|
||||
get_os_info,
|
||||
get_supervisor_info, # noqa: F401
|
||||
@@ -168,6 +172,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||
|
||||
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||
|
||||
DEPRECATION_URL = (
|
||||
"https://www.home-assistant.io/blog/2025/05/22/"
|
||||
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
|
||||
)
|
||||
|
||||
|
||||
def valid_addon(value: Any) -> str:
|
||||
"""Validate value is a valid addon slug."""
|
||||
@@ -225,6 +234,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = await arch_file.read()
|
||||
return {"x86": "i386"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
class APIEndpointSettings(NamedTuple):
|
||||
"""Settings for API endpoint."""
|
||||
|
||||
@@ -546,6 +566,62 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
|
||||
arch = await _get_arch()
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
info = get_info(hass)
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
board = os_info.get("board")
|
||||
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
|
||||
unsupported_os_on_board = board in {"rpi3", "rpi4"}
|
||||
if is_haos and (unsupported_board or unsupported_os_on_board):
|
||||
issue_id = "deprecated_os_"
|
||||
if unsupported_os_on_board:
|
||||
issue_id += "aarch64"
|
||||
elif unsupported_board:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
bit32 = _is_32_bit()
|
||||
deprecated_architecture = bit32 and not (
|
||||
unsupported_board or unsupported_os_on_board
|
||||
)
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": "OS" if is_haos else "Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
listener()
|
||||
|
||||
listener = coordinator.async_add_listener(deprecated_setup_issue)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,26 +5,13 @@ from __future__ import annotations
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
TRAVEL_MODE_PUBLIC,
|
||||
)
|
||||
from .const import TRAVEL_MODE_PUBLIC
|
||||
from .coordinator import (
|
||||
HereConfigEntry,
|
||||
HERERoutingDataUpdateCoordinator,
|
||||
HERETransitDataUpdateCoordinator,
|
||||
)
|
||||
from .model import HERETravelTimeConfig
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -33,29 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry)
|
||||
"""Set up HERE Travel Time from a config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
|
||||
arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, ""))
|
||||
departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, ""))
|
||||
|
||||
here_travel_time_config = HERETravelTimeConfig(
|
||||
destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
|
||||
destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
|
||||
destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
|
||||
origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
|
||||
origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
|
||||
origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=config_entry.options[CONF_ROUTE_MODE],
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
|
||||
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
|
||||
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
|
||||
cls = HERETransitDataUpdateCoordinator
|
||||
else:
|
||||
cls = HERERoutingDataUpdateCoordinator
|
||||
|
||||
data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config)
|
||||
data_coordinator = cls(hass, config_entry, api_key)
|
||||
config_entry.runtime_data = data_coordinator
|
||||
|
||||
async def _async_update_at_start(_: HomeAssistant) -> None:
|
||||
|
||||
@@ -26,7 +26,7 @@ from here_transit import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfLength
|
||||
from homeassistant.const import CONF_MODE, UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
@@ -34,8 +34,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
|
||||
from .model import HERETravelTimeConfig, HERETravelTimeData
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTE_MODE_FASTEST,
|
||||
)
|
||||
from .model import HERETravelTimeAPIParams, HERETravelTimeData
|
||||
|
||||
BACKOFF_MULTIPLIER = 1.1
|
||||
|
||||
@@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[
|
||||
|
||||
|
||||
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
|
||||
"""here_routing DataUpdateCoordinator."""
|
||||
"""HERETravelTime DataUpdateCoordinator for the routing API."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERERoutingApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if self.config.route_mode == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
||||
" mode: %s, arrival: %s, departure: %s"
|
||||
),
|
||||
origin,
|
||||
destination,
|
||||
route_mode,
|
||||
TransportMode(self.config.travel_mode),
|
||||
arrival,
|
||||
departure,
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.route_mode,
|
||||
TransportMode(params.travel_mode),
|
||||
params.arrival,
|
||||
params.departure,
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self._api.route(
|
||||
transport_mode=TransportMode(self.config.travel_mode),
|
||||
origin=here_routing.Place(origin[0], origin[1]),
|
||||
destination=here_routing.Place(destination[0], destination[1]),
|
||||
routing_mode=route_mode,
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
transport_mode=TransportMode(params.travel_mode),
|
||||
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
||||
destination=here_routing.Place(
|
||||
params.destination[0], params.destination[1]
|
||||
),
|
||||
routing_mode=params.route_mode,
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
return_values=[Return.POLYINE, Return.SUMMARY],
|
||||
spans=[Spans.NAMES],
|
||||
)
|
||||
@@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
class HERETransitDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[HERETravelTimeData | None]
|
||||
):
|
||||
"""HERETravelTime DataUpdateCoordinator."""
|
||||
"""HERETravelTime DataUpdateCoordinator for the transit API."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator(
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERETransitApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData | None:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
|
||||
" departure: %s"
|
||||
),
|
||||
origin,
|
||||
destination,
|
||||
arrival,
|
||||
departure,
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.arrival,
|
||||
params.departure,
|
||||
)
|
||||
try:
|
||||
response = await self._api.route(
|
||||
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
|
||||
destination=here_transit.Place(
|
||||
latitude=destination[0], longitude=destination[1]
|
||||
origin=here_transit.Place(
|
||||
latitude=params.origin[0], longitude=params.origin[1]
|
||||
),
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
destination=here_transit.Place(
|
||||
latitude=params.destination[0], longitude=params.destination[1]
|
||||
),
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
return_values=[
|
||||
here_transit.Return.POLYLINE,
|
||||
here_transit.Return.TRAVEL_SUMMARY,
|
||||
@@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator(
|
||||
|
||||
def prepare_parameters(
|
||||
hass: HomeAssistant,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> tuple[list[str], list[str], str | None, str | None]:
|
||||
config_entry: HereConfigEntry,
|
||||
) -> HERETravelTimeAPIParams:
|
||||
"""Prepare parameters for the HERE api."""
|
||||
|
||||
def _from_entity_id(entity_id: str) -> list[str]:
|
||||
@@ -305,32 +308,55 @@ def prepare_parameters(
|
||||
return formatted_coordinates
|
||||
|
||||
# Destination
|
||||
if config.destination_entity_id is not None:
|
||||
destination = _from_entity_id(config.destination_entity_id)
|
||||
if (
|
||||
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
|
||||
) is not None:
|
||||
destination = _from_entity_id(str(destination_entity_id))
|
||||
else:
|
||||
destination = [
|
||||
str(config.destination_latitude),
|
||||
str(config.destination_longitude),
|
||||
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
|
||||
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
|
||||
]
|
||||
|
||||
# Origin
|
||||
if config.origin_entity_id is not None:
|
||||
origin = _from_entity_id(config.origin_entity_id)
|
||||
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
|
||||
origin = _from_entity_id(str(origin_entity_id))
|
||||
else:
|
||||
origin = [
|
||||
str(config.origin_latitude),
|
||||
str(config.origin_longitude),
|
||||
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
|
||||
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
|
||||
]
|
||||
|
||||
# Arrival/Departure
|
||||
arrival: str | None = None
|
||||
departure: str | None = None
|
||||
if config.arrival is not None:
|
||||
arrival = next_datetime(config.arrival).isoformat()
|
||||
if config.departure is not None:
|
||||
departure = next_datetime(config.departure).isoformat()
|
||||
arrival: datetime | None = None
|
||||
if (
|
||||
conf_arrival := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_ARRIVAL_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
arrival = next_datetime(conf_arrival)
|
||||
departure: datetime | None = None
|
||||
if (
|
||||
conf_departure := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_DEPARTURE_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
departure = next_datetime(conf_departure)
|
||||
|
||||
return (origin, destination, arrival, departure)
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
|
||||
return HERETravelTimeAPIParams(
|
||||
destination=destination,
|
||||
origin=origin,
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=route_mode,
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
|
||||
|
||||
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
@@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict):
|
||||
|
||||
|
||||
@dataclass
|
||||
class HERETravelTimeConfig:
|
||||
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
|
||||
class HERETravelTimeAPIParams:
|
||||
"""Configuration for polling the HERE API."""
|
||||
|
||||
destination_latitude: float | None
|
||||
destination_longitude: float | None
|
||||
destination_entity_id: str | None
|
||||
origin_latitude: float | None
|
||||
origin_longitude: float | None
|
||||
origin_entity_id: str | None
|
||||
destination: list[str]
|
||||
origin: list[str]
|
||||
travel_mode: str
|
||||
route_mode: str
|
||||
arrival: time | None
|
||||
departure: time | None
|
||||
arrival: datetime | None
|
||||
departure: datetime | None
|
||||
|
||||
@@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
|
||||
@@ -51,6 +53,30 @@ async def async_setup_entry(
|
||||
entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
# history_stats does not allow replacing the input entity.
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_ENTITY_ID]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ OPTIONS_FLOW = {
|
||||
}
|
||||
|
||||
|
||||
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for History stats."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -73,9 +73,7 @@ async def async_setup_entry(
|
||||
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
|
||||
"""Hive Water Heater Device."""
|
||||
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_operation_list = SUPPORT_WATER_HEATER
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config as conf_util, core_config
|
||||
@@ -38,7 +40,6 @@ from homeassistant.helpers import (
|
||||
restore_state,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service import (
|
||||
async_extract_config_entry_ids,
|
||||
@@ -95,6 +96,17 @@ DEPRECATION_URL = (
|
||||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = (await arch_file.read()).strip()
|
||||
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
||||
@@ -402,79 +414,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
info = await async_get_system_info(hass)
|
||||
|
||||
installation_type = info["installation_type"][15:]
|
||||
deprecated_method = installation_type in {
|
||||
"Core",
|
||||
"Supervised",
|
||||
}
|
||||
arch = info["arch"]
|
||||
if arch == "armv7":
|
||||
if installation_type == "OS":
|
||||
# Local import to avoid circular dependencies
|
||||
# We use the import helper because hassio
|
||||
# may not be loaded yet and we don't want to
|
||||
# do blocking I/O in the event loop to import it.
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components import hassio
|
||||
else:
|
||||
hassio = await async_import_module(
|
||||
hass, "homeassistant.components.hassio"
|
||||
)
|
||||
os_info = hassio.get_os_info(hass)
|
||||
assert os_info is not None
|
||||
issue_id = "deprecated_os_"
|
||||
board = os_info.get("board")
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
if installation_type in {"Core", "Container"}:
|
||||
deprecated_method = installation_type == "Core"
|
||||
bit32 = _is_32_bit()
|
||||
arch = info["arch"]
|
||||
if bit32 and installation_type == "Container":
|
||||
arch = await _get_arch()
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_container",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container",
|
||||
translation_placeholders={"arch": arch},
|
||||
)
|
||||
deprecated_architecture = bit32 and installation_type != "Container"
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
elif installation_type == "Container":
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_container_armv7",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container_armv7",
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -107,9 +107,9 @@
|
||||
"title": "Deprecation notice: 32-bit architecture",
|
||||
"description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_container_armv7": {
|
||||
"deprecated_container": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
"description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_os_aarch64": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
|
||||
@@ -215,17 +215,17 @@ class HomeAssistantSkyConnectOptionsFlowHandler(
|
||||
"""Instantiate options flow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._usb_info = get_usb_service_info(self._config_entry)
|
||||
self._usb_info = get_usb_service_info(self.config_entry)
|
||||
self._hw_variant = HardwareVariant.from_usb_product_name(
|
||||
self._config_entry.data[PRODUCT]
|
||||
self.config_entry.data[PRODUCT]
|
||||
)
|
||||
self._hardware_name = self._hw_variant.full_name
|
||||
self._device = self._usb_info.device
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self._config_entry.data[FIRMWARE]),
|
||||
firmware_version=self._config_entry.data[FIRMWARE_VERSION],
|
||||
firmware_type=ApplicationType(self.config_entry.data[FIRMWARE]),
|
||||
firmware_version=self.config_entry.data[FIRMWARE_VERSION],
|
||||
source="guess",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
@@ -288,7 +288,7 @@ class HomeAssistantYellowOptionsFlowHandler(
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self._config_entry.data["firmware"]),
|
||||
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
|
||||
firmware_version=None,
|
||||
source="guess",
|
||||
owners=[],
|
||||
|
||||
@@ -27,7 +27,6 @@ PLATFORMS = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
Platform.VALVE,
|
||||
]
|
||||
|
||||
@@ -83,7 +83,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
|
||||
self._heating_mode is not None
|
||||
):
|
||||
if self._heating_mode.current_value == self._heating_mode.minimum:
|
||||
if self._heating_mode.current_value == 0:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.HEAT
|
||||
@@ -91,10 +91,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the hvac action."""
|
||||
if (
|
||||
self._heating_mode is not None
|
||||
and self._heating_mode.current_value == self._heating_mode.minimum
|
||||
):
|
||||
if self._heating_mode is not None and self._heating_mode.current_value == 0:
|
||||
return HVACAction.OFF
|
||||
|
||||
if (
|
||||
@@ -113,12 +110,10 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
if (
|
||||
ClimateEntityFeature.PRESET_MODE in self.supported_features
|
||||
and self._heating_mode is not None
|
||||
and self._heating_mode.current_value > self._heating_mode.minimum
|
||||
and self._heating_mode.current_value > 0
|
||||
):
|
||||
assert self._attr_preset_modes is not None
|
||||
return self._attr_preset_modes[
|
||||
int(self._heating_mode.current_value - self._heating_mode.minimum) - 1
|
||||
]
|
||||
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@@ -152,16 +147,14 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
# Currently only HEAT and OFF are supported.
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode,
|
||||
(hvac_mode == HVACMode.HEAT) + self._heating_mode.minimum,
|
||||
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
assert self._heating_mode is not None and self._attr_preset_modes is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode,
|
||||
self._attr_preset_modes.index(preset_mode) + self._heating_mode.minimum + 1,
|
||||
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
@@ -175,16 +168,12 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, 1 + self._heating_mode.minimum
|
||||
)
|
||||
await self.async_set_homee_value(self._heating_mode, 1)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, 0 + self._heating_mode.minimum
|
||||
)
|
||||
await self.async_set_homee_value(self._heating_mode, 0)
|
||||
|
||||
|
||||
def get_climate_features(
|
||||
@@ -204,10 +193,7 @@ def get_climate_features(
|
||||
if attribute.maximum > 1:
|
||||
# Node supports more modes than off and heating.
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
if attribute.maximum < 5:
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
else:
|
||||
preset_modes.extend([PRESET_ECO])
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
|
||||
if len(preset_modes) > 0:
|
||||
preset_modes.insert(0, PRESET_NONE)
|
||||
|
||||
@@ -83,54 +83,3 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reconfigure flow."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
reconfigure_entry.data[CONF_USERNAME],
|
||||
reconfigure_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
self.homee.disconnect()
|
||||
await self.homee.wait_until_disconnected()
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_hub")
|
||||
|
||||
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
|
||||
): str
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": reconfigure_entry.runtime_data.settings.uid
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Diagnostics for homee integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import DOMAIN, HomeeConfigEntry
|
||||
|
||||
TO_REDACT = [CONF_PASSWORD, CONF_USERNAME, "latitude", "longitude", "wlan_ssid"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"settings": async_redact_data(entry.runtime_data.settings.raw_data, TO_REDACT),
|
||||
"devices": [{"node": node.raw_data} for node in entry.runtime_data.nodes],
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
# Extract node_id from the device identifiers
|
||||
split_uid = next(
|
||||
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
|
||||
).split("-")
|
||||
# Homee hub itself only has MAC as identifier and a node_id of -1
|
||||
node_id = -1 if len(split_uid) < 2 else split_uid[1]
|
||||
|
||||
node = entry.runtime_data.get_node_by_id(int(node_id))
|
||||
assert node is not None
|
||||
return {
|
||||
"homee node": node.raw_data,
|
||||
}
|
||||
@@ -31,22 +31,6 @@ class HomeeNumberEntityDescription(NumberEntityDescription):
|
||||
|
||||
|
||||
NUMBER_DESCRIPTIONS = {
|
||||
AttributeType.BUTTON_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
|
||||
key="button_brightness_active",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.BUTTON_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
|
||||
key="button_brightness_dimmed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DISPLAY_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
|
||||
key="display_brightness_active",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DISPLAY_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
|
||||
key="display_brightness_dimmed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
|
||||
key="down_position",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -64,14 +48,6 @@ NUMBER_DESCRIPTIONS = {
|
||||
key="endposition_configuration",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.EXTERNAL_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
|
||||
key="external_temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.FLOOR_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
|
||||
key="floor_temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
|
||||
key="motion_alarm_cancelation_delay",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -107,11 +83,6 @@ NUMBER_DESCRIPTIONS = {
|
||||
key="temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.TEMPERATURE_REPORT_INTERVAL: HomeeNumberEntityDescription(
|
||||
key="temperature_report_interval",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.UP_TIME: HomeeNumberEntityDescription(
|
||||
key="up_time",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
|
||||
@@ -14,11 +14,6 @@ from .entity import HomeeEntity
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
|
||||
AttributeType.DISPLAY_TEMPERATURE_SELECTION: SelectEntityDescription(
|
||||
key="display_temperature_selection",
|
||||
options=["target", "current"],
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.REPEATER_MODE: SelectEntityDescription(
|
||||
key="repeater_mode",
|
||||
options=["off", "level1", "level2"],
|
||||
|
||||
@@ -129,16 +129,6 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.EXTERNAL_TEMPERATURE: HomeeSensorEntityDescription(
|
||||
key="external_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.FLOOR_TEMPERATURE: HomeeSensorEntityDescription(
|
||||
key="floor_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.INDOOR_RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
|
||||
key="indoor_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""The homee siren platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
|
||||
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add siren entities for homee."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeSiren(attribute, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type == AttributeType.SIREN
|
||||
)
|
||||
|
||||
|
||||
class HomeeSiren(HomeeEntity, SirenEntity):
|
||||
"""Representation of a homee siren device."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the siren."""
|
||||
return self._attribute.current_value == 1.0
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on."""
|
||||
await self.async_set_homee_value(1)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren off."""
|
||||
await self.async_set_homee_value(0)
|
||||
@@ -2,9 +2,7 @@
|
||||
"config": {
|
||||
"flow_title": "homee {name} ({host})",
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_hub": "Address belongs to a different homee."
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -24,16 +22,6 @@
|
||||
"username": "The username for your homee.",
|
||||
"password": "The password for your homee."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure homee {name}",
|
||||
"description": "Reconfigure the IP address of your homee.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your homee."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -199,18 +187,6 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"button_brightness_active": {
|
||||
"name": "Button brightness (active)"
|
||||
},
|
||||
"button_brightness_dimmed": {
|
||||
"name": "Button brightness (dimmed)"
|
||||
},
|
||||
"display_brightness_active": {
|
||||
"name": "Display brightness (active)"
|
||||
},
|
||||
"display_brightness_dimmed": {
|
||||
"name": "Display brightness (dimmed)"
|
||||
},
|
||||
"down_position": {
|
||||
"name": "Down position"
|
||||
},
|
||||
@@ -223,12 +199,6 @@
|
||||
"endposition_configuration": {
|
||||
"name": "End position"
|
||||
},
|
||||
"external_temperature_offset": {
|
||||
"name": "External temperature offset"
|
||||
},
|
||||
"floor_temperature_offset": {
|
||||
"name": "Floor temperature offset"
|
||||
},
|
||||
"motion_alarm_cancelation_delay": {
|
||||
"name": "Motion alarm delay"
|
||||
},
|
||||
@@ -253,9 +223,6 @@
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
"temperature_report_interval": {
|
||||
"name": "Temperature report interval"
|
||||
},
|
||||
"up_time": {
|
||||
"name": "Up-movement duration"
|
||||
},
|
||||
@@ -267,13 +234,6 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"display_temperature_selection": {
|
||||
"name": "Displayed temperature",
|
||||
"state": {
|
||||
"target": "Target",
|
||||
"current": "Measured"
|
||||
}
|
||||
},
|
||||
"repeater_mode": {
|
||||
"name": "Repeater mode",
|
||||
"state": {
|
||||
@@ -305,12 +265,6 @@
|
||||
"exhaust_motor_revs": {
|
||||
"name": "Exhaust motor speed"
|
||||
},
|
||||
"external_temperature": {
|
||||
"name": "External temperature"
|
||||
},
|
||||
"floor_temperature": {
|
||||
"name": "Floor temperature"
|
||||
},
|
||||
"indoor_humidity": {
|
||||
"name": "Indoor humidity"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.15"],
|
||||
"requirements": ["aiohomekit==3.2.14"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ from .const import (
|
||||
HMIPC_NAME,
|
||||
)
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .services import async_setup_services
|
||||
from .services import async_setup_services, async_unload_services
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -63,8 +63,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -85,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
if not await hap.async_setup():
|
||||
return False
|
||||
|
||||
await async_setup_services(hass)
|
||||
_async_remove_obsolete_entities(hass, entry, hap)
|
||||
|
||||
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
|
||||
@@ -116,6 +115,8 @@ async def async_unload_entry(
|
||||
assert hap.reset_connection_listener is not None
|
||||
hap.reset_connection_listener()
|
||||
|
||||
await async_unload_services(hass)
|
||||
|
||||
return await hap.async_reset()
|
||||
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ class HomematicipHAP:
|
||||
self.config_entry.data.get(HMIPC_AUTHTOKEN),
|
||||
self.config_entry.data.get(HMIPC_NAME),
|
||||
)
|
||||
|
||||
except HmipcConnectionError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except Exception as err: # noqa: BLE001
|
||||
@@ -210,41 +211,13 @@ class HomematicipHAP:
|
||||
for device in self.home.devices:
|
||||
device.fire_update_event()
|
||||
|
||||
async def async_connect(self) -> None:
|
||||
"""Start WebSocket connection."""
|
||||
tries = 0
|
||||
while True:
|
||||
retry_delay = 2 ** min(tries, 8)
|
||||
async def async_connect(self, home: AsyncHome) -> None:
|
||||
"""Connect to HomematicIP Cloud Websocket."""
|
||||
await home.enable_events()
|
||||
|
||||
try:
|
||||
await self.home.get_current_state_async()
|
||||
hmip_events = self.home.enable_events()
|
||||
self.home.set_on_connected_handler(self.ws_connected_handler)
|
||||
self.home.set_on_disconnected_handler(self.ws_disconnected_handler)
|
||||
tries = 0
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Error connecting to HomematicIP with HAP %s. "
|
||||
"Retrying in %d seconds"
|
||||
),
|
||||
self.config_entry.unique_id,
|
||||
retry_delay,
|
||||
)
|
||||
|
||||
if self._ws_close_requested:
|
||||
break
|
||||
self._ws_close_requested = False
|
||||
tries += 1
|
||||
|
||||
try:
|
||||
self._retry_task = self.hass.async_create_task(
|
||||
asyncio.sleep(retry_delay)
|
||||
)
|
||||
await self._retry_task
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
home.set_on_connected_handler(self.ws_connected_handler)
|
||||
home.set_on_disconnected_handler(self.ws_disconnected_handler)
|
||||
home.set_on_reconnect_handler(self.ws_reconnected_handler)
|
||||
|
||||
async def async_reset(self) -> bool:
|
||||
"""Close the websocket connection."""
|
||||
@@ -272,14 +245,22 @@ class HomematicipHAP:
|
||||
|
||||
async def ws_connected_handler(self) -> None:
|
||||
"""Handle websocket connected."""
|
||||
_LOGGER.debug("WebSocket connection to HomematicIP established")
|
||||
_LOGGER.info("Websocket connection to HomematicIP Cloud established")
|
||||
if self._ws_connection_closed.is_set():
|
||||
await self.get_state()
|
||||
self._ws_connection_closed.clear()
|
||||
|
||||
async def ws_disconnected_handler(self) -> None:
|
||||
"""Handle websocket disconnection."""
|
||||
_LOGGER.warning("WebSocket connection to HomematicIP closed")
|
||||
_LOGGER.warning("Websocket connection to HomematicIP Cloud closed")
|
||||
self._ws_connection_closed.set()
|
||||
|
||||
async def ws_reconnected_handler(self, reason: str) -> None:
|
||||
"""Handle websocket reconnection."""
|
||||
_LOGGER.info(
|
||||
"Websocket connection to HomematicIP Cloud re-established due to reason: %s",
|
||||
reason,
|
||||
)
|
||||
self._ws_connection_closed.set()
|
||||
|
||||
async def get_hap(
|
||||
@@ -306,6 +287,6 @@ class HomematicipHAP:
|
||||
home.on_update(self.async_update)
|
||||
home.on_create(self.async_create_entity)
|
||||
|
||||
hass.loop.create_task(self.async_connect())
|
||||
await self.async_connect(home)
|
||||
|
||||
return home
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.4"]
|
||||
"requirements": ["homematicip==2.0.5"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user