mirror of
https://github.com/home-assistant/core.git
synced 2026-05-03 19:41:15 +02:00
Compare commits
163 Commits
2023.10.0b0
...
2023.10.2
| Author | SHA1 | Date | |
|---|---|---|---|
| f5b5215247 | |||
| 014546c75e | |||
| b0dabfa3f7 | |||
| ca1d6ddbb6 | |||
| 7670b5d3b0 | |||
| 04dc44c069 | |||
| c2cf497302 | |||
| 34693d4a9b | |||
| c9b9851605 | |||
| 3b13c9129a | |||
| ffe60102fd | |||
| 5946681454 | |||
| eae6f9b0f8 | |||
| 959d21a576 | |||
| 785df0c8e1 | |||
| 62805aed2b | |||
| 1a2c9fd9a9 | |||
| f0a1977d2e | |||
| 8b3fc107df | |||
| 417ba3644b | |||
| 49f060d95b | |||
| ed57d0beac | |||
| d7a36cb6a4 | |||
| 887263d80e | |||
| c4737e4423 | |||
| 327e6d2362 | |||
| e32044f884 | |||
| dbc3382dfb | |||
| a042703dd7 | |||
| 8109c77f6a | |||
| 2639602f5b | |||
| d5c26beb91 | |||
| f24843f211 | |||
| 5f0bf4e2a3 | |||
| bab524f264 | |||
| ede7d13c1e | |||
| db91e9a720 | |||
| c11dd58c1d | |||
| a6edfa85b1 | |||
| b3080ae005 | |||
| 5925b6b912 | |||
| 42b53c6349 | |||
| d26b1b370a | |||
| 7369ae8c9f | |||
| 6c2d1e2142 | |||
| 4a5b0222ab | |||
| 76f78e249b | |||
| 81f582eeb7 | |||
| 7f6506cfcf | |||
| d14934861e | |||
| d469626855 | |||
| 9725a0daf9 | |||
| 26c7ba38d0 | |||
| 948bbdd2bf | |||
| eadc70ede0 | |||
| 2210db4ca6 | |||
| c5585b0706 | |||
| 37cfa5efb7 | |||
| a506ba94d1 | |||
| f8c7d502df | |||
| f0cb2ba005 | |||
| f7ab00a8bf | |||
| e8c38fe99e | |||
| a4f0da8286 | |||
| 223f3a434b | |||
| 10e43048bd | |||
| 2345a2be5f | |||
| 7dfb397aef | |||
| 22bf1a0582 | |||
| 01daae69ab | |||
| 512b2af13c | |||
| 8e05df2b44 | |||
| 0470ca3e76 | |||
| ebde9914f2 | |||
| 337f9197bb | |||
| 9b9a16e9c6 | |||
| 55ff8e1fcb | |||
| 937a26117c | |||
| 776b26de3f | |||
| b9a929e63b | |||
| 9c5d9344e2 | |||
| 38423ad6f1 | |||
| 9e4f9a88ad | |||
| e0cbbf7d57 | |||
| fd6eb61489 | |||
| a9bc380c32 | |||
| be32db70a0 | |||
| 0e29ccf069 | |||
| 9834c1de9a | |||
| 791293ca87 | |||
| 98d7945521 | |||
| 06d6122663 | |||
| bad9b1c95f | |||
| a0e5f016e1 | |||
| 5370db4a3e | |||
| b069f92d95 | |||
| 9b810dcf9f | |||
| 3cfae48577 | |||
| ad53ff037e | |||
| 63b5ba6b3a | |||
| e76396b184 | |||
| ebf8061117 | |||
| ced616fafa | |||
| 18f3fb42c9 | |||
| bfe16e2536 | |||
| 98ca71fc96 | |||
| 93033e037d | |||
| cfa923252b | |||
| 8c84237e6b | |||
| cf6f0cf266 | |||
| b24f09b47e | |||
| e0d7c1440b | |||
| b20f9c40be | |||
| b27097808d | |||
| d7fa98454b | |||
| 531479bf5b | |||
| 3941d2c897 | |||
| c4d85ac41f | |||
| 5106907571 | |||
| 04829f0a1b | |||
| 01182e8a5c | |||
| af041d2900 | |||
| 822af4d40d | |||
| 730acb34f2 | |||
| b5eb158697 | |||
| d216fbddae | |||
| d84d83a42a | |||
| c1ade85d65 | |||
| 124eda6906 | |||
| 65c7b30720 | |||
| 1d2c570a01 | |||
| 2cc229ce42 | |||
| 73356ae232 | |||
| bfd7275972 | |||
| ef3bd0100c | |||
| 3f57c33f32 | |||
| bae3379938 | |||
| 85838c6af9 | |||
| 9c0bc57fed | |||
| 97448eff8f | |||
| 2f6fefefa7 | |||
| d8f96d7709 | |||
| fff3c6c6e9 | |||
| 17362e1954 | |||
| 1bbd4662b7 | |||
| ad8033c0f2 | |||
| 081f194f6a | |||
| f13059eaf5 | |||
| 0147108b89 | |||
| ffad30734b | |||
| 5bd306392f | |||
| d6c42ee8e7 | |||
| 35eaebd182 | |||
| 81e8ca130f | |||
| 9ab340047d | |||
| b02f64196b | |||
| af37de46bd | |||
| be93793db9 | |||
| c287564e68 | |||
| 115c3d6e49 | |||
| 415042f356 | |||
| dde4b07c29 | |||
| 10e8173d4e |
@@ -898,6 +898,7 @@ omit =
|
||||
homeassistant/components/opengarage/cover.py
|
||||
homeassistant/components/opengarage/entity.py
|
||||
homeassistant/components/opengarage/sensor.py
|
||||
homeassistant/components/openhardwaremonitor/sensor.py
|
||||
homeassistant/components/openhome/__init__.py
|
||||
homeassistant/components/openhome/const.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
|
||||
@@ -738,8 +738,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/matrix/ @PaarthShah
|
||||
/homeassistant/components/matter/ @home-assistant/matter
|
||||
/tests/components/matter/ @home-assistant/matter
|
||||
/homeassistant/components/mazda/ @bdr99
|
||||
/tests/components/mazda/ @bdr99
|
||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||
/tests/components/meater/ @Sotolotl @emontnemery
|
||||
/homeassistant/components/medcom_ble/ @elafargue
|
||||
|
||||
+3
-6
@@ -15,9 +15,8 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
|
||||
@@ -39,9 +38,8 @@ RUN \
|
||||
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
@@ -49,9 +47,8 @@ COPY . homeassistant/
|
||||
RUN \
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
|
||||
-e ./homeassistant \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.08.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.08.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.09.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.09.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.09.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.09.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.09.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -125,6 +125,13 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the current target temperature."""
|
||||
# If the system is in MyZone mode, and a zone is set, return that temperature instead.
|
||||
if (
|
||||
self._ac["myZone"] > 0
|
||||
and not self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED)
|
||||
and not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED)
|
||||
):
|
||||
return self._myzone["setTemp"]
|
||||
return self._ac["setTemp"]
|
||||
|
||||
@property
|
||||
|
||||
@@ -62,6 +62,12 @@ class AdvantageAirAcEntity(AdvantageAirEntity):
|
||||
def _ac(self) -> dict[str, Any]:
|
||||
return self.coordinator.data["aircons"][self.ac_key]["info"]
|
||||
|
||||
@property
|
||||
def _myzone(self) -> dict[str, Any]:
|
||||
return self.coordinator.data["aircons"][self.ac_key]["zones"].get(
|
||||
f"z{self._ac['myZone']:02}"
|
||||
)
|
||||
|
||||
|
||||
class AdvantageAirZoneEntity(AdvantageAirAcEntity):
|
||||
"""Parent class for Advantage Air Zone Entities."""
|
||||
|
||||
@@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
|
||||
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
|
||||
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
|
||||
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
|
||||
await self._async_update_hvac_params(params)
|
||||
|
||||
@callback
|
||||
@@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
self._attr_target_temperature_high = self.get_airzone_value(
|
||||
AZD_HEAT_TEMP_SET
|
||||
)
|
||||
self._attr_target_temperature_low = self.get_airzone_value(
|
||||
AZD_COOL_TEMP_SET
|
||||
)
|
||||
self._attr_target_temperature_low = self.get_airzone_value(
|
||||
AZD_HEAT_TEMP_SET
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.2.2"]
|
||||
"requirements": ["aioairzone-cloud==0.2.3"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.components import stt
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_CONFIG, DOMAIN
|
||||
from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DOMAIN
|
||||
from .error import PipelineNotFound
|
||||
from .pipeline import (
|
||||
AudioSettings,
|
||||
@@ -45,7 +45,9 @@ __all__ = (
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{vol.Optional("debug_recording_dir"): str},
|
||||
{
|
||||
vol.Optional(CONF_DEBUG_RECORDING_DIR): str,
|
||||
},
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
|
||||
@@ -2,3 +2,12 @@
|
||||
DOMAIN = "assist_pipeline"
|
||||
|
||||
DATA_CONFIG = f"{DOMAIN}.config"
|
||||
|
||||
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
|
||||
|
||||
DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds
|
||||
|
||||
CONF_DEBUG_RECORDING_DIR = "debug_recording_dir"
|
||||
|
||||
DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up"
|
||||
DEFAULT_WAKE_WORD_COOLDOWN = 2 # seconds
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["webrtc-noise-gain==1.2.1"]
|
||||
"requirements": ["webrtc-noise-gain==1.2.3"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import array
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import StrEnum
|
||||
@@ -48,7 +48,13 @@ from homeassistant.util import (
|
||||
)
|
||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||
|
||||
from .const import DATA_CONFIG, DOMAIN
|
||||
from .const import (
|
||||
CONF_DEBUG_RECORDING_DIR,
|
||||
DATA_CONFIG,
|
||||
DATA_LAST_WAKE_UP,
|
||||
DEFAULT_WAKE_WORD_COOLDOWN,
|
||||
DOMAIN,
|
||||
)
|
||||
from .error import (
|
||||
IntentRecognitionError,
|
||||
PipelineError,
|
||||
@@ -399,6 +405,9 @@ class WakeWordSettings:
|
||||
audio_seconds_to_buffer: float = 0
|
||||
"""Seconds of audio to buffer before detection and forward to STT."""
|
||||
|
||||
cooldown_seconds: float = DEFAULT_WAKE_WORD_COOLDOWN
|
||||
"""Seconds after a wake word detection where other detections are ignored."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AudioSettings:
|
||||
@@ -475,7 +484,7 @@ class PipelineRun:
|
||||
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False, repr=False)
|
||||
tts_engine: str = field(init=False, repr=False)
|
||||
tts_options: dict | None = field(init=False, default=None)
|
||||
wake_word_entity_id: str = field(init=False, repr=False)
|
||||
wake_word_entity_id: str | None = field(init=False, default=None, repr=False)
|
||||
wake_word_entity: wake_word.WakeWordDetectionEntity = field(init=False, repr=False)
|
||||
|
||||
abort_wake_word_detection: bool = field(init=False, default=False)
|
||||
@@ -518,6 +527,13 @@ class PipelineRun:
|
||||
self.audio_settings.noise_suppression_level,
|
||||
)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Compare pipeline runs by id."""
|
||||
if isinstance(other, PipelineRun):
|
||||
return self.id == other.id
|
||||
|
||||
return False
|
||||
|
||||
@callback
|
||||
def process_event(self, event: PipelineEvent) -> None:
|
||||
"""Log an event and call listener."""
|
||||
@@ -596,6 +612,8 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
wake_word_settings = self.wake_word_settings or WakeWordSettings()
|
||||
|
||||
# Remove language since it doesn't apply to wake words yet
|
||||
metadata_dict.pop("language", None)
|
||||
|
||||
@@ -605,6 +623,7 @@ class PipelineRun:
|
||||
{
|
||||
"entity_id": self.wake_word_entity_id,
|
||||
"metadata": metadata_dict,
|
||||
"timeout": wake_word_settings.timeout or 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -612,8 +631,6 @@ class PipelineRun:
|
||||
if self.debug_recording_queue is not None:
|
||||
self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_entity_id}")
|
||||
|
||||
wake_word_settings = self.wake_word_settings or WakeWordSettings()
|
||||
|
||||
wake_word_vad: VoiceActivityTimeout | None = None
|
||||
if (wake_word_settings.timeout is not None) and (
|
||||
wake_word_settings.timeout > 0
|
||||
@@ -663,6 +680,17 @@ class PipelineRun:
|
||||
if result is None:
|
||||
wake_word_output: dict[str, Any] = {}
|
||||
else:
|
||||
# Avoid duplicate detections by checking cooldown
|
||||
last_wake_up = self.hass.data.get(DATA_LAST_WAKE_UP)
|
||||
if last_wake_up is not None:
|
||||
sec_since_last_wake_up = time.monotonic() - last_wake_up
|
||||
if sec_since_last_wake_up < wake_word_settings.cooldown_seconds:
|
||||
_LOGGER.debug("Duplicate wake word detection occurred")
|
||||
raise WakeWordDetectionAborted
|
||||
|
||||
# Record last wake up time to block duplicate detections
|
||||
self.hass.data[DATA_LAST_WAKE_UP] = time.monotonic()
|
||||
|
||||
if result.queued_audio:
|
||||
# Add audio that was pending at detection.
|
||||
#
|
||||
@@ -1025,7 +1053,7 @@ class PipelineRun:
|
||||
# Directory to save audio for each pipeline run.
|
||||
# Configured in YAML for assist_pipeline.
|
||||
if debug_recording_dir := self.hass.data[DATA_CONFIG].get(
|
||||
"debug_recording_dir"
|
||||
CONF_DEBUG_RECORDING_DIR
|
||||
):
|
||||
if device_id is None:
|
||||
# <debug_recording_dir>/<pipeline.name>/<run.id>
|
||||
@@ -1565,21 +1593,19 @@ class PipelineRuns:
|
||||
|
||||
def __init__(self, pipeline_store: PipelineStorageCollection) -> None:
|
||||
"""Initialize."""
|
||||
self._pipeline_runs: dict[str, list[PipelineRun]] = {}
|
||||
self._pipeline_runs: dict[str, dict[str, PipelineRun]] = defaultdict(dict)
|
||||
self._pipeline_store = pipeline_store
|
||||
pipeline_store.async_add_listener(self._change_listener)
|
||||
|
||||
def add_run(self, pipeline_run: PipelineRun) -> None:
|
||||
"""Add pipeline run."""
|
||||
pipeline_id = pipeline_run.pipeline.id
|
||||
if pipeline_id not in self._pipeline_runs:
|
||||
self._pipeline_runs[pipeline_id] = []
|
||||
self._pipeline_runs[pipeline_id].append(pipeline_run)
|
||||
self._pipeline_runs[pipeline_id][pipeline_run.id] = pipeline_run
|
||||
|
||||
def remove_run(self, pipeline_run: PipelineRun) -> None:
|
||||
"""Remove pipeline run."""
|
||||
pipeline_id = pipeline_run.pipeline.id
|
||||
self._pipeline_runs[pipeline_id].remove(pipeline_run)
|
||||
self._pipeline_runs[pipeline_id].pop(pipeline_run.id)
|
||||
|
||||
async def _change_listener(
|
||||
self, change_type: str, item_id: str, change: dict
|
||||
@@ -1589,7 +1615,7 @@ class PipelineRuns:
|
||||
return
|
||||
if pipeline_runs := self._pipeline_runs.get(item_id):
|
||||
# Create a temporary list in case the list is modified while we iterate
|
||||
for pipeline_run in list(pipeline_runs):
|
||||
for pipeline_run in list(pipeline_runs.values()):
|
||||
pipeline_run.abort_wake_word_detection = True
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import language as language_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DEFAULT_PIPELINE_TIMEOUT, DEFAULT_WAKE_WORD_TIMEOUT, DOMAIN
|
||||
from .error import PipelineNotFound
|
||||
from .pipeline import (
|
||||
AudioSettings,
|
||||
@@ -30,9 +30,6 @@ from .pipeline import (
|
||||
async_get_pipeline,
|
||||
)
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
DEFAULT_WAKE_WORD_TIMEOUT = 3
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -117,7 +114,7 @@ async def websocket_run(
|
||||
)
|
||||
return
|
||||
|
||||
timeout = msg.get("timeout", DEFAULT_TIMEOUT)
|
||||
timeout = msg.get("timeout", DEFAULT_PIPELINE_TIMEOUT)
|
||||
start_stage = PipelineStage(msg["start_stage"])
|
||||
end_stage = PipelineStage(msg["end_stage"])
|
||||
handler_id: int | None = None
|
||||
|
||||
@@ -3,11 +3,10 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aussiebb.asyncio import AussieBB
|
||||
from aussiebb.const import FETCH_TYPES, NBN_TYPES, PHONE_TYPES
|
||||
from aussiebb.const import FETCH_TYPES
|
||||
from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -23,19 +22,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
# Backport for the pyaussiebb=0.0.15 validate_service_type method
|
||||
def validate_service_type(service: dict[str, Any]) -> None:
|
||||
"""Check the service types against known types."""
|
||||
|
||||
if "type" not in service:
|
||||
raise ValueError("Field 'type' not found in service data")
|
||||
if service["type"] not in NBN_TYPES + PHONE_TYPES + ["Hardware"]:
|
||||
raise UnrecognisedServiceType(
|
||||
f"Service type {service['type']=} {service['name']=} - not recognised - ",
|
||||
"please report this at https://github.com/yaleman/aussiebb/issues/new",
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Aussie Broadband from a config entry."""
|
||||
# Login to the Aussie Broadband API and retrieve the current service list
|
||||
@@ -44,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_PASSWORD],
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
# Overwrite the pyaussiebb=0.0.15 validate_service_type method with backport
|
||||
# Required until pydantic 2.x is supported
|
||||
client.validate_service_type = validate_service_type
|
||||
|
||||
try:
|
||||
await client.login()
|
||||
services = await client.get_services(drop_types=FETCH_TYPES)
|
||||
@@ -61,10 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
try:
|
||||
return await client.get_usage(service_id)
|
||||
except UnrecognisedServiceType as err:
|
||||
raise UpdateFailed(
|
||||
f"Service {service_id} of type '{services[service_id]['type']}' was"
|
||||
" unrecognised"
|
||||
) from err
|
||||
raise UpdateFailed(f"Service {service_id} was unrecognised") from err
|
||||
|
||||
return async_update_data
|
||||
|
||||
|
||||
@@ -286,6 +286,13 @@
|
||||
"on": "[%key:component::binary_sensor::entity_component::gas::state::on%]"
|
||||
}
|
||||
},
|
||||
"tamper": {
|
||||
"name": "Tamper",
|
||||
"state": {
|
||||
"off": "[%key:component::binary_sensor::entity_component::gas::state::off%]",
|
||||
"on": "Tampering detected"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"name": "Update",
|
||||
"state": {
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
"bluetooth-adapters==0.16.1",
|
||||
"bluetooth-auto-recovery==1.2.3",
|
||||
"bluetooth-data-tools==1.12.0",
|
||||
"dbus-fast==2.11.0"
|
||||
"dbus-fast==2.11.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected==0.14.0"]
|
||||
"requirements": ["bimmer-connected==0.14.1"]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ from broadlink.exceptions import BroadlinkException
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_MODE,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
@@ -46,6 +45,8 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_min_color_temp_kelvin = 2700
|
||||
_attr_max_color_temp_kelvin = 6500
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
@@ -80,7 +81,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
|
||||
self._attr_hs_color = [data["hue"], data["saturation"]]
|
||||
|
||||
if "colortemp" in data:
|
||||
self._attr_color_temp = round((data["colortemp"] - 2700) / 100 + 153)
|
||||
self._attr_color_temp_kelvin = data["colortemp"]
|
||||
|
||||
if "bulb_colormode" in data:
|
||||
if data["bulb_colormode"] == BROADLINK_COLOR_MODE_RGB:
|
||||
@@ -108,21 +109,11 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
|
||||
state["saturation"] = int(hs_color[1])
|
||||
state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB
|
||||
|
||||
elif ATTR_COLOR_TEMP in kwargs:
|
||||
color_temp = kwargs[ATTR_COLOR_TEMP]
|
||||
state["colortemp"] = (color_temp - 153) * 100 + 2700
|
||||
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
color_temp = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
state["colortemp"] = color_temp
|
||||
state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE
|
||||
|
||||
elif ATTR_COLOR_MODE in kwargs:
|
||||
color_mode = kwargs[ATTR_COLOR_MODE]
|
||||
if color_mode == ColorMode.HS:
|
||||
state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB
|
||||
elif color_mode == ColorMode.COLOR_TEMP:
|
||||
state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE
|
||||
else:
|
||||
# Scenes are not yet supported.
|
||||
state["bulb_colormode"] = BROADLINK_COLOR_MODE_SCENES
|
||||
|
||||
await self._async_set_state(state)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -528,19 +528,24 @@ class CalendarEntity(Entity):
|
||||
the current or upcoming event.
|
||||
"""
|
||||
super().async_write_ha_state()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs)
|
||||
)
|
||||
for unsub in self._alarm_unsubs:
|
||||
unsub()
|
||||
self._alarm_unsubs.clear()
|
||||
|
||||
now = dt_util.now()
|
||||
event = self.event
|
||||
if event is None or now >= event.end_datetime_local:
|
||||
_LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_id, event)
|
||||
return
|
||||
|
||||
@callback
|
||||
def update(_: datetime.datetime) -> None:
|
||||
"""Run when the active or upcoming event starts or ends."""
|
||||
self._async_write_ha_state()
|
||||
"""Update state and reschedule next alarms."""
|
||||
_LOGGER.debug("Running %s update", self.entity_id)
|
||||
self.async_write_ha_state()
|
||||
|
||||
if now < event.start_datetime_local:
|
||||
self._alarm_unsubs.append(
|
||||
@@ -553,6 +558,13 @@ class CalendarEntity(Entity):
|
||||
self._alarm_unsubs.append(
|
||||
async_track_point_in_time(self.hass, update, event.end_datetime_local)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Scheduled %d updates for %s (%s, %s)",
|
||||
len(self._alarm_unsubs),
|
||||
self.entity_id,
|
||||
event.start_datetime_local,
|
||||
event.end_datetime_local,
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass.
|
||||
@@ -561,6 +573,7 @@ class CalendarEntity(Entity):
|
||||
"""
|
||||
for unsub in self._alarm_unsubs:
|
||||
unsub()
|
||||
self._alarm_unsubs.clear()
|
||||
|
||||
async def async_get_events(
|
||||
self,
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
import CO2Signal
|
||||
from requests.exceptions import JSONDecodeError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
|
||||
@@ -24,7 +24,10 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{vol.Optional(DOMAIN): {}},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
# Extend the existing light.turn_on service schema
|
||||
SERVICE_SCHEMA = vol.All(
|
||||
@@ -62,11 +65,12 @@ def _get_color(file_handler) -> tuple:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Color extractor component."""
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
|
||||
if DOMAIN in config:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"]
|
||||
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.10.2"]
|
||||
}
|
||||
|
||||
@@ -135,9 +135,11 @@ async def async_migrate_unique_id(
|
||||
) -> None:
|
||||
"""Migrate old entry."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
ent_reg = er.async_get(hass)
|
||||
old_unique_id = config_entry.unique_id
|
||||
new_unique_id = api.device.mac
|
||||
new_name = api.device.values.get("name")
|
||||
new_mac = dr.format_mac(new_unique_id)
|
||||
new_name = api.name
|
||||
|
||||
@callback
|
||||
def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
|
||||
@@ -147,15 +149,36 @@ async def async_migrate_unique_id(
|
||||
if new_unique_id == old_unique_id:
|
||||
return
|
||||
|
||||
duplicate = dev_reg.async_get_device(
|
||||
connections={(CONNECTION_NETWORK_MAC, new_mac)}, identifiers=None
|
||||
)
|
||||
|
||||
# Remove duplicated device
|
||||
if duplicate is not None:
|
||||
if config_entry.entry_id in duplicate.config_entries:
|
||||
_LOGGER.debug(
|
||||
"Removing duplicated device %s",
|
||||
duplicate.name,
|
||||
)
|
||||
|
||||
# The automatic cleanup in entity registry is scheduled as a task, remove
|
||||
# the entities manually to avoid unique_id collision when the entities
|
||||
# are migrated.
|
||||
duplicate_entities = er.async_entries_for_device(
|
||||
ent_reg, duplicate.id, True
|
||||
)
|
||||
for entity in duplicate_entities:
|
||||
ent_reg.async_remove(entity.entity_id)
|
||||
|
||||
dev_reg.async_remove_device(duplicate.id)
|
||||
|
||||
# Migrate devices
|
||||
for device_entry in dr.async_entries_for_config_entry(
|
||||
dev_reg, config_entry.entry_id
|
||||
):
|
||||
for connection in device_entry.connections:
|
||||
if connection[1] == old_unique_id:
|
||||
new_connections = {
|
||||
(CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id))
|
||||
}
|
||||
new_connections = {(CONNECTION_NETWORK_MAC, new_mac)}
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating device %s connections to %s",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==0.11.3"],
|
||||
"requirements": ["denonavr==0.11.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
||||
@@ -8,7 +8,15 @@ import logging
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
from denonavr import DenonAVR
|
||||
from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
|
||||
from denonavr.const import (
|
||||
ALL_TELNET_EVENTS,
|
||||
ALL_ZONES,
|
||||
POWER_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from denonavr.exceptions import (
|
||||
AvrCommandError,
|
||||
AvrForbiddenError,
|
||||
@@ -73,6 +81,23 @@ SERVICE_GET_COMMAND = "get_command"
|
||||
SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq"
|
||||
SERVICE_UPDATE_AUDYSSEY = "update_audyssey"
|
||||
|
||||
# HA Telnet events
|
||||
TELNET_EVENTS = {
|
||||
"HD",
|
||||
"MS",
|
||||
"MU",
|
||||
"MV",
|
||||
"NS",
|
||||
"NSE",
|
||||
"PS",
|
||||
"SI",
|
||||
"SS",
|
||||
"TF",
|
||||
"ZM",
|
||||
"Z2",
|
||||
"Z3",
|
||||
}
|
||||
|
||||
_DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice")
|
||||
_R = TypeVar("_R")
|
||||
_P = ParamSpec("_P")
|
||||
@@ -254,7 +279,9 @@ class DenonDevice(MediaPlayerEntity):
|
||||
async def _telnet_callback(self, zone, event, parameter) -> None:
|
||||
"""Process a telnet command callback."""
|
||||
# There are multiple checks implemented which reduce unnecessary updates of the ha state machine
|
||||
if zone != self._receiver.zone:
|
||||
if zone not in (self._receiver.zone, ALL_ZONES):
|
||||
return
|
||||
if event not in TELNET_EVENTS:
|
||||
return
|
||||
# Some updates trigger multiple events like one for artist and one for title for one change
|
||||
# We skip every event except the last one
|
||||
@@ -268,11 +295,11 @@ class DenonDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for telnet events."""
|
||||
self._receiver.register_callback("ALL", self._telnet_callback)
|
||||
self._receiver.register_callback(ALL_TELNET_EVENTS, self._telnet_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Clean up the entity."""
|
||||
self._receiver.unregister_callback("ALL", self._telnet_callback)
|
||||
self._receiver.unregister_callback(ALL_TELNET_EVENTS, self._telnet_callback)
|
||||
|
||||
@async_log_errors
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -35,6 +35,7 @@ from .const import (
|
||||
CONNECTED_PLC_DEVICES,
|
||||
CONNECTED_WIFI_CLIENTS,
|
||||
DOMAIN,
|
||||
FIRMWARE_UPDATE_INTERVAL,
|
||||
LONG_UPDATE_INTERVAL,
|
||||
NEIGHBORING_WIFI_NETWORKS,
|
||||
REGULAR_FIRMWARE,
|
||||
@@ -146,7 +147,7 @@ async def async_setup_entry( # noqa: C901
|
||||
_LOGGER,
|
||||
name=REGULAR_FIRMWARE,
|
||||
update_method=async_update_firmware_available,
|
||||
update_interval=LONG_UPDATE_INTERVAL,
|
||||
update_interval=FIRMWARE_UPDATE_INTERVAL,
|
||||
)
|
||||
if device.device and "wifi1" in device.device.features:
|
||||
coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
|
||||
|
||||
@@ -14,6 +14,7 @@ PRODUCT = "product"
|
||||
SERIAL_NUMBER = "serial_number"
|
||||
TITLE = "title"
|
||||
|
||||
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=5)
|
||||
LONG_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
SHORT_UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doods",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==10.0.0"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==10.0.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/duotecno",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pyDuotecno==2023.9.0"]
|
||||
"requirements": ["pyDuotecno==2023.10.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.5.36"]
|
||||
"requirements": ["env-canada==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -327,20 +327,23 @@ class ESPHomeManager:
|
||||
) -> int | None:
|
||||
"""Start a voice assistant pipeline."""
|
||||
if self.voice_assistant_udp_server is not None:
|
||||
return None
|
||||
_LOGGER.warning("Voice assistant UDP server was not stopped")
|
||||
self.voice_assistant_udp_server.stop()
|
||||
self.voice_assistant_udp_server.close()
|
||||
self.voice_assistant_udp_server = None
|
||||
|
||||
hass = self.hass
|
||||
voice_assistant_udp_server = VoiceAssistantUDPServer(
|
||||
self.voice_assistant_udp_server = VoiceAssistantUDPServer(
|
||||
hass,
|
||||
self.entry_data,
|
||||
self._handle_pipeline_event,
|
||||
self._handle_pipeline_finished,
|
||||
)
|
||||
port = await voice_assistant_udp_server.start_server()
|
||||
port = await self.voice_assistant_udp_server.start_server()
|
||||
|
||||
assert self.device_id is not None, "Device ID must be set"
|
||||
hass.async_create_background_task(
|
||||
voice_assistant_udp_server.run_pipeline(
|
||||
self.voice_assistant_udp_server.run_pipeline(
|
||||
device_id=self.device_id,
|
||||
conversation_id=conversation_id or None,
|
||||
flags=flags,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"async-interrupt==1.1.1",
|
||||
"aioesphomeapi==17.0.0",
|
||||
"aioesphomeapi==17.0.1",
|
||||
"bluetooth-data-tools==1.12.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
|
||||
@@ -24,7 +24,10 @@ from homeassistant.components.assist_pipeline import (
|
||||
async_pipeline_from_audio_stream,
|
||||
select as pipeline_select,
|
||||
)
|
||||
from homeassistant.components.assist_pipeline.error import WakeWordDetectionError
|
||||
from homeassistant.components.assist_pipeline.error import (
|
||||
WakeWordDetectionAborted,
|
||||
WakeWordDetectionError,
|
||||
)
|
||||
from homeassistant.components.media_player import async_process_play_media_url
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
|
||||
@@ -219,7 +222,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
audio_settings: VoiceAssistantAudioSettings | None = None,
|
||||
) -> None:
|
||||
"""Run the Voice Assistant pipeline."""
|
||||
if audio_settings is None:
|
||||
if audio_settings is None or audio_settings.volume_multiplier == 0:
|
||||
audio_settings = VoiceAssistantAudioSettings()
|
||||
|
||||
tts_audio_output = (
|
||||
@@ -257,6 +260,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
noise_suppression_level=audio_settings.noise_suppression_level,
|
||||
auto_gain_dbfs=audio_settings.auto_gain,
|
||||
volume_multiplier=audio_settings.volume_multiplier,
|
||||
is_vad_enabled=bool(flags & VoiceAssistantCommandFlag.USE_VAD),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -273,6 +277,8 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
},
|
||||
)
|
||||
_LOGGER.warning("Pipeline not found")
|
||||
except WakeWordDetectionAborted:
|
||||
pass # Wake word detection was aborted and `handle_finished` is enough.
|
||||
except WakeWordDetectionError as e:
|
||||
self.handle_event(
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_ERROR,
|
||||
@@ -281,7 +287,6 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
"message": e.message,
|
||||
},
|
||||
)
|
||||
_LOGGER.warning("No Wake word provider found")
|
||||
finally:
|
||||
self.handle_finished()
|
||||
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["eufylife-ble-client==0.1.7"]
|
||||
"requirements": ["eufylife-ble-client==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfronius"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["PyFronius==0.7.1"]
|
||||
"requirements": ["PyFronius==0.7.2"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20230926.0"]
|
||||
"requirements": ["home-assistant-frontend==20231005.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["ha-av==10.1.1", "Pillow==10.0.0"]
|
||||
"requirements": ["ha-av==10.1.1", "Pillow==10.0.1"]
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ async def async_setup_entry(
|
||||
SERVICE_CREATE_EVENT,
|
||||
CREATE_EVENT_SCHEMA,
|
||||
async_create_event,
|
||||
required_features=CalendarEntityFeature.CREATE_EVENT,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -112,12 +112,22 @@ class GoogleMapsScanner:
|
||||
|
||||
last_seen = dt_util.as_utc(person.datetime)
|
||||
if last_seen < self._prev_seen.get(dev_id, last_seen):
|
||||
_LOGGER.warning(
|
||||
_LOGGER.debug(
|
||||
"Ignoring %s update because timestamp is older than last timestamp",
|
||||
person.nickname,
|
||||
)
|
||||
_LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id])
|
||||
continue
|
||||
if last_seen == self._prev_seen.get(dev_id, last_seen) and hasattr(
|
||||
self, "success_init"
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Ignoring %s update because timestamp "
|
||||
"is the same as the last timestamp %s",
|
||||
person.nickname,
|
||||
last_seen,
|
||||
)
|
||||
continue
|
||||
self._prev_seen[dev_id] = last_seen
|
||||
|
||||
attrs = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
|
||||
"description": "You can specify the origin and destination in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
|
||||
@@ -88,11 +88,13 @@ from .handler import ( # noqa: F401
|
||||
async_get_addon_discovery_info,
|
||||
async_get_addon_info,
|
||||
async_get_addon_store_info,
|
||||
async_get_green_settings,
|
||||
async_get_yellow_settings,
|
||||
async_install_addon,
|
||||
async_reboot_host,
|
||||
async_restart_addon,
|
||||
async_set_addon_options,
|
||||
async_set_green_settings,
|
||||
async_set_yellow_settings,
|
||||
async_start_addon,
|
||||
async_stop_addon,
|
||||
|
||||
@@ -263,6 +263,27 @@ async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> b
|
||||
return await hassio.send_command(command, timeout=None)
|
||||
|
||||
|
||||
@api_data
|
||||
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
|
||||
"""Return settings specific to Home Assistant Green."""
|
||||
hassio: HassIO = hass.data[DOMAIN]
|
||||
return await hassio.send_command("/os/boards/green", method="get")
|
||||
|
||||
|
||||
@api_data
|
||||
async def async_set_green_settings(
|
||||
hass: HomeAssistant, settings: dict[str, bool]
|
||||
) -> dict:
|
||||
"""Set settings specific to Home Assistant Green.
|
||||
|
||||
Returns an empty dict.
|
||||
"""
|
||||
hassio: HassIO = hass.data[DOMAIN]
|
||||
return await hassio.send_command(
|
||||
"/os/boards/green", method="post", payload=settings
|
||||
)
|
||||
|
||||
|
||||
@api_data
|
||||
async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]:
|
||||
"""Return settings specific to Home Assistant Yellow."""
|
||||
|
||||
@@ -60,3 +60,5 @@ reload_config_entry:
|
||||
text:
|
||||
|
||||
save_persistent_states:
|
||||
|
||||
reload_all:
|
||||
|
||||
@@ -125,6 +125,10 @@
|
||||
"save_persistent_states": {
|
||||
"name": "Save persistent states",
|
||||
"description": "Saves the persistent states immediately. Maintains the normal periodic saving interval."
|
||||
},
|
||||
"reload_all": {
|
||||
"name": "Reload all",
|
||||
"description": "Reload all YAML configuration that can be reloaded without restarting Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,100 @@
|
||||
"""Config flow for the Home Assistant Green integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
HassioAPIError,
|
||||
async_get_green_settings,
|
||||
async_set_green_settings,
|
||||
is_hassio,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_HW_SETTINGS_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Sorted to match front panel left to right
|
||||
vol.Required("power_led"): selector.BooleanSelector(),
|
||||
vol.Required("activity_led"): selector.BooleanSelector(),
|
||||
vol.Required("system_health_led"): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Home Assistant Green."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> HomeAssistantGreenOptionsFlow:
|
||||
"""Return the options flow."""
|
||||
return HomeAssistantGreenOptionsFlow()
|
||||
|
||||
async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return self.async_create_entry(title="Home Assistant Green", data={})
|
||||
|
||||
|
||||
class HomeAssistantGreenOptionsFlow(OptionsFlow):
|
||||
"""Handle an option flow for Home Assistant Green."""
|
||||
|
||||
_hw_settings: dict[str, bool] | None = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(reason="not_hassio")
|
||||
|
||||
return await self.async_step_hardware_settings()
|
||||
|
||||
async def async_step_hardware_settings(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle hardware settings."""
|
||||
|
||||
if user_input is not None:
|
||||
if self._hw_settings == user_input:
|
||||
return self.async_create_entry(data={})
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await async_set_green_settings(self.hass, user_input)
|
||||
except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err:
|
||||
_LOGGER.warning("Failed to write hardware settings", exc_info=err)
|
||||
return self.async_abort(reason="write_hw_settings_error")
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
self._hw_settings: dict[str, bool] = await async_get_green_settings(
|
||||
self.hass
|
||||
)
|
||||
except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err:
|
||||
_LOGGER.warning("Failed to read hardware settings", exc_info=err)
|
||||
return self.async_abort(reason="read_hw_settings_error")
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
STEP_HW_SETTINGS_SCHEMA, self._hw_settings
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="hardware_settings", data_schema=schema)
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from .const import DOMAIN
|
||||
|
||||
BOARD_NAME = "Home Assistant Green"
|
||||
DOCUMENTATION_URL = "https://green.home-assistant.io/documentation/"
|
||||
MANUFACTURER = "homeassistant"
|
||||
MODEL = "green"
|
||||
|
||||
@@ -39,6 +40,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
config_entries=config_entries,
|
||||
dongle=None,
|
||||
name=BOARD_NAME,
|
||||
url=None,
|
||||
url=DOCUMENTATION_URL,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"options": {
|
||||
"step": {
|
||||
"hardware_settings": {
|
||||
"title": "Configure hardware settings",
|
||||
"data": {
|
||||
"activity_led": "Green: activity LED",
|
||||
"power_led": "White: power LED",
|
||||
"system_health_led": "Yellow: system health LED"
|
||||
}
|
||||
},
|
||||
"reboot_menu": {
|
||||
"title": "Reboot required",
|
||||
"description": "The settings have changed, but the new settings will not take effect until the system is rebooted",
|
||||
"menu_options": {
|
||||
"reboot_later": "Reboot manually later",
|
||||
"reboot_now": "Reboot now"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"read_hw_settings_error": "Failed to read hardware settings",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"write_hw_settings_error": "Failed to write hardware settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -885,7 +885,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC):
|
||||
|
||||
|
||||
async def check_multi_pan_addon(hass: HomeAssistant) -> None:
|
||||
"""Check the multi-PAN addon state, and start it if installed but not started.
|
||||
"""Check the multiprotocol addon state, and start it if installed but not started.
|
||||
|
||||
Does nothing if Hass.io is not loaded.
|
||||
Raises on error or if the add-on is installed but not started.
|
||||
|
||||
@@ -45,7 +45,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
return
|
||||
|
||||
hw_discovery_data = {
|
||||
"name": "SkyConnect Multi-PAN",
|
||||
"name": "SkyConnect Multiprotocol",
|
||||
"port": {
|
||||
"path": get_zigbee_socket(),
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH
|
||||
|
||||
def _zha_name(self) -> str:
|
||||
"""Return the ZHA name."""
|
||||
return "SkyConnect Multi-PAN"
|
||||
return "SkyConnect Multiprotocol"
|
||||
|
||||
def _hardware_name(self) -> str:
|
||||
"""Return the name of the hardware."""
|
||||
|
||||
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hw_discovery_data = ZHA_HW_DISCOVERY_DATA
|
||||
else:
|
||||
hw_discovery_data = {
|
||||
"name": "Yellow Multi-PAN",
|
||||
"name": "Yellow Multiprotocol",
|
||||
"port": {
|
||||
"path": get_zigbee_socket(),
|
||||
},
|
||||
|
||||
@@ -153,7 +153,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
|
||||
|
||||
def _zha_name(self) -> str:
|
||||
"""Return the ZHA name."""
|
||||
return "Yellow Multi-PAN"
|
||||
return "Yellow Multiprotocol"
|
||||
|
||||
def _hardware_name(self) -> str:
|
||||
"""Return the name of the hardware."""
|
||||
|
||||
@@ -465,7 +465,9 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
def async_update_state_callback(self, new_state: State | None) -> None:
|
||||
"""Handle state change listener callback."""
|
||||
_LOGGER.debug("New_state: %s", new_state)
|
||||
if new_state is None:
|
||||
# HomeKit handles unavailable state via the available property
|
||||
# so we should not propagate it here
|
||||
if new_state is None or new_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
battery_state = None
|
||||
battery_charging_state = None
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.7.1",
|
||||
"HAP-python==4.8.0",
|
||||
"fnv-hash-fast==0.4.1",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohue"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohue==4.6.2"],
|
||||
"requirements": ["aiohue==4.7.0"],
|
||||
"zeroconf": ["_hue._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for Hue binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypeAlias
|
||||
from typing import TypeAlias
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.config import (
|
||||
@@ -9,9 +9,17 @@ from aiohue.v2.controllers.config import (
|
||||
EntertainmentConfigurationController,
|
||||
)
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.sensors import MotionController
|
||||
from aiohue.v2.controllers.sensors import (
|
||||
CameraMotionController,
|
||||
ContactController,
|
||||
MotionController,
|
||||
TamperController,
|
||||
)
|
||||
from aiohue.v2.models.camera_motion import CameraMotion
|
||||
from aiohue.v2.models.contact import Contact, ContactState
|
||||
from aiohue.v2.models.entertainment_configuration import EntertainmentStatus
|
||||
from aiohue.v2.models.motion import Motion
|
||||
from aiohue.v2.models.tamper import Tamper, TamperState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -25,8 +33,16 @@ from ..bridge import HueBridge
|
||||
from ..const import DOMAIN
|
||||
from .entity import HueBaseEntity
|
||||
|
||||
SensorType: TypeAlias = Motion | EntertainmentConfiguration
|
||||
ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController
|
||||
SensorType: TypeAlias = (
|
||||
CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper
|
||||
)
|
||||
ControllerType: TypeAlias = (
|
||||
CameraMotionController
|
||||
| ContactController
|
||||
| MotionController
|
||||
| EntertainmentConfigurationController
|
||||
| TamperController
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -57,8 +73,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# setup for each binary-sensor-type hue resource
|
||||
register_items(api.sensors.camera_motion, HueMotionSensor)
|
||||
register_items(api.sensors.motion, HueMotionSensor)
|
||||
register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor)
|
||||
register_items(api.sensors.contact, HueContactSensor)
|
||||
register_items(api.sensors.tamper, HueTamperSensor)
|
||||
|
||||
|
||||
class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity):
|
||||
@@ -87,12 +106,7 @@ class HueMotionSensor(HueBinarySensorBase):
|
||||
if not self.resource.enabled:
|
||||
# Force None (unknown) if the sensor is set to disabled in Hue
|
||||
return None
|
||||
return self.resource.motion.motion
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {"motion_valid": self.resource.motion.motion_valid}
|
||||
return self.resource.motion.value
|
||||
|
||||
|
||||
class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
||||
@@ -110,3 +124,30 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
||||
"""Return sensor name."""
|
||||
type_title = self.resource.type.value.replace("_", " ").title()
|
||||
return f"{self.resource.metadata.name}: {type_title}"
|
||||
|
||||
|
||||
class HueContactSensor(HueBinarySensorBase):
|
||||
"""Representation of a Hue Contact sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.OPENING
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if not self.resource.enabled:
|
||||
# Force None (unknown) if the sensor is set to disabled in Hue
|
||||
return None
|
||||
return self.resource.contact_report.state != ContactState.CONTACT
|
||||
|
||||
|
||||
class HueTamperSensor(HueBinarySensorBase):
|
||||
"""Representation of a Hue Tamper sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.TAMPER
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if not self.resource.tamper_reports:
|
||||
return False
|
||||
return self.resource.tamper_reports[0].state == TamperState.TAMPERED
|
||||
|
||||
@@ -100,12 +100,7 @@ class HueTemperatureSensor(HueSensorBase):
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the value reported by the sensor."""
|
||||
return round(self.resource.temperature.temperature, 1)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {"temperature_valid": self.resource.temperature.temperature_valid}
|
||||
return round(self.resource.temperature.value, 1)
|
||||
|
||||
|
||||
class HueLightLevelSensor(HueSensorBase):
|
||||
@@ -122,14 +117,13 @@ class HueLightLevelSensor(HueSensorBase):
|
||||
# scale used because the human eye adjusts to light levels and small
|
||||
# changes at low lux levels are more noticeable than at high lux
|
||||
# levels.
|
||||
return int(10 ** ((self.resource.light.light_level - 1) / 10000))
|
||||
return int(10 ** ((self.resource.light.value - 1) / 10000))
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {
|
||||
"light_level": self.resource.light.light_level,
|
||||
"light_level_valid": self.resource.light.light_level_valid,
|
||||
"light_level": self.resource.light.value,
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +143,8 @@ class HueBatterySensor(HueSensorBase):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
if self.resource.power_state.battery_state is None:
|
||||
return {}
|
||||
return {"battery_state": self.resource.power_state.battery_state.value}
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bleak import BleakError
|
||||
from bleak.exc import BleakError
|
||||
from bluetooth_data_tools import human_readable_name
|
||||
from idasen_ha import Desk
|
||||
from idasen_ha import AuthFailedError, Desk
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -64,6 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
desk = Desk(None)
|
||||
try:
|
||||
await desk.connect(discovery_info.device, monitor_height=False)
|
||||
except AuthFailedError as err:
|
||||
_LOGGER.exception("AuthFailedError", exc_info=err)
|
||||
errors["base"] = "auth_failed"
|
||||
except TimeoutError as err:
|
||||
_LOGGER.exception("TimeoutError", exc_info=err)
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["idasen-ha==1.4"]
|
||||
"requirements": ["idasen-ha==1.4.1"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"auth_failed": "Unable to authenticate with the desk. This is usually solved by using an ESPHome Bluetooth Proxy. Please check the integration documentation for alternative workarounds.",
|
||||
"cannot_connect": "Cannot connect. Make sure that the desk is in Bluetooth pairing mode. If not already, you can also use an ESPHome Bluetooth Proxy, as it provides a better connection.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==10.0.0"]
|
||||
"requirements": ["Pillow==10.0.1"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.11.2",
|
||||
"xknxproject==3.2.0",
|
||||
"xknxproject==3.3.0",
|
||||
"knx-frontend==2023.6.23.191712"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"requirements": ["pylitterbot==2023.4.8"]
|
||||
"requirements": ["pylitterbot==2023.4.9"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/loqed",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["loqedAPI==2.1.7"],
|
||||
"requirements": ["loqedAPI==2.1.8"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pylutron_caseta"],
|
||||
"requirements": ["pylutron-caseta==0.18.2"],
|
||||
"requirements": ["pylutron-caseta==0.18.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_lutron._tcp.local.",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/matrix",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"requirements": ["matrix-nio==0.21.2", "Pillow==10.0.0"]
|
||||
"requirements": ["matrix-nio==0.21.2", "Pillow==10.0.1"]
|
||||
}
|
||||
|
||||
@@ -1,213 +1,26 @@
|
||||
"""The Mazda Connected Services integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from pymazda import (
|
||||
Client as MazdaAPI,
|
||||
MazdaAccountLockedException,
|
||||
MazdaAPIEncryptionException,
|
||||
MazdaAuthenticationException,
|
||||
MazdaException,
|
||||
MazdaTokenExpiredException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DATA_VEHICLES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
DOMAIN = "mazda"
|
||||
|
||||
|
||||
async def with_timeout(task, timeout_seconds=30):
|
||||
"""Run an async task with a timeout."""
|
||||
async with asyncio.timeout(timeout_seconds):
|
||||
return await task
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
"""Set up Mazda Connected Services from a config entry."""
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
region = entry.data[CONF_REGION]
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
mazda_client = MazdaAPI(
|
||||
email, password, region, websession=websession, use_cached_vehicle_list=True
|
||||
)
|
||||
|
||||
try:
|
||||
await mazda_client.validate_credentials()
|
||||
except MazdaAuthenticationException as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except (
|
||||
MazdaException,
|
||||
MazdaAccountLockedException,
|
||||
MazdaTokenExpiredException,
|
||||
MazdaAPIEncryptionException,
|
||||
) as ex:
|
||||
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
async def async_handle_service_call(service_call: ServiceCall) -> None:
|
||||
"""Handle a service call."""
|
||||
# Get device entry from device registry
|
||||
dev_reg = dr.async_get(hass)
|
||||
device_id = service_call.data["device_id"]
|
||||
device_entry = dev_reg.async_get(device_id)
|
||||
if TYPE_CHECKING:
|
||||
# For mypy: it has already been checked in validate_mazda_device_id
|
||||
assert device_entry
|
||||
|
||||
# Get vehicle VIN from device identifiers
|
||||
mazda_identifiers = (
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
)
|
||||
vin_identifier = next(mazda_identifiers)
|
||||
vin = vin_identifier[1]
|
||||
|
||||
# Get vehicle ID and API client from hass.data
|
||||
vehicle_id = 0
|
||||
api_client = None
|
||||
for entry_data in hass.data[DOMAIN].values():
|
||||
for vehicle in entry_data[DATA_VEHICLES]:
|
||||
if vehicle["vin"] == vin:
|
||||
vehicle_id = vehicle["id"]
|
||||
api_client = entry_data[DATA_CLIENT]
|
||||
break
|
||||
|
||||
if vehicle_id == 0 or api_client is None:
|
||||
raise HomeAssistantError("Vehicle ID not found")
|
||||
|
||||
api_method = getattr(api_client, service_call.service)
|
||||
try:
|
||||
latitude = service_call.data["latitude"]
|
||||
longitude = service_call.data["longitude"]
|
||||
poi_name = service_call.data["poi_name"]
|
||||
await api_method(vehicle_id, latitude, longitude, poi_name)
|
||||
except Exception as ex:
|
||||
raise HomeAssistantError(ex) from ex
|
||||
|
||||
def validate_mazda_device_id(device_id):
|
||||
"""Check that a device ID exists in the registry and has at least one 'mazda' identifier."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
if (device_entry := dev_reg.async_get(device_id)) is None:
|
||||
raise vol.Invalid("Invalid device ID")
|
||||
|
||||
mazda_identifiers = [
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
]
|
||||
if not mazda_identifiers:
|
||||
raise vol.Invalid("Device ID is not a Mazda vehicle")
|
||||
|
||||
return device_id
|
||||
|
||||
service_schema_send_poi = vol.Schema(
|
||||
{
|
||||
vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id),
|
||||
vol.Required("latitude"): cv.latitude,
|
||||
vol.Required("longitude"): cv.longitude,
|
||||
vol.Required("poi_name"): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from Mazda API."""
|
||||
try:
|
||||
vehicles = await with_timeout(mazda_client.get_vehicles())
|
||||
|
||||
# The Mazda API can throw an error when multiple simultaneous requests are
|
||||
# made for the same account, so we can only make one request at a time here
|
||||
for vehicle in vehicles:
|
||||
vehicle["status"] = await with_timeout(
|
||||
mazda_client.get_vehicle_status(vehicle["id"])
|
||||
)
|
||||
|
||||
# If vehicle is electric, get additional EV-specific status info
|
||||
if vehicle["isElectric"]:
|
||||
vehicle["evStatus"] = await with_timeout(
|
||||
mazda_client.get_ev_vehicle_status(vehicle["id"])
|
||||
)
|
||||
vehicle["hvacSetting"] = await with_timeout(
|
||||
mazda_client.get_hvac_setting(vehicle["id"])
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
|
||||
|
||||
return vehicles
|
||||
except MazdaAuthenticationException as ex:
|
||||
raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex
|
||||
except Exception as ex:
|
||||
_LOGGER.exception(
|
||||
"Unknown error occurred during Mazda update request: %s", ex
|
||||
)
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=180),
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_CLIENT: mazda_client,
|
||||
DATA_COORDINATOR: coordinator,
|
||||
DATA_REGION: region,
|
||||
DATA_VEHICLES: [],
|
||||
}
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Setup components
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Register services
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"send_poi",
|
||||
async_handle_service_call,
|
||||
schema=service_schema_send_poi,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"dmca": "https://github.com/github/dmca/blob/master/2023/10/2023-10-10-mazda.md",
|
||||
"entries": "/config/integrations/integration/mazda",
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -215,45 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
# Only remove services if it is the last config entry
|
||||
if len(hass.data[DOMAIN]) == 1:
|
||||
hass.services.async_remove(DOMAIN, "send_poi")
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class MazdaEntity(CoordinatorEntity):
|
||||
"""Defines a base Mazda entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, client, coordinator, index):
|
||||
"""Initialize the Mazda entity."""
|
||||
super().__init__(coordinator)
|
||||
self.client = client
|
||||
self.index = index
|
||||
self.vin = self.data["vin"]
|
||||
self.vehicle_id = self.data["id"]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.vin)},
|
||||
manufacturer="Mazda",
|
||||
model=f"{self.data['modelYear']} {self.data['carlineName']}",
|
||||
name=self.vehicle_name,
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Shortcut to access coordinator data for the entity."""
|
||||
return self.coordinator.data[self.index]
|
||||
|
||||
@property
|
||||
def vehicle_name(self):
|
||||
"""Return the vehicle name, to be used as a prefix for names of other entities."""
|
||||
if "nickname" in self.data and len(self.data["nickname"]) > 0:
|
||||
return self.data["nickname"]
|
||||
return f"{self.data['modelYear']} {self.data['carlineName']}"
|
||||
return True
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
"""Platform for Mazda binary sensor integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
@dataclass
|
||||
class MazdaBinarySensorRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
# Function to determine the value for this binary sensor, given the coordinator data
|
||||
value_fn: Callable[[dict[str, Any]], bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MazdaBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin
|
||||
):
|
||||
"""Describes a Mazda binary sensor entity."""
|
||||
|
||||
# Function to determine whether the vehicle supports this binary sensor, given the coordinator data
|
||||
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
|
||||
|
||||
|
||||
def _plugged_in_supported(data):
|
||||
"""Determine if 'plugged in' binary sensor is supported."""
|
||||
return (
|
||||
data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None
|
||||
)
|
||||
|
||||
|
||||
BINARY_SENSOR_ENTITIES = [
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="driver_door",
|
||||
translation_key="driver_door",
|
||||
icon="mdi:car-door",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="passenger_door",
|
||||
translation_key="passenger_door",
|
||||
icon="mdi:car-door",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="rear_left_door",
|
||||
translation_key="rear_left_door",
|
||||
icon="mdi:car-door",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="rear_right_door",
|
||||
translation_key="rear_right_door",
|
||||
icon="mdi:car-door",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="trunk",
|
||||
translation_key="trunk",
|
||||
icon="mdi:car-back",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["trunkOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="hood",
|
||||
translation_key="hood",
|
||||
icon="mdi:car",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
value_fn=lambda data: data["status"]["doors"]["hoodOpen"],
|
||||
),
|
||||
MazdaBinarySensorEntityDescription(
|
||||
key="ev_plugged_in",
|
||||
translation_key="ev_plugged_in",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
is_supported=_plugged_in_supported,
|
||||
value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
MazdaBinarySensorEntity(client, coordinator, index, description)
|
||||
for index, data in enumerate(coordinator.data)
|
||||
for description in BINARY_SENSOR_ENTITIES
|
||||
if description.is_supported(data)
|
||||
)
|
||||
|
||||
|
||||
class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity):
|
||||
"""Representation of a Mazda vehicle binary sensor."""
|
||||
|
||||
entity_description: MazdaBinarySensorEntityDescription
|
||||
|
||||
def __init__(self, client, coordinator, index, description):
|
||||
"""Initialize Mazda binary sensor."""
|
||||
super().__init__(client, coordinator, index)
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = f"{self.vin}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self.entity_description.value_fn(self.data)
|
||||
@@ -1,150 +0,0 @@
|
||||
"""Platform for Mazda button integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pymazda import (
|
||||
Client as MazdaAPIClient,
|
||||
MazdaAccountLockedException,
|
||||
MazdaAPIEncryptionException,
|
||||
MazdaAuthenticationException,
|
||||
MazdaException,
|
||||
MazdaLoginFailedException,
|
||||
MazdaTokenExpiredException,
|
||||
)
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def handle_button_press(
|
||||
client: MazdaAPIClient,
|
||||
key: str,
|
||||
vehicle_id: int,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Handle a press for a Mazda button entity."""
|
||||
api_method = getattr(client, key)
|
||||
|
||||
try:
|
||||
await api_method(vehicle_id)
|
||||
except (
|
||||
MazdaException,
|
||||
MazdaAuthenticationException,
|
||||
MazdaAccountLockedException,
|
||||
MazdaTokenExpiredException,
|
||||
MazdaAPIEncryptionException,
|
||||
MazdaLoginFailedException,
|
||||
) as ex:
|
||||
raise HomeAssistantError(ex) from ex
|
||||
|
||||
|
||||
async def handle_refresh_vehicle_status(
|
||||
client: MazdaAPIClient,
|
||||
key: str,
|
||||
vehicle_id: int,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Handle a request to refresh the vehicle status."""
|
||||
await handle_button_press(client, key, vehicle_id, coordinator)
|
||||
|
||||
await coordinator.async_request_refresh()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MazdaButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes a Mazda button entity."""
|
||||
|
||||
# Function to determine whether the vehicle supports this button,
|
||||
# given the coordinator data
|
||||
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
|
||||
|
||||
async_press: Callable[
|
||||
[MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable
|
||||
] = handle_button_press
|
||||
|
||||
|
||||
BUTTON_ENTITIES = [
|
||||
MazdaButtonEntityDescription(
|
||||
key="start_engine",
|
||||
translation_key="start_engine",
|
||||
icon="mdi:engine",
|
||||
is_supported=lambda data: not data["isElectric"],
|
||||
),
|
||||
MazdaButtonEntityDescription(
|
||||
key="stop_engine",
|
||||
translation_key="stop_engine",
|
||||
icon="mdi:engine-off",
|
||||
is_supported=lambda data: not data["isElectric"],
|
||||
),
|
||||
MazdaButtonEntityDescription(
|
||||
key="turn_on_hazard_lights",
|
||||
translation_key="turn_on_hazard_lights",
|
||||
icon="mdi:hazard-lights",
|
||||
is_supported=lambda data: not data["isElectric"],
|
||||
),
|
||||
MazdaButtonEntityDescription(
|
||||
key="turn_off_hazard_lights",
|
||||
translation_key="turn_off_hazard_lights",
|
||||
icon="mdi:hazard-lights",
|
||||
is_supported=lambda data: not data["isElectric"],
|
||||
),
|
||||
MazdaButtonEntityDescription(
|
||||
key="refresh_vehicle_status",
|
||||
translation_key="refresh_vehicle_status",
|
||||
icon="mdi:refresh",
|
||||
async_press=handle_refresh_vehicle_status,
|
||||
is_supported=lambda data: data["isElectric"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the button platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
MazdaButtonEntity(client, coordinator, index, description)
|
||||
for index, data in enumerate(coordinator.data)
|
||||
for description in BUTTON_ENTITIES
|
||||
if description.is_supported(data)
|
||||
)
|
||||
|
||||
|
||||
class MazdaButtonEntity(MazdaEntity, ButtonEntity):
|
||||
"""Representation of a Mazda button."""
|
||||
|
||||
entity_description: MazdaButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: MazdaAPIClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
index: int,
|
||||
description: MazdaButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Mazda button."""
|
||||
super().__init__(client, coordinator, index)
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = f"{self.vin}_{description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self.entity_description.async_press(
|
||||
self.client, self.entity_description.key, self.vehicle_id, self.coordinator
|
||||
)
|
||||
@@ -1,187 +0,0 @@
|
||||
"""Platform for Mazda climate integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pymazda import Client as MazdaAPIClient
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN
|
||||
|
||||
PRESET_DEFROSTER_OFF = "Defroster Off"
|
||||
PRESET_DEFROSTER_FRONT = "Front Defroster"
|
||||
PRESET_DEFROSTER_REAR = "Rear Defroster"
|
||||
PRESET_DEFROSTER_FRONT_AND_REAR = "Front and Rear Defroster"
|
||||
|
||||
|
||||
def _front_defroster_enabled(preset_mode: str | None) -> bool:
|
||||
return preset_mode in [
|
||||
PRESET_DEFROSTER_FRONT_AND_REAR,
|
||||
PRESET_DEFROSTER_FRONT,
|
||||
]
|
||||
|
||||
|
||||
def _rear_defroster_enabled(preset_mode: str | None) -> bool:
|
||||
return preset_mode in [
|
||||
PRESET_DEFROSTER_FRONT_AND_REAR,
|
||||
PRESET_DEFROSTER_REAR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the climate platform."""
|
||||
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
client = entry_data[DATA_CLIENT]
|
||||
coordinator = entry_data[DATA_COORDINATOR]
|
||||
region = entry_data[DATA_REGION]
|
||||
|
||||
async_add_entities(
|
||||
MazdaClimateEntity(client, coordinator, index, region)
|
||||
for index, data in enumerate(coordinator.data)
|
||||
if data["isElectric"]
|
||||
)
|
||||
|
||||
|
||||
class MazdaClimateEntity(MazdaEntity, ClimateEntity):
|
||||
"""Class for a Mazda climate entity."""
|
||||
|
||||
_attr_translation_key = "climate"
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
|
||||
_attr_preset_modes = [
|
||||
PRESET_DEFROSTER_OFF,
|
||||
PRESET_DEFROSTER_FRONT,
|
||||
PRESET_DEFROSTER_REAR,
|
||||
PRESET_DEFROSTER_FRONT_AND_REAR,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: MazdaAPIClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
index: int,
|
||||
region: str,
|
||||
) -> None:
|
||||
"""Initialize Mazda climate entity."""
|
||||
super().__init__(client, coordinator, index)
|
||||
|
||||
self.region = region
|
||||
self._attr_unique_id = self.vin
|
||||
|
||||
if self.data["hvacSetting"]["temperatureUnit"] == "F":
|
||||
self._attr_precision = PRECISION_WHOLE
|
||||
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
self._attr_min_temp = 61.0
|
||||
self._attr_max_temp = 83.0
|
||||
else:
|
||||
self._attr_precision = PRECISION_HALVES
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
if region == "MJO":
|
||||
self._attr_min_temp = 18.5
|
||||
self._attr_max_temp = 31.5
|
||||
else:
|
||||
self._attr_min_temp = 15.5
|
||||
self._attr_max_temp = 28.5
|
||||
|
||||
self._update_state_attributes()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator data updates."""
|
||||
self._update_state_attributes()
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _update_state_attributes(self) -> None:
|
||||
# Update the HVAC mode
|
||||
hvac_on = self.client.get_assumed_hvac_mode(self.vehicle_id)
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL if hvac_on else HVACMode.OFF
|
||||
|
||||
# Update the target temperature
|
||||
hvac_setting = self.client.get_assumed_hvac_setting(self.vehicle_id)
|
||||
self._attr_target_temperature = hvac_setting.get("temperature")
|
||||
|
||||
# Update the current temperature
|
||||
current_temperature_celsius = self.data["evStatus"]["hvacInfo"][
|
||||
"interiorTemperatureCelsius"
|
||||
]
|
||||
if self.data["hvacSetting"]["temperatureUnit"] == "F":
|
||||
self._attr_current_temperature = TemperatureConverter.convert(
|
||||
current_temperature_celsius,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
)
|
||||
else:
|
||||
self._attr_current_temperature = current_temperature_celsius
|
||||
|
||||
# Update the preset mode based on the state of the front and rear defrosters
|
||||
front_defroster = hvac_setting.get("frontDefroster")
|
||||
rear_defroster = hvac_setting.get("rearDefroster")
|
||||
if front_defroster and rear_defroster:
|
||||
self._attr_preset_mode = PRESET_DEFROSTER_FRONT_AND_REAR
|
||||
elif front_defroster:
|
||||
self._attr_preset_mode = PRESET_DEFROSTER_FRONT
|
||||
elif rear_defroster:
|
||||
self._attr_preset_mode = PRESET_DEFROSTER_REAR
|
||||
else:
|
||||
self._attr_preset_mode = PRESET_DEFROSTER_OFF
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set a new HVAC mode."""
|
||||
if hvac_mode == HVACMode.HEAT_COOL:
|
||||
await self.client.turn_on_hvac(self.vehicle_id)
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
await self.client.turn_off_hvac(self.vehicle_id)
|
||||
|
||||
self._handle_coordinator_update()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
precision = self.precision
|
||||
rounded_temperature = round(temperature / precision) * precision
|
||||
|
||||
await self.client.set_hvac_setting(
|
||||
self.vehicle_id,
|
||||
rounded_temperature,
|
||||
self.data["hvacSetting"]["temperatureUnit"],
|
||||
_front_defroster_enabled(self._attr_preset_mode),
|
||||
_rear_defroster_enabled(self._attr_preset_mode),
|
||||
)
|
||||
|
||||
self._handle_coordinator_update()
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Turn on/off the front/rear defrosters according to the chosen preset mode."""
|
||||
await self.client.set_hvac_setting(
|
||||
self.vehicle_id,
|
||||
self._attr_target_temperature,
|
||||
self.data["hvacSetting"]["temperatureUnit"],
|
||||
_front_defroster_enabled(preset_mode),
|
||||
_rear_defroster_enabled(preset_mode),
|
||||
)
|
||||
|
||||
self._handle_coordinator_update()
|
||||
@@ -1,110 +1,11 @@
|
||||
"""Config flow for Mazda Connected Services integration."""
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
"""The Mazda Connected Services integration."""
|
||||
|
||||
import aiohttp
|
||||
from pymazda import (
|
||||
Client as MazdaAPI,
|
||||
MazdaAccountLockedException,
|
||||
MazdaAuthenticationException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN, MAZDA_REGIONS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS),
|
||||
}
|
||||
)
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class MazdaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Mazda Connected Services."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Start the mazda config flow."""
|
||||
self._reauth_entry = None
|
||||
self._email = None
|
||||
self._region = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._email = user_input[CONF_EMAIL]
|
||||
self._region = user_input[CONF_REGION]
|
||||
unique_id = user_input[CONF_EMAIL].lower()
|
||||
await self.async_set_unique_id(unique_id)
|
||||
if not self._reauth_entry:
|
||||
self._abort_if_unique_id_configured()
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
mazda_client = MazdaAPI(
|
||||
user_input[CONF_EMAIL],
|
||||
user_input[CONF_PASSWORD],
|
||||
user_input[CONF_REGION],
|
||||
websession,
|
||||
)
|
||||
|
||||
try:
|
||||
await mazda_client.validate_credentials()
|
||||
except MazdaAuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except MazdaAccountLockedException:
|
||||
errors["base"] = "account_locked"
|
||||
except aiohttp.ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
errors["base"] = "unknown"
|
||||
_LOGGER.exception(
|
||||
"Unknown error occurred during Mazda login request: %s", ex
|
||||
)
|
||||
else:
|
||||
if not self._reauth_entry:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL], data=user_input
|
||||
)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._reauth_entry, data=user_input, unique_id=unique_id
|
||||
)
|
||||
# Reload the config entry otherwise devices will remain unavailable
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
||||
)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL, default=self._email): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_REGION, default=self._region): vol.In(
|
||||
MAZDA_REGIONS
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Perform reauth if the user credentials have changed."""
|
||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
self._email = entry_data[CONF_EMAIL]
|
||||
self._region = entry_data[CONF_REGION]
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Constants for the Mazda Connected Services integration."""
|
||||
|
||||
DOMAIN = "mazda"
|
||||
|
||||
DATA_CLIENT = "mazda_client"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DATA_REGION = "region"
|
||||
DATA_VEHICLES = "vehicles"
|
||||
|
||||
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Platform for Mazda device tracker integration."""
|
||||
from homeassistant.components.device_tracker import SourceType, TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the device tracker platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
|
||||
for index, _ in enumerate(coordinator.data):
|
||||
entities.append(MazdaDeviceTracker(client, coordinator, index))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MazdaDeviceTracker(MazdaEntity, TrackerEntity):
|
||||
"""Class for the device tracker."""
|
||||
|
||||
_attr_translation_key = "device_tracker"
|
||||
_attr_icon = "mdi:car"
|
||||
_attr_force_update = False
|
||||
|
||||
def __init__(self, client, coordinator, index) -> None:
|
||||
"""Initialize Mazda device tracker."""
|
||||
super().__init__(client, coordinator, index)
|
||||
|
||||
self._attr_unique_id = self.vin
|
||||
|
||||
@property
|
||||
def source_type(self) -> SourceType:
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return SourceType.GPS
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
return self.data["status"]["latitude"]
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
return self.data["status"]["longitude"]
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Diagnostics support for the Mazda integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import DATA_COORDINATOR, DOMAIN
|
||||
|
||||
TO_REDACT_INFO = [CONF_EMAIL, CONF_PASSWORD]
|
||||
TO_REDACT_DATA = ["vin", "id", "latitude", "longitude"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
diagnostics_data = {
|
||||
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
|
||||
"data": [
|
||||
async_redact_data(vehicle, TO_REDACT_DATA) for vehicle in coordinator.data
|
||||
],
|
||||
}
|
||||
|
||||
return diagnostics_data
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
vin = next(iter(device.identifiers))[1]
|
||||
|
||||
target_vehicle = None
|
||||
for vehicle in coordinator.data:
|
||||
if vehicle["vin"] == vin:
|
||||
target_vehicle = vehicle
|
||||
break
|
||||
|
||||
if target_vehicle is None:
|
||||
raise HomeAssistantError("Vehicle not found")
|
||||
|
||||
diagnostics_data = {
|
||||
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
|
||||
"data": async_redact_data(target_vehicle, TO_REDACT_DATA),
|
||||
}
|
||||
|
||||
return diagnostics_data
|
||||
@@ -1,58 +0,0 @@
|
||||
"""Platform for Mazda lock integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the lock platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
|
||||
for index, _ in enumerate(coordinator.data):
|
||||
entities.append(MazdaLock(client, coordinator, index))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MazdaLock(MazdaEntity, LockEntity):
|
||||
"""Class for the lock."""
|
||||
|
||||
_attr_translation_key = "lock"
|
||||
|
||||
def __init__(self, client, coordinator, index) -> None:
|
||||
"""Initialize Mazda lock."""
|
||||
super().__init__(client, coordinator, index)
|
||||
|
||||
self._attr_unique_id = self.vin
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool | None:
|
||||
"""Return true if lock is locked."""
|
||||
return self.client.get_assumed_lock_state(self.vehicle_id)
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the vehicle doors."""
|
||||
await self.client.lock_doors(self.vehicle_id)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the vehicle doors."""
|
||||
await self.client.unlock_doors(self.vehicle_id)
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"domain": "mazda",
|
||||
"name": "Mazda Connected Services",
|
||||
"codeowners": ["@bdr99"],
|
||||
"config_flow": true,
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pymazda"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pymazda==0.3.11"]
|
||||
"requirements": []
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
"""Platform for Mazda sensor integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
@dataclass
|
||||
class MazdaSensorRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
# Function to determine the value for this sensor, given the coordinator data
|
||||
# and the configured unit system
|
||||
value: Callable[[dict[str, Any]], StateType]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MazdaSensorEntityDescription(
|
||||
SensorEntityDescription, MazdaSensorRequiredKeysMixin
|
||||
):
|
||||
"""Describes a Mazda sensor entity."""
|
||||
|
||||
# Function to determine whether the vehicle supports this sensor,
|
||||
# given the coordinator data
|
||||
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
|
||||
|
||||
|
||||
def _fuel_remaining_percentage_supported(data):
|
||||
"""Determine if fuel remaining percentage is supported."""
|
||||
return (not data["isElectric"]) and (
|
||||
data["status"]["fuelRemainingPercent"] is not None
|
||||
)
|
||||
|
||||
|
||||
def _fuel_distance_remaining_supported(data):
|
||||
"""Determine if fuel distance remaining is supported."""
|
||||
return (not data["isElectric"]) and (
|
||||
data["status"]["fuelDistanceRemainingKm"] is not None
|
||||
)
|
||||
|
||||
|
||||
def _front_left_tire_pressure_supported(data):
|
||||
"""Determine if front left tire pressure is supported."""
|
||||
return data["status"]["tirePressure"]["frontLeftTirePressurePsi"] is not None
|
||||
|
||||
|
||||
def _front_right_tire_pressure_supported(data):
|
||||
"""Determine if front right tire pressure is supported."""
|
||||
return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None
|
||||
|
||||
|
||||
def _rear_left_tire_pressure_supported(data):
|
||||
"""Determine if rear left tire pressure is supported."""
|
||||
return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None
|
||||
|
||||
|
||||
def _rear_right_tire_pressure_supported(data):
|
||||
"""Determine if rear right tire pressure is supported."""
|
||||
return data["status"]["tirePressure"]["rearRightTirePressurePsi"] is not None
|
||||
|
||||
|
||||
def _ev_charge_level_supported(data):
|
||||
"""Determine if charge level is supported."""
|
||||
return (
|
||||
data["isElectric"]
|
||||
and data["evStatus"]["chargeInfo"]["batteryLevelPercentage"] is not None
|
||||
)
|
||||
|
||||
|
||||
def _ev_remaining_range_supported(data):
|
||||
"""Determine if remaining range is supported."""
|
||||
return (
|
||||
data["isElectric"]
|
||||
and data["evStatus"]["chargeInfo"]["drivingRangeKm"] is not None
|
||||
)
|
||||
|
||||
|
||||
def _fuel_distance_remaining_value(data):
|
||||
"""Get the fuel distance remaining value."""
|
||||
return round(data["status"]["fuelDistanceRemainingKm"])
|
||||
|
||||
|
||||
def _odometer_value(data):
|
||||
"""Get the odometer value."""
|
||||
# In order to match the behavior of the Mazda mobile app, we always round down
|
||||
return int(data["status"]["odometerKm"])
|
||||
|
||||
|
||||
def _front_left_tire_pressure_value(data):
|
||||
"""Get the front left tire pressure value."""
|
||||
return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"])
|
||||
|
||||
|
||||
def _front_right_tire_pressure_value(data):
|
||||
"""Get the front right tire pressure value."""
|
||||
return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"])
|
||||
|
||||
|
||||
def _rear_left_tire_pressure_value(data):
|
||||
"""Get the rear left tire pressure value."""
|
||||
return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"])
|
||||
|
||||
|
||||
def _rear_right_tire_pressure_value(data):
|
||||
"""Get the rear right tire pressure value."""
|
||||
return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"])
|
||||
|
||||
|
||||
def _ev_charge_level_value(data):
|
||||
"""Get the charge level value."""
|
||||
return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"])
|
||||
|
||||
|
||||
def _ev_remaining_range_value(data):
|
||||
"""Get the remaining range value."""
|
||||
return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"])
|
||||
|
||||
|
||||
SENSOR_ENTITIES = [
|
||||
MazdaSensorEntityDescription(
|
||||
key="fuel_remaining_percentage",
|
||||
translation_key="fuel_remaining_percentage",
|
||||
icon="mdi:gas-station",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_fuel_remaining_percentage_supported,
|
||||
value=lambda data: data["status"]["fuelRemainingPercent"],
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="fuel_distance_remaining",
|
||||
translation_key="fuel_distance_remaining",
|
||||
icon="mdi:gas-station",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_fuel_distance_remaining_supported,
|
||||
value=_fuel_distance_remaining_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="odometer",
|
||||
translation_key="odometer",
|
||||
icon="mdi:speedometer",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
is_supported=lambda data: data["status"]["odometerKm"] is not None,
|
||||
value=_odometer_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="front_left_tire_pressure",
|
||||
translation_key="front_left_tire_pressure",
|
||||
icon="mdi:car-tire-alert",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_front_left_tire_pressure_supported,
|
||||
value=_front_left_tire_pressure_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="front_right_tire_pressure",
|
||||
translation_key="front_right_tire_pressure",
|
||||
icon="mdi:car-tire-alert",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_front_right_tire_pressure_supported,
|
||||
value=_front_right_tire_pressure_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="rear_left_tire_pressure",
|
||||
translation_key="rear_left_tire_pressure",
|
||||
icon="mdi:car-tire-alert",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_rear_left_tire_pressure_supported,
|
||||
value=_rear_left_tire_pressure_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="rear_right_tire_pressure",
|
||||
translation_key="rear_right_tire_pressure",
|
||||
icon="mdi:car-tire-alert",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_rear_right_tire_pressure_supported,
|
||||
value=_rear_right_tire_pressure_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="ev_charge_level",
|
||||
translation_key="ev_charge_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_ev_charge_level_supported,
|
||||
value=_ev_charge_level_value,
|
||||
),
|
||||
MazdaSensorEntityDescription(
|
||||
key="ev_remaining_range",
|
||||
translation_key="ev_remaining_range",
|
||||
icon="mdi:ev-station",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
is_supported=_ev_remaining_range_supported,
|
||||
value=_ev_remaining_range_value,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
for index, data in enumerate(coordinator.data):
|
||||
for description in SENSOR_ENTITIES:
|
||||
if description.is_supported(data):
|
||||
entities.append(
|
||||
MazdaSensorEntity(client, coordinator, index, description)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MazdaSensorEntity(MazdaEntity, SensorEntity):
|
||||
"""Representation of a Mazda vehicle sensor."""
|
||||
|
||||
entity_description: MazdaSensorEntityDescription
|
||||
|
||||
def __init__(self, client, coordinator, index, description):
|
||||
"""Initialize Mazda sensor."""
|
||||
super().__init__(client, coordinator, index)
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = f"{self.vin}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value(self.data)
|
||||
@@ -1,30 +0,0 @@
|
||||
send_poi:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: mazda
|
||||
latitude:
|
||||
example: 12.34567
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -90
|
||||
max: 90
|
||||
unit_of_measurement: °
|
||||
mode: box
|
||||
longitude:
|
||||
example: -34.56789
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -180
|
||||
max: 180
|
||||
unit_of_measurement: °
|
||||
mode: box
|
||||
poi_name:
|
||||
example: Work
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
@@ -1,139 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"account_locked": "Account locked. Please try again later.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "Region"
|
||||
},
|
||||
"description": "Please enter the email address and password you use to log into the MyMazda mobile app."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"driver_door": {
|
||||
"name": "Driver door"
|
||||
},
|
||||
"passenger_door": {
|
||||
"name": "Passenger door"
|
||||
},
|
||||
"rear_left_door": {
|
||||
"name": "Rear left door"
|
||||
},
|
||||
"rear_right_door": {
|
||||
"name": "Rear right door"
|
||||
},
|
||||
"trunk": {
|
||||
"name": "Trunk"
|
||||
},
|
||||
"hood": {
|
||||
"name": "Hood"
|
||||
},
|
||||
"ev_plugged_in": {
|
||||
"name": "Plugged in"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"start_engine": {
|
||||
"name": "Start engine"
|
||||
},
|
||||
"stop_engine": {
|
||||
"name": "Stop engine"
|
||||
},
|
||||
"turn_on_hazard_lights": {
|
||||
"name": "Turn on hazard lights"
|
||||
},
|
||||
"turn_off_hazard_lights": {
|
||||
"name": "Turn off hazard lights"
|
||||
},
|
||||
"refresh_vehicle_status": {
|
||||
"name": "Refresh status"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"climate": {
|
||||
"name": "[%key:component::climate::title%]"
|
||||
}
|
||||
},
|
||||
"device_tracker": {
|
||||
"device_tracker": {
|
||||
"name": "[%key:component::device_tracker::title%]"
|
||||
}
|
||||
},
|
||||
"lock": {
|
||||
"lock": {
|
||||
"name": "[%key:component::lock::title%]"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"fuel_remaining_percentage": {
|
||||
"name": "Fuel remaining percentage"
|
||||
},
|
||||
"fuel_distance_remaining": {
|
||||
"name": "Fuel distance remaining"
|
||||
},
|
||||
"odometer": {
|
||||
"name": "Odometer"
|
||||
},
|
||||
"front_left_tire_pressure": {
|
||||
"name": "Front left tire pressure"
|
||||
},
|
||||
"front_right_tire_pressure": {
|
||||
"name": "Front right tire pressure"
|
||||
},
|
||||
"rear_left_tire_pressure": {
|
||||
"name": "Rear left tire pressure"
|
||||
},
|
||||
"rear_right_tire_pressure": {
|
||||
"name": "Rear right tire pressure"
|
||||
},
|
||||
"ev_charge_level": {
|
||||
"name": "Charge level"
|
||||
},
|
||||
"ev_remaining_range": {
|
||||
"name": "Remaining range"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"charging": {
|
||||
"name": "Charging"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_poi": {
|
||||
"name": "Send POI",
|
||||
"description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Vehicle",
|
||||
"description": "The vehicle to send the GPS location to."
|
||||
},
|
||||
"latitude": {
|
||||
"name": "[%key:common::config_flow::data::latitude%]",
|
||||
"description": "The latitude of the location to send."
|
||||
},
|
||||
"longitude": {
|
||||
"name": "[%key:common::config_flow::data::longitude%]",
|
||||
"description": "The longitude of the location to send."
|
||||
},
|
||||
"poi_name": {
|
||||
"name": "POI name",
|
||||
"description": "A friendly name for the location."
|
||||
}
|
||||
}
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The Mazda integration has been removed",
|
||||
"description": "The Mazda integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with their services, [has been taken offline by Mazda]({dmca}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Mazda integration entries]({entries})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Platform for Mazda switch integration."""
|
||||
from typing import Any
|
||||
|
||||
from pymazda import Client as MazdaAPIClient
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import MazdaEntity
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
MazdaChargingSwitch(client, coordinator, index)
|
||||
for index, data in enumerate(coordinator.data)
|
||||
if data["isElectric"]
|
||||
)
|
||||
|
||||
|
||||
class MazdaChargingSwitch(MazdaEntity, SwitchEntity):
|
||||
"""Class for the charging switch."""
|
||||
|
||||
_attr_translation_key = "charging"
|
||||
_attr_icon = "mdi:ev-station"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: MazdaAPIClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
index: int,
|
||||
) -> None:
|
||||
"""Initialize Mazda charging switch."""
|
||||
super().__init__(client, coordinator, index)
|
||||
|
||||
self._attr_unique_id = self.vin
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the vehicle is charging."""
|
||||
return self.data["evStatus"]["chargeInfo"]["charging"]
|
||||
|
||||
async def refresh_status_and_write_state(self):
|
||||
"""Request a status update, retrieve it through the coordinator, and write the state."""
|
||||
await self.client.refresh_vehicle_status(self.vehicle_id)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start charging the vehicle."""
|
||||
await self.client.start_charging(self.vehicle_id)
|
||||
|
||||
await self.refresh_status_and_write_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop charging the vehicle."""
|
||||
await self.client.stop_charging(self.vehicle_id)
|
||||
|
||||
await self.refresh_status_and_write_state()
|
||||
@@ -153,7 +153,7 @@ class MediaExtractor:
|
||||
except MEQueryException:
|
||||
_LOGGER.error("Wrong query format: %s", stream_query)
|
||||
return
|
||||
|
||||
_LOGGER.debug("Selected the following stream: %s", stream_url)
|
||||
data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID}
|
||||
data[ATTR_MEDIA_CONTENT_ID] = stream_url
|
||||
|
||||
@@ -193,9 +193,16 @@ def get_best_stream(formats: list[dict[str, Any]]) -> str:
|
||||
|
||||
|
||||
def get_best_stream_youtube(formats: list[dict[str, Any]]) -> str:
|
||||
"""YouTube requests also include manifest files.
|
||||
"""YouTube responses also include files with only video or audio.
|
||||
|
||||
They don't have a filesize so we skip all formats without filesize.
|
||||
So we filter on files with both audio and video codec.
|
||||
"""
|
||||
|
||||
return get_best_stream([format for format in formats if "filesize" in format])
|
||||
return get_best_stream(
|
||||
[
|
||||
format
|
||||
for format in formats
|
||||
if format.get("acodec", "none") != "none"
|
||||
and format.get("vcodec", "none") != "none"
|
||||
]
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mill", "mill_local"],
|
||||
"requirements": ["millheater==0.11.5", "mill-local==0.3.0"]
|
||||
"requirements": ["millheater==0.11.6", "mill-local==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
|
||||
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
|
||||
DataType.STRING: ENTRY("s", 1, PARM_IS_LEGAL(True, False, False, False, False)),
|
||||
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)),
|
||||
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
|
||||
}
|
||||
|
||||
@@ -143,7 +143,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
|
||||
f"{name}: Size of structure is {size} bytes but `{CONF_COUNT}: {count}` is {bytecount} bytes"
|
||||
)
|
||||
else:
|
||||
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
|
||||
if data_type != DataType.STRING:
|
||||
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
|
||||
if slave_count:
|
||||
structure = (
|
||||
f">{slave_count + 1}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized
|
||||
from homeassistant.helpers import config_validation as cv, event as ev, template
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -364,8 +364,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def _reload_config(call: ServiceCall) -> None:
|
||||
"""Reload the platforms."""
|
||||
# Fetch updated manual configured items and validate
|
||||
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
|
||||
# Fetch updated manually configured items and validate
|
||||
if (
|
||||
config_yaml := await async_integration_yaml_config(hass, DOMAIN)
|
||||
) is None:
|
||||
# Raise in case we have an invalid configuration
|
||||
raise HomeAssistantError(
|
||||
"Error reloading manually configured MQTT items, "
|
||||
"check your configuration.yaml"
|
||||
)
|
||||
mqtt_data.config = config_yaml.get(DOMAIN, {})
|
||||
|
||||
# Reload the modern yaml platforms
|
||||
|
||||
@@ -180,7 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
@write_state_on_attr_change(self, {"_attr_is_on"})
|
||||
@write_state_on_attr_change(self, {"_attr_is_on", "_expired"})
|
||||
def state_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle a new received MQTT state message."""
|
||||
# auto-expire enabled?
|
||||
|
||||
@@ -52,7 +52,7 @@ DEFAULT_SOURCE_TYPE = SourceType.GPS
|
||||
def valid_config(config: ConfigType) -> ConfigType:
|
||||
"""Check if there is a state topic or json_attributes_topic."""
|
||||
if CONF_STATE_TOPIC not in config and CONF_JSON_ATTRS_TOPIC not in config:
|
||||
raise vol.MultipleInvalid(
|
||||
raise vol.Invalid(
|
||||
f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}"
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -277,7 +277,9 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
)
|
||||
|
||||
@callback
|
||||
@write_state_on_attr_change(self, {"_attr_native_value", "_attr_last_reset"})
|
||||
@write_state_on_attr_change(
|
||||
self, {"_attr_native_value", "_attr_last_reset", "_expired"}
|
||||
)
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle new MQTT messages."""
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pkce", "pymyq"],
|
||||
"requirements": ["python-myq==3.1.9"]
|
||||
"requirements": ["python-myq==3.1.13"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any
|
||||
from mysensors import BaseAsyncGateway, Sensor
|
||||
from mysensors.sensor import ChildSensor
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -212,6 +212,8 @@ class MySensorsChildEntity(MySensorNodeEntity):
|
||||
|
||||
attr[ATTR_CHILD_ID] = self.child_id
|
||||
attr[ATTR_DESCRIPTION] = self._child.description
|
||||
# We should deprecate the battery level attribute in the future.
|
||||
attr[ATTR_BATTERY_LEVEL] = self._node.battery_level
|
||||
|
||||
set_req = self.gateway.const.SetReq
|
||||
for value_type, value in self._values.items():
|
||||
|
||||
@@ -58,7 +58,7 @@ def _get_stop_tags(
|
||||
# Append directions for stops with shared titles
|
||||
for tag, title in tags.items():
|
||||
if title_counts[title] > 1:
|
||||
tags[tag] = f"{title} ({stop_directions[tag]})"
|
||||
tags[tag] = f"{title} ({stop_directions.get(tag, tag)})"
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"submit_movie_request": {
|
||||
"name": "Sumbit movie request",
|
||||
"name": "Submit movie request",
|
||||
"description": "Searches for a movie and requests the first result.",
|
||||
"fields": {
|
||||
"name": {
|
||||
|
||||
@@ -48,15 +48,16 @@ async def async_parse_motion_alarm(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:VideoSource/MotionAlarm
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Motion Alarm",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -71,15 +72,16 @@ async def async_parse_image_too_blurry(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:VideoSource/ImageTooBlurry/*
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Image Too Blurry",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -95,15 +97,16 @@ async def async_parse_image_too_dark(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:VideoSource/ImageTooDark/*
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Image Too Dark",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -119,15 +122,16 @@ async def async_parse_image_too_bright(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:VideoSource/ImageTooBright/*
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Image Too Bright",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -143,15 +147,16 @@ async def async_parse_scene_change(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:VideoSource/GlobalSceneChange/*
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Global Scene Change",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -167,8 +172,9 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None:
|
||||
audio_source = ""
|
||||
audio_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "AudioSourceConfigurationToken":
|
||||
audio_source = source.Value
|
||||
if source.Name == "AudioAnalyticsConfigurationToken":
|
||||
@@ -177,12 +183,12 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{audio_source}_{audio_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{audio_source}_{audio_analytics}_{rule}",
|
||||
"Detected Sound",
|
||||
"binary_sensor",
|
||||
"sound",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -198,8 +204,9 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -208,12 +215,12 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
evt = Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Field Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
return evt
|
||||
except (AttributeError, KeyError):
|
||||
@@ -230,8 +237,9 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -240,12 +248,12 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Cell Motion Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -261,8 +269,9 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -271,12 +280,12 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Motion Region Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value in ["1", "true"],
|
||||
message_value.Data.SimpleItem[0].Value in ["1", "true"],
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -292,8 +301,9 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -302,12 +312,12 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Tamper Detection",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -322,18 +332,19 @@ async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
try:
|
||||
video_source = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}",
|
||||
f"{uid}_{topic_value}_{video_source}",
|
||||
"Pet Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -347,18 +358,19 @@ async def async_parse_vehicle_detector(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
try:
|
||||
video_source = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}",
|
||||
f"{uid}_{topic_value}_{video_source}",
|
||||
"Vehicle Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -372,18 +384,19 @@ async def async_parse_person_detector(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
try:
|
||||
video_source = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}",
|
||||
f"{uid}_{topic_value}_{video_source}",
|
||||
"Person Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -397,18 +410,19 @@ async def async_parse_face_detector(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
try:
|
||||
video_source = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}",
|
||||
f"{uid}_{topic_value}_{video_source}",
|
||||
"Face Detection",
|
||||
"binary_sensor",
|
||||
"motion",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -422,18 +436,19 @@ async def async_parse_visitor_detector(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
try:
|
||||
video_source = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "Source":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}",
|
||||
f"{uid}_{topic_value}_{video_source}",
|
||||
"Visitor Detection",
|
||||
"binary_sensor",
|
||||
"occupancy",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -446,15 +461,16 @@ async def async_parse_digital_input(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Device/Trigger/DigitalInput
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Digital Input",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -467,15 +483,16 @@ async def async_parse_relay(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Device/Trigger/Relay
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Relay Triggered",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "active",
|
||||
message_value.Data.SimpleItem[0].Value == "active",
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
@@ -488,15 +505,16 @@ async def async_parse_storage_failure(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Device/HardwareFailure/StorageFailure
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Storage Failure",
|
||||
"binary_sensor",
|
||||
"problem",
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "true",
|
||||
message_value.Data.SimpleItem[0].Value == "true",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -510,13 +528,14 @@ async def async_parse_processor_usage(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Monitoring/ProcessorUsage
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
usage = float(value_1.Data.SimpleItem[0].Value)
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
usage = float(message_value.Data.SimpleItem[0].Value)
|
||||
if usage <= 1:
|
||||
usage *= 100
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}",
|
||||
f"{uid}_{topic_value}",
|
||||
"Processor Usage",
|
||||
"sensor",
|
||||
None,
|
||||
@@ -535,10 +554,11 @@ async def async_parse_last_reboot(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Monitoring/OperatingTime/LastReboot
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{value_1}",
|
||||
f"{uid}_{topic_value}",
|
||||
"Last Reboot",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
@@ -557,10 +577,11 @@ async def async_parse_last_reset(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Monitoring/OperatingTime/LastReset
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{value_1}",
|
||||
f"{uid}_{topic_value}",
|
||||
"Last Reset",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
@@ -581,10 +602,11 @@ async def async_parse_backup_last(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{value_1}",
|
||||
f"{uid}_{topic_value}",
|
||||
"Last Backup",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
@@ -604,10 +626,11 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event | None:
|
||||
Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization
|
||||
"""
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
|
||||
return Event(
|
||||
f"{uid}_{value_1}",
|
||||
f"{uid}_{topic_value}",
|
||||
"Last Clock Synchronization",
|
||||
"sensor",
|
||||
"timestamp",
|
||||
@@ -628,15 +651,16 @@ async def async_parse_jobstate(uid: str, msg) -> Event | None:
|
||||
"""
|
||||
|
||||
try:
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
source = value_1.Source.SimpleItem[0].Value
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
source = message_value.Source.SimpleItem[0].Value
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{source}",
|
||||
f"{uid}_{topic_value}_{source}",
|
||||
"Recording Job State",
|
||||
"binary_sensor",
|
||||
None,
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value == "Active",
|
||||
message_value.Data.SimpleItem[0].Value == "Active",
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -653,8 +677,9 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = source.Value
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -663,12 +688,12 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Line Detector Crossed",
|
||||
"sensor",
|
||||
None,
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value,
|
||||
message_value.Data.SimpleItem[0].Value,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
@@ -685,8 +710,9 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
|
||||
video_source = ""
|
||||
video_analytics = ""
|
||||
rule = ""
|
||||
value_1 = msg.Message._value_1 # pylint: disable=protected-access
|
||||
for source in value_1.Source.SimpleItem:
|
||||
message_value = msg.Message._value_1 # pylint: disable=protected-access
|
||||
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
|
||||
for source in message_value.Source.SimpleItem:
|
||||
if source.Name == "VideoSourceConfigurationToken":
|
||||
video_source = _normalize_video_source(source.Value)
|
||||
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||
@@ -695,12 +721,12 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
|
||||
rule = source.Value
|
||||
|
||||
return Event(
|
||||
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
|
||||
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
|
||||
"Count Aggregation Counter",
|
||||
"sensor",
|
||||
None,
|
||||
None,
|
||||
value_1.Data.SimpleItem[0].Value,
|
||||
message_value.Data.SimpleItem[0].Value,
|
||||
EntityCategory.DIAGNOSTIC,
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
|
||||
@@ -79,6 +79,8 @@ class OpenHardwareMonitorDevice(SensorEntity):
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the device."""
|
||||
if self.value == "-":
|
||||
return None
|
||||
return self.value
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.0.34"]
|
||||
"requirements": ["opower==0.0.35"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["haphilipsjs"],
|
||||
"requirements": ["ha-philipsjs==3.1.0"]
|
||||
"requirements": ["ha-philipsjs==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["plexapi", "plexwebsocket"],
|
||||
"requirements": [
|
||||
"PlexAPI==4.13.2",
|
||||
"PlexAPI==4.15.3",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.13"
|
||||
],
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
"name": "DHW comfort mode"
|
||||
},
|
||||
"lock": {
|
||||
"name": "[%key:component::lock::entity_component::_::name%]"
|
||||
"name": "[%key:component::lock::title%]"
|
||||
},
|
||||
"relay": {
|
||||
"name": "Relay"
|
||||
|
||||
@@ -277,7 +277,8 @@ class MinutPointEntity(Entity):
|
||||
sw_version=device["firmware"]["installed"],
|
||||
via_device=(DOMAIN, device["home"]),
|
||||
)
|
||||
self._attr_name = f"{self._name} {device_class.capitalize()}"
|
||||
if device_class:
|
||||
self._attr_name = f"{self._name} {device_class.capitalize()}"
|
||||
|
||||
def __str__(self):
|
||||
"""Return string representation of device."""
|
||||
|
||||
@@ -19,6 +19,30 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_IRK = "irk"
|
||||
|
||||
|
||||
def _parse_irk(irk: str) -> bytes | None:
|
||||
if irk.startswith("irk:"):
|
||||
irk = irk[4:]
|
||||
|
||||
if irk.endswith("="):
|
||||
try:
|
||||
irk_bytes = bytes(reversed(base64.b64decode(irk)))
|
||||
except binascii.Error:
|
||||
# IRK is not valid base64
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
irk_bytes = binascii.unhexlify(irk)
|
||||
except binascii.Error:
|
||||
# IRK is not correctly hex encoded
|
||||
return None
|
||||
|
||||
if len(irk_bytes) != 16:
|
||||
# IRK must be 16 bytes when decoded
|
||||
return None
|
||||
|
||||
return irk_bytes
|
||||
|
||||
|
||||
class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for BLE Device Tracker."""
|
||||
|
||||
@@ -35,15 +59,8 @@ class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
irk = user_input[CONF_IRK]
|
||||
if irk.startswith("irk:"):
|
||||
irk = irk[4:]
|
||||
|
||||
if irk.endswith("="):
|
||||
irk_bytes = bytes(reversed(base64.b64decode(irk)))
|
||||
else:
|
||||
irk_bytes = binascii.unhexlify(irk)
|
||||
|
||||
if len(irk_bytes) != 16:
|
||||
if not (irk_bytes := _parse_irk(irk)):
|
||||
errors[CONF_IRK] = "irk_not_valid"
|
||||
elif not (service_info := async_last_service_info(self.hass, irk_bytes)):
|
||||
errors[CONF_IRK] = "irk_not_found"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user