mirror of
https://github.com/home-assistant/core.git
synced 2025-10-21 01:29:46 +00:00
Compare commits
3 Commits
2023.12.1
...
fix-host-d
Author | SHA1 | Date | |
---|---|---|---|
![]() |
83458d24c7 | ||
![]() |
df6d43adc4 | ||
![]() |
5cbfc1c224 |
@@ -633,6 +633,8 @@ omit =
|
|||||||
homeassistant/components/kodi/browse_media.py
|
homeassistant/components/kodi/browse_media.py
|
||||||
homeassistant/components/kodi/media_player.py
|
homeassistant/components/kodi/media_player.py
|
||||||
homeassistant/components/kodi/notify.py
|
homeassistant/components/kodi/notify.py
|
||||||
|
homeassistant/components/komfovent/__init__.py
|
||||||
|
homeassistant/components/komfovent/climate.py
|
||||||
homeassistant/components/konnected/__init__.py
|
homeassistant/components/konnected/__init__.py
|
||||||
homeassistant/components/konnected/panel.py
|
homeassistant/components/konnected/panel.py
|
||||||
homeassistant/components/konnected/switch.py
|
homeassistant/components/konnected/switch.py
|
||||||
|
@@ -259,8 +259,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||||
/homeassistant/components/derivative/ @afaucogney
|
/homeassistant/components/derivative/ @afaucogney
|
||||||
/tests/components/derivative/ @afaucogney
|
/tests/components/derivative/ @afaucogney
|
||||||
/homeassistant/components/devialet/ @fwestenberg
|
|
||||||
/tests/components/devialet/ @fwestenberg
|
|
||||||
/homeassistant/components/device_automation/ @home-assistant/core
|
/homeassistant/components/device_automation/ @home-assistant/core
|
||||||
/tests/components/device_automation/ @home-assistant/core
|
/tests/components/device_automation/ @home-assistant/core
|
||||||
/homeassistant/components/device_tracker/ @home-assistant/core
|
/homeassistant/components/device_tracker/ @home-assistant/core
|
||||||
@@ -663,6 +661,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||||
/homeassistant/components/kodi/ @OnFreund
|
/homeassistant/components/kodi/ @OnFreund
|
||||||
/tests/components/kodi/ @OnFreund
|
/tests/components/kodi/ @OnFreund
|
||||||
|
/homeassistant/components/komfovent/ @ProstoSanja
|
||||||
|
/tests/components/komfovent/ @ProstoSanja
|
||||||
/homeassistant/components/konnected/ @heythisisnate
|
/homeassistant/components/konnected/ @heythisisnate
|
||||||
/tests/components/konnected/ @heythisisnate
|
/tests/components/konnected/ @heythisisnate
|
||||||
/homeassistant/components/kostal_plenticore/ @stegm
|
/homeassistant/components/kostal_plenticore/ @stegm
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
# Automatically generated by hassfest.
|
|
||||||
#
|
|
||||||
# To update, run python3 -m script.hassfest -p docker
|
|
||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adax", "adax_local"],
|
"loggers": ["adax", "adax_local"],
|
||||||
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
|
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The IP address of the Agent DVR server."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -3,6 +3,7 @@ from typing import Final
|
|||||||
|
|
||||||
DOMAIN: Final = "airq"
|
DOMAIN: Final = "airq"
|
||||||
MANUFACTURER: Final = "CorantGmbH"
|
MANUFACTURER: Final = "CorantGmbH"
|
||||||
|
TARGET_ROUTE: Final = "average"
|
||||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
||||||
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
||||||
UPDATE_INTERVAL: float = 10.0
|
UPDATE_INTERVAL: float = 10.0
|
||||||
|
@@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER, UPDATE_INTERVAL
|
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,4 +56,6 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||||||
hw_version=info["hw_version"],
|
hw_version=info["hw_version"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return await self.airq.get_latest_data()
|
|
||||||
|
data = await self.airq.get(TARGET_ROUTE)
|
||||||
|
return self.airq.drop_uncertainties_from_data(data)
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairq"],
|
"loggers": ["aioairq"],
|
||||||
"requirements": ["aioairq==0.3.1"]
|
"requirements": ["aioairq==0.2.4"]
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
"device_path": "Device Path"
|
"device_path": "Device Path"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
|
"host": "The hostname or IP address of AlarDecoder device that is connected to your alarm panel.",
|
||||||
"port": "The port on which AlarmDecoder is accessible (for example, 10000)"
|
"port": "The port on which AlarmDecoder is accessible (for example, 10000)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,6 @@
|
|||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The IP address of the device running the Android IP Webcam app. The IP address is shown in the app once you start the server."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -41,6 +41,7 @@ from homeassistant.exceptions import (
|
|||||||
Unauthorized,
|
Unauthorized,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import config_validation as cv, template
|
from homeassistant.helpers import config_validation as cv, template
|
||||||
|
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||||
from homeassistant.helpers.event import EventStateChangedData
|
from homeassistant.helpers.event import EventStateChangedData
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
from homeassistant.helpers.service import async_get_all_descriptions
|
from homeassistant.helpers.service import async_get_all_descriptions
|
||||||
@@ -217,11 +218,9 @@ class APIStatesView(HomeAssistantView):
|
|||||||
if entity_perm(state.entity_id, "read")
|
if entity_perm(state.entity_id, "read")
|
||||||
)
|
)
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
body=f'[{",".join(states)}]',
|
body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON
|
||||||
content_type=CONTENT_TYPE_JSON,
|
|
||||||
zlib_executor_size=32768,
|
|
||||||
)
|
)
|
||||||
response.enable_compression()
|
enable_compression(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -391,14 +390,17 @@ class APIDomainServicesView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# shield the service call from cancellation on connection drop
|
async with timeout(SERVICE_WAIT_TIMEOUT):
|
||||||
await shield(
|
# shield the service call from cancellation on connection drop
|
||||||
hass.services.async_call(
|
await shield(
|
||||||
domain, service, data, blocking=True, context=context
|
hass.services.async_call(
|
||||||
|
domain, service, data, blocking=True, context=context
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
except (vol.Invalid, ServiceNotFound) as ex:
|
except (vol.Invalid, ServiceNotFound) as ex:
|
||||||
raise HTTPBadRequest() from ex
|
raise HTTPBadRequest() from ex
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
cancel_listen()
|
cancel_listen()
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Empty, Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Final, cast
|
from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
@@ -1010,8 +1010,8 @@ class PipelineRun:
|
|||||||
self.tts_engine = engine
|
self.tts_engine = engine
|
||||||
self.tts_options = tts_options
|
self.tts_options = tts_options
|
||||||
|
|
||||||
async def text_to_speech(self, tts_input: str) -> None:
|
async def text_to_speech(self, tts_input: str) -> str:
|
||||||
"""Run text-to-speech portion of pipeline."""
|
"""Run text-to-speech portion of pipeline. Returns URL of TTS audio."""
|
||||||
self.process_event(
|
self.process_event(
|
||||||
PipelineEvent(
|
PipelineEvent(
|
||||||
PipelineEventType.TTS_START,
|
PipelineEventType.TTS_START,
|
||||||
@@ -1024,40 +1024,43 @@ class PipelineRun:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if tts_input := tts_input.strip():
|
try:
|
||||||
try:
|
# Synthesize audio and get URL
|
||||||
# Synthesize audio and get URL
|
tts_media_id = tts_generate_media_source_id(
|
||||||
tts_media_id = tts_generate_media_source_id(
|
self.hass,
|
||||||
self.hass,
|
tts_input,
|
||||||
tts_input,
|
engine=self.tts_engine,
|
||||||
engine=self.tts_engine,
|
language=self.pipeline.tts_language,
|
||||||
language=self.pipeline.tts_language,
|
options=self.tts_options,
|
||||||
options=self.tts_options,
|
)
|
||||||
)
|
tts_media = await media_source.async_resolve_media(
|
||||||
tts_media = await media_source.async_resolve_media(
|
self.hass,
|
||||||
self.hass,
|
tts_media_id,
|
||||||
tts_media_id,
|
None,
|
||||||
None,
|
)
|
||||||
)
|
except Exception as src_error:
|
||||||
except Exception as src_error:
|
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
raise TextToSpeechError(
|
||||||
raise TextToSpeechError(
|
code="tts-failed",
|
||||||
code="tts-failed",
|
message="Unexpected error during text-to-speech",
|
||||||
message="Unexpected error during text-to-speech",
|
) from src_error
|
||||||
) from src_error
|
|
||||||
|
|
||||||
_LOGGER.debug("TTS result %s", tts_media)
|
_LOGGER.debug("TTS result %s", tts_media)
|
||||||
tts_output = {
|
|
||||||
"media_id": tts_media_id,
|
|
||||||
**asdict(tts_media),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
tts_output = {}
|
|
||||||
|
|
||||||
self.process_event(
|
self.process_event(
|
||||||
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
|
PipelineEvent(
|
||||||
|
PipelineEventType.TTS_END,
|
||||||
|
{
|
||||||
|
"tts_output": {
|
||||||
|
"media_id": tts_media_id,
|
||||||
|
**asdict(tts_media),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return tts_media.url
|
||||||
|
|
||||||
def _capture_chunk(self, audio_bytes: bytes | None) -> None:
|
def _capture_chunk(self, audio_bytes: bytes | None) -> None:
|
||||||
"""Forward audio chunk to various capturing mechanisms."""
|
"""Forward audio chunk to various capturing mechanisms."""
|
||||||
if self.debug_recording_queue is not None:
|
if self.debug_recording_queue is not None:
|
||||||
@@ -1244,8 +1247,6 @@ def _pipeline_debug_recording_thread_proc(
|
|||||||
# Chunk of 16-bit mono audio at 16Khz
|
# Chunk of 16-bit mono audio at 16Khz
|
||||||
if wav_writer is not None:
|
if wav_writer is not None:
|
||||||
wav_writer.writeframes(message)
|
wav_writer.writeframes(message)
|
||||||
except Empty:
|
|
||||||
pass # occurs when pipeline has unexpected error
|
|
||||||
except Exception: # pylint: disable=broad-exception-caught
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
_LOGGER.exception("Unexpected error in debug recording thread")
|
_LOGGER.exception("Unexpected error in debug recording thread")
|
||||||
finally:
|
finally:
|
||||||
|
@@ -55,9 +55,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
_AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge")
|
_AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge")
|
||||||
_FuncType = Callable[
|
_FuncType = Callable[[_AsusWrtBridgeT], Awaitable[list[Any] | dict[str, Any]]]
|
||||||
[_AsusWrtBridgeT], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]
|
|
||||||
]
|
|
||||||
_ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]]
|
_ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]]
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +81,7 @@ def handle_errors_and_zip(
|
|||||||
|
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
return dict(zip(keys, list(data.values())))
|
return dict(zip(keys, list(data.values())))
|
||||||
if not isinstance(data, (list, tuple)):
|
if not isinstance(data, list):
|
||||||
raise UpdateFailed("Received invalid data type")
|
raise UpdateFailed("Received invalid data type")
|
||||||
return dict(zip(keys, data))
|
return dict(zip(keys, data))
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "AsusWRT",
|
||||||
"description": "Set required parameter to connect to your router",
|
"description": "Set required parameter to connect to your router",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
@@ -10,12 +11,10 @@
|
|||||||
"ssh_key": "Path to your SSH key file (instead of password)",
|
"ssh_key": "Path to your SSH key file (instead of password)",
|
||||||
"protocol": "Communication protocol to use",
|
"protocol": "Communication protocol to use",
|
||||||
"port": "Port (leave empty for protocol default)"
|
"port": "Port (leave empty for protocol default)"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your ASUSWRT router."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"legacy": {
|
"legacy": {
|
||||||
|
"title": "AsusWRT",
|
||||||
"description": "Set required parameters to connect to your router",
|
"description": "Set required parameters to connect to your router",
|
||||||
"data": {
|
"data": {
|
||||||
"mode": "Router operating mode"
|
"mode": "Router operating mode"
|
||||||
@@ -38,6 +37,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
|
"title": "AsusWRT Options",
|
||||||
"data": {
|
"data": {
|
||||||
"consider_home": "Seconds to wait before considering a device away",
|
"consider_home": "Seconds to wait before considering a device away",
|
||||||
"track_unknown": "Track unknown / unnamed devices",
|
"track_unknown": "Track unknown / unnamed devices",
|
||||||
|
@@ -2,13 +2,10 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Connect to the device",
|
"title": "Connect to the device",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the Atag device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -3,16 +3,12 @@
|
|||||||
"flow_title": "{name} ({host})",
|
"flow_title": "{name} ({host})",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Set up an Axis device",
|
"title": "Set up Axis device",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the Axis device.",
|
|
||||||
"username": "The user name you set up on your Axis device. It is recommended to create a user specifically for Home Assistant."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -93,6 +93,8 @@ class BAFFan(BAFEntity, FanEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set the preset mode of the fan."""
|
"""Set the preset mode of the fan."""
|
||||||
|
if preset_mode != PRESET_MODE_AUTO:
|
||||||
|
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||||
self._device.fan_mode = OffOnAuto.AUTO
|
self._device.fan_mode = OffOnAuto.AUTO
|
||||||
|
|
||||||
async def async_set_direction(self, direction: str) -> None:
|
async def async_set_direction(self, direction: str) -> None:
|
||||||
|
@@ -2,12 +2,9 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Connect to the Balboa Wi-Fi device",
|
"title": "Connect to the Balboa Wi-Fi device",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "Hostname or IP address of your Balboa Spa Wifi Device. For example, 192.168.1.58."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
|
|
||||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
||||||
from .coordinator import BlinkUpdateCoordinator
|
from .coordinator import BlinkUpdateCoordinator
|
||||||
from .services import setup_services
|
from .services import async_setup_services
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up Blink."""
|
"""Set up Blink."""
|
||||||
|
|
||||||
setup_services(hass)
|
await async_setup_services(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
"""Services for the Blink integration."""
|
"""Services for the Blink integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
@@ -12,7 +14,7 @@ from homeassistant.const import (
|
|||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
|
|
||||||
@@ -25,67 +27,56 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .coordinator import BlinkUpdateCoordinator
|
from .coordinator import BlinkUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_FILENAME): cv.string,
|
vol.Required(CONF_FILENAME): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||||
{
|
{vol.Required(ATTR_DEVICE_ID): cv.ensure_list, vol.Optional(CONF_PIN): cv.string}
|
||||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
|
||||||
vol.Optional(CONF_PIN): cv.string,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_FILE_PATH): cv.string,
|
vol.Required(CONF_FILE_PATH): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_services(hass: HomeAssistant) -> None:
|
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||||
"""Set up the services for the Blink integration."""
|
"""Set up the services for the Blink integration."""
|
||||||
|
|
||||||
def collect_coordinators(
|
async def collect_coordinators(
|
||||||
device_ids: list[str],
|
device_ids: list[str],
|
||||||
) -> list[BlinkUpdateCoordinator]:
|
) -> list[BlinkUpdateCoordinator]:
|
||||||
config_entries: list[ConfigEntry] = []
|
config_entries = list[ConfigEntry]()
|
||||||
registry = dr.async_get(hass)
|
registry = dr.async_get(hass)
|
||||||
for target in device_ids:
|
for target in device_ids:
|
||||||
device = registry.async_get(target)
|
device = registry.async_get(target)
|
||||||
if device:
|
if device:
|
||||||
device_entries: list[ConfigEntry] = []
|
device_entries = list[ConfigEntry]()
|
||||||
for entry_id in device.config_entries:
|
for entry_id in device.config_entries:
|
||||||
entry = hass.config_entries.async_get_entry(entry_id)
|
entry = hass.config_entries.async_get_entry(entry_id)
|
||||||
if entry and entry.domain == DOMAIN:
|
if entry and entry.domain == DOMAIN:
|
||||||
device_entries.append(entry)
|
device_entries.append(entry)
|
||||||
if not device_entries:
|
if not device_entries:
|
||||||
raise ServiceValidationError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
f"Device '{target}' is not a {DOMAIN} device"
|
||||||
translation_key="invalid_device",
|
|
||||||
translation_placeholders={"target": target, "domain": DOMAIN},
|
|
||||||
)
|
)
|
||||||
config_entries.extend(device_entries)
|
config_entries.extend(device_entries)
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
f"Device '{target}' not found in device registry"
|
||||||
translation_key="device_not_found",
|
|
||||||
translation_placeholders={"target": target},
|
|
||||||
)
|
)
|
||||||
|
coordinators = list[BlinkUpdateCoordinator]()
|
||||||
coordinators: list[BlinkUpdateCoordinator] = []
|
|
||||||
for config_entry in config_entries:
|
for config_entry in config_entries:
|
||||||
if config_entry.state != ConfigEntryState.LOADED:
|
if config_entry.state != ConfigEntryState.LOADED:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="not_loaded",
|
|
||||||
translation_placeholders={"target": config_entry.title},
|
|
||||||
)
|
|
||||||
|
|
||||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||||
return coordinators
|
return coordinators
|
||||||
|
|
||||||
@@ -94,36 +85,24 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||||||
camera_name = call.data[CONF_NAME]
|
camera_name = call.data[CONF_NAME]
|
||||||
video_path = call.data[CONF_FILENAME]
|
video_path = call.data[CONF_FILENAME]
|
||||||
if not hass.config.is_allowed_path(video_path):
|
if not hass.config.is_allowed_path(video_path):
|
||||||
raise ServiceValidationError(
|
_LOGGER.error("Can't write %s, no access to path!", video_path)
|
||||||
translation_domain=DOMAIN,
|
return
|
||||||
translation_key="no_path",
|
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||||
translation_placeholders={"target": video_path},
|
|
||||||
)
|
|
||||||
|
|
||||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
|
||||||
all_cameras = coordinator.api.cameras
|
all_cameras = coordinator.api.cameras
|
||||||
if camera_name in all_cameras:
|
if camera_name in all_cameras:
|
||||||
try:
|
try:
|
||||||
await all_cameras[camera_name].video_to_file(video_path)
|
await all_cameras[camera_name].video_to_file(video_path)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise ServiceValidationError(
|
_LOGGER.error("Can't write image to file: %s", err)
|
||||||
str(err),
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="cant_write",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
|
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
|
||||||
"""Save multiple recent clips to output directory."""
|
"""Save multiple recent clips to output directory."""
|
||||||
camera_name = call.data[CONF_NAME]
|
camera_name = call.data[CONF_NAME]
|
||||||
clips_dir = call.data[CONF_FILE_PATH]
|
clips_dir = call.data[CONF_FILE_PATH]
|
||||||
if not hass.config.is_allowed_path(clips_dir):
|
if not hass.config.is_allowed_path(clips_dir):
|
||||||
raise ServiceValidationError(
|
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
|
||||||
translation_domain=DOMAIN,
|
return
|
||||||
translation_key="no_path",
|
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||||
translation_placeholders={"target": clips_dir},
|
|
||||||
)
|
|
||||||
|
|
||||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
|
||||||
all_cameras = coordinator.api.cameras
|
all_cameras = coordinator.api.cameras
|
||||||
if camera_name in all_cameras:
|
if camera_name in all_cameras:
|
||||||
try:
|
try:
|
||||||
@@ -131,15 +110,11 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||||||
output_dir=clips_dir
|
output_dir=clips_dir
|
||||||
)
|
)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise ServiceValidationError(
|
_LOGGER.error("Can't write recent clips to directory: %s", err)
|
||||||
str(err),
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="cant_write",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
async def send_pin(call: ServiceCall):
|
async def send_pin(call: ServiceCall):
|
||||||
"""Call blink to send new pin."""
|
"""Call blink to send new pin."""
|
||||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||||
await coordinator.api.auth.send_auth_key(
|
await coordinator.api.auth.send_auth_key(
|
||||||
coordinator.api,
|
coordinator.api,
|
||||||
call.data[CONF_PIN],
|
call.data[CONF_PIN],
|
||||||
@@ -147,7 +122,7 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
async def blink_refresh(call: ServiceCall):
|
async def blink_refresh(call: ServiceCall):
|
||||||
"""Call blink to refresh info."""
|
"""Call blink to refresh info."""
|
||||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||||
await coordinator.api.refresh(force_cache=True)
|
await coordinator.api.refresh(force_cache=True)
|
||||||
|
|
||||||
# Register all the above services
|
# Register all the above services
|
||||||
|
@@ -101,22 +101,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"invalid_device": {
|
|
||||||
"message": "Device '{target}' is not a {domain} device"
|
|
||||||
},
|
|
||||||
"device_not_found": {
|
|
||||||
"message": "Device '{target}' not found in device registry"
|
|
||||||
},
|
|
||||||
"no_path": {
|
|
||||||
"message": "Can't write to directory {target}, no access to path!"
|
|
||||||
},
|
|
||||||
"cant_write": {
|
|
||||||
"message": "Can't write to file"
|
|
||||||
},
|
|
||||||
"not_loaded": {
|
|
||||||
"message": "{target} is not loaded"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["bimmer_connected"],
|
"loggers": ["bimmer_connected"],
|
||||||
"requirements": ["bimmer-connected[china]==0.14.6"]
|
"requirements": ["bimmer-connected==0.14.3"]
|
||||||
}
|
}
|
||||||
|
@@ -199,6 +199,10 @@ class BondFan(BondEntity, FanEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set the preset mode of the fan."""
|
"""Set the preset mode of the fan."""
|
||||||
|
if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action(
|
||||||
|
Action.BREEZE_ON
|
||||||
|
):
|
||||||
|
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||||
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
|
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
@@ -12,9 +12,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The IP address of your Bond hub."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"title": "SHC authentication parameters",
|
"title": "SHC authentication parameters",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Bosch Smart Home Controller."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"credentials": {
|
"credentials": {
|
||||||
|
@@ -5,9 +5,6 @@
|
|||||||
"description": "Ensure that your TV is turned on before trying to set it up.",
|
"description": "Ensure that your TV is turned on before trying to set it up.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the Sony Bravia TV to control."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"authorize": {
|
"authorize": {
|
||||||
|
@@ -3,13 +3,10 @@
|
|||||||
"flow_title": "{name} ({model} at {host})",
|
"flow_title": "{name} ({model} at {host})",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Connect to the device",
|
"title": "Connect to the device",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"timeout": "Timeout"
|
"timeout": "Timeout"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Broadlink device."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"type": "Type of the printer"
|
"type": "Type of the printer"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the Brother printer to control."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zeroconf_confirm": {
|
"zeroconf_confirm": {
|
||||||
|
@@ -60,7 +60,8 @@ async def async_setup_entry(
|
|||||||
data.static,
|
data.static,
|
||||||
entry,
|
entry,
|
||||||
)
|
)
|
||||||
]
|
],
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -11,9 +11,6 @@
|
|||||||
"passkey": "Passkey string",
|
"passkey": "Passkey string",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your BSB-Lan device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -11,11 +11,7 @@ async def async_get_calendars(
|
|||||||
hass: HomeAssistant, client: caldav.DAVClient, component: str
|
hass: HomeAssistant, client: caldav.DAVClient, component: str
|
||||||
) -> list[caldav.Calendar]:
|
) -> list[caldav.Calendar]:
|
||||||
"""Get all calendars that support the specified component."""
|
"""Get all calendars that support the specified component."""
|
||||||
|
calendars = await hass.async_add_executor_job(client.principal().calendars)
|
||||||
def _get_calendars() -> list[caldav.Calendar]:
|
|
||||||
return client.principal().calendars()
|
|
||||||
|
|
||||||
calendars = await hass.async_add_executor_job(_get_calendars)
|
|
||||||
components_results = await asyncio.gather(
|
components_results = await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
hass.async_add_executor_job(calendar.get_supported_components)
|
hass.async_add_executor_job(calendar.get_supported_components)
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
import caldav
|
import caldav
|
||||||
from caldav.lib.error import DAVError, NotFoundError
|
from caldav.lib.error import DAVError, NotFoundError
|
||||||
@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .api import async_get_calendars, get_attr_value
|
from .api import async_get_calendars, get_attr_value
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -72,12 +71,6 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
|||||||
or (summary := get_attr_value(todo, "summary")) is None
|
or (summary := get_attr_value(todo, "summary")) is None
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
due: date | datetime | None = None
|
|
||||||
if due_value := get_attr_value(todo, "due"):
|
|
||||||
if isinstance(due_value, datetime):
|
|
||||||
due = dt_util.as_local(due_value)
|
|
||||||
elif isinstance(due_value, date):
|
|
||||||
due = due_value
|
|
||||||
return TodoItem(
|
return TodoItem(
|
||||||
uid=uid,
|
uid=uid,
|
||||||
summary=summary,
|
summary=summary,
|
||||||
@@ -85,28 +78,9 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
|||||||
get_attr_value(todo, "status") or "",
|
get_attr_value(todo, "status") or "",
|
||||||
TodoItemStatus.NEEDS_ACTION,
|
TodoItemStatus.NEEDS_ACTION,
|
||||||
),
|
),
|
||||||
due=due,
|
|
||||||
description=get_attr_value(todo, "description"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _to_ics_fields(item: TodoItem) -> dict[str, Any]:
|
|
||||||
"""Convert a TodoItem to the set of add or update arguments."""
|
|
||||||
item_data: dict[str, Any] = {}
|
|
||||||
if summary := item.summary:
|
|
||||||
item_data["summary"] = summary
|
|
||||||
if status := item.status:
|
|
||||||
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
|
||||||
if due := item.due:
|
|
||||||
if isinstance(due, datetime):
|
|
||||||
item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ")
|
|
||||||
else:
|
|
||||||
item_data["due"] = due.strftime("%Y%m%d")
|
|
||||||
if description := item.description:
|
|
||||||
item_data["description"] = description
|
|
||||||
return item_data
|
|
||||||
|
|
||||||
|
|
||||||
class WebDavTodoListEntity(TodoListEntity):
|
class WebDavTodoListEntity(TodoListEntity):
|
||||||
"""CalDAV To-do list entity."""
|
"""CalDAV To-do list entity."""
|
||||||
|
|
||||||
@@ -115,9 +89,6 @@ class WebDavTodoListEntity(TodoListEntity):
|
|||||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
|
||||||
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
|
||||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
||||||
@@ -145,7 +116,13 @@ class WebDavTodoListEntity(TodoListEntity):
|
|||||||
"""Add an item to the To-do list."""
|
"""Add an item to the To-do list."""
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
partial(self._calendar.save_todo, **_to_ics_fields(item)),
|
partial(
|
||||||
|
self._calendar.save_todo,
|
||||||
|
summary=item.summary,
|
||||||
|
status=TODO_STATUS_MAP_INV.get(
|
||||||
|
item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION"
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except (requests.ConnectionError, DAVError) as err:
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||||
@@ -162,7 +139,10 @@ class WebDavTodoListEntity(TodoListEntity):
|
|||||||
except (requests.ConnectionError, DAVError) as err:
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||||
vtodo.update(**_to_ics_fields(item))
|
if item.summary:
|
||||||
|
vtodo["summary"] = item.summary
|
||||||
|
if item.status:
|
||||||
|
vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION")
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
partial(
|
partial(
|
||||||
|
@@ -73,7 +73,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get_events": {
|
"get_events": {
|
||||||
"name": "Get events",
|
"name": "Get event",
|
||||||
"description": "Get events on a calendar within a time range.",
|
"description": "Get events on a calendar within a time range.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"start_date_time": {
|
"start_date_time": {
|
||||||
|
@@ -68,13 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator):
|
|||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
"""Update device data."""
|
"""Update device data."""
|
||||||
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
|
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.api.login()
|
await self.api.login()
|
||||||
return await self.api.get_all_devices()
|
|
||||||
except exceptions.CannotConnect as err:
|
except exceptions.CannotConnect as err:
|
||||||
_LOGGER.warning("Connection error for %s", self._host)
|
_LOGGER.warning("Connection error for %s", self._host)
|
||||||
await self.api.close()
|
await self.api.close()
|
||||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||||
except exceptions.CannotAuthenticate:
|
except exceptions.CannotAuthenticate:
|
||||||
raise ConfigEntryAuthFailed
|
raise ConfigEntryAuthFailed
|
||||||
|
|
||||||
|
return await self.api.get_all_devices()
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"requirements": ["aiocomelit==0.6.2"]
|
"requirements": ["aiocomelit==0.5.2"]
|
||||||
}
|
}
|
||||||
|
@@ -13,9 +13,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"pin": "[%key:common::config_flow::data::pin%]"
|
"pin": "[%key:common::config_flow::data::pin%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Comelit device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -649,7 +649,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
if device_area is None:
|
if device_area is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return {"area": device_area.id}
|
return {"area": device_area.name}
|
||||||
|
|
||||||
def _get_error_text(
|
def _get_error_text(
|
||||||
self, response_type: ResponseType, lang_intents: LanguageIntents | None
|
self, response_type: ResponseType, lang_intents: LanguageIntents | None
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.12.05"]
|
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.17"]
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Set up your CoolMasterNet connection details.",
|
"title": "Set up your CoolMasterNet connection details.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"off": "Can be turned off",
|
"off": "Can be turned off",
|
||||||
@@ -12,9 +12,6 @@
|
|||||||
"dry": "Support dry mode",
|
"dry": "Support dry mode",
|
||||||
"fan_only": "Support fan only mode",
|
"fan_only": "Support fan only mode",
|
||||||
"swing_support": "Control swing mode"
|
"swing_support": "Control swing mode"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your CoolMasterNet device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -67,7 +67,7 @@ DECONZ_TO_COLOR_MODE = {
|
|||||||
LightColorMode.XY: ColorMode.XY,
|
LightColorMode.XY: ColorMode.XY,
|
||||||
}
|
}
|
||||||
|
|
||||||
XMAS_LIGHT_EFFECTS = [
|
TS0601_EFFECTS = [
|
||||||
"carnival",
|
"carnival",
|
||||||
"collide",
|
"collide",
|
||||||
"fading",
|
"fading",
|
||||||
@@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
|
|||||||
if device.effect is not None:
|
if device.effect is not None:
|
||||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||||
self._attr_effect_list = [EFFECT_COLORLOOP]
|
self._attr_effect_list = [EFFECT_COLORLOOP]
|
||||||
if device.model_id in ("HG06467", "TS0601"):
|
if device.model_id == "TS0601":
|
||||||
self._attr_effect_list = XMAS_LIGHT_EFFECTS
|
self._attr_effect_list += TS0601_EFFECTS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_mode(self) -> str | None:
|
def color_mode(self) -> str | None:
|
||||||
|
@@ -11,14 +11,11 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your deCONZ host."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
"title": "Link with deCONZ",
|
"title": "Link with deCONZ",
|
||||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings > Gateway > Advanced\n2. Press \"Authenticate app\" button"
|
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
|
||||||
},
|
},
|
||||||
"hassio_confirm": {
|
"hassio_confirm": {
|
||||||
"title": "deCONZ Zigbee gateway via Home Assistant add-on",
|
"title": "deCONZ Zigbee gateway via Home Assistant add-on",
|
||||||
|
@@ -9,9 +9,6 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"web_port": "Web port (for visiting service)"
|
"web_port": "Web port (for visiting service)"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Deluge device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -161,9 +161,12 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
|
|||||||
|
|
||||||
def set_preset_mode(self, preset_mode: str) -> None:
|
def set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
self._preset_mode = preset_mode
|
if self.preset_modes and preset_mode in self.preset_modes:
|
||||||
self._percentage = None
|
self._preset_mode = preset_mode
|
||||||
self.schedule_update_ha_state()
|
self._percentage = None
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||||
|
|
||||||
def turn_on(
|
def turn_on(
|
||||||
self,
|
self,
|
||||||
@@ -227,6 +230,10 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
|
if self.preset_modes is None or preset_mode not in self.preset_modes:
|
||||||
|
raise ValueError(
|
||||||
|
f"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
|
||||||
|
)
|
||||||
self._preset_mode = preset_mode
|
self._preset_mode = preset_mode
|
||||||
self._percentage = None
|
self._percentage = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
"""The Devialet integration."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from devialet import DevialetApi
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Set up Devialet from a config entry."""
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = DevialetApi(
|
|
||||||
entry.data[CONF_HOST], session
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Unload Devialet config entry."""
|
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
|
||||||
del hass.data[DOMAIN][entry.entry_id]
|
|
||||||
return unload_ok
|
|
@@ -1,104 +0,0 @@
|
|||||||
"""Support for Devialet Phantom speakers."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from devialet.devialet_api import DevialetApi
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
|
||||||
|
|
||||||
|
|
||||||
class DevialetFlowHandler(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Config flow for Devialet."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize flow."""
|
|
||||||
self._host: str | None = None
|
|
||||||
self._name: str | None = None
|
|
||||||
self._model: str | None = None
|
|
||||||
self._serial: str | None = None
|
|
||||||
self._errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
async def async_validate_input(self) -> FlowResult | None:
|
|
||||||
"""Validate the input using the Devialet API."""
|
|
||||||
|
|
||||||
self._errors.clear()
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
client = DevialetApi(self._host, session)
|
|
||||||
|
|
||||||
if not await client.async_update() or client.serial is None:
|
|
||||||
self._errors["base"] = "cannot_connect"
|
|
||||||
LOGGER.error("Cannot connect")
|
|
||||||
return None
|
|
||||||
|
|
||||||
await self.async_set_unique_id(client.serial)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=client.device_name,
|
|
||||||
data={CONF_HOST: self._host, CONF_NAME: client.device_name},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle a flow initialized by the user or zeroconf."""
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
self._host = user_input[CONF_HOST]
|
|
||||||
result = await self.async_validate_input()
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
|
||||||
errors=self._errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
|
||||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle a flow initialized by zeroconf discovery."""
|
|
||||||
LOGGER.info("Devialet device found via ZEROCONF: %s", discovery_info)
|
|
||||||
|
|
||||||
self._host = discovery_info.host
|
|
||||||
self._name = discovery_info.name.split(".", 1)[0]
|
|
||||||
self._model = discovery_info.properties["model"]
|
|
||||||
self._serial = discovery_info.properties["serialNumber"]
|
|
||||||
|
|
||||||
await self.async_set_unique_id(self._serial)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
self.context["title_placeholders"] = {"title": self._name}
|
|
||||||
return await self.async_step_confirm()
|
|
||||||
|
|
||||||
async def async_step_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle user-confirmation of discovered node."""
|
|
||||||
title = f"{self._name} ({self._model})"
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
result = await self.async_validate_input()
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="confirm",
|
|
||||||
description_placeholders={"device": self._model, "title": title},
|
|
||||||
errors=self._errors,
|
|
||||||
last_step=True,
|
|
||||||
)
|
|
@@ -1,12 +0,0 @@
|
|||||||
"""Constants for the Devialet integration."""
|
|
||||||
from typing import Final
|
|
||||||
|
|
||||||
DOMAIN: Final = "devialet"
|
|
||||||
MANUFACTURER: Final = "Devialet"
|
|
||||||
|
|
||||||
SOUND_MODES = {
|
|
||||||
"Custom": "custom",
|
|
||||||
"Flat": "flat",
|
|
||||||
"Night mode": "night mode",
|
|
||||||
"Voice": "voice",
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
"""Class representing a Devialet update coordinator."""
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from devialet import DevialetApi
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=5)
|
|
||||||
|
|
||||||
|
|
||||||
class DevialetCoordinator(DataUpdateCoordinator[None]):
|
|
||||||
"""Devialet update coordinator."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: DevialetApi) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
)
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
|
||||||
"""Fetch data from API endpoint."""
|
|
||||||
await self.client.async_update()
|
|
@@ -1,20 +0,0 @@
|
|||||||
"""Diagnostics support for Devialet."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from devialet import DevialetApi
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
|
||||||
hass: HomeAssistant, entry: ConfigEntry
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return diagnostics for a config entry."""
|
|
||||||
client: DevialetApi = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
|
|
||||||
return await client.async_get_diagnostics()
|
|
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "devialet",
|
|
||||||
"name": "Devialet",
|
|
||||||
"after_dependencies": ["zeroconf"],
|
|
||||||
"codeowners": ["@fwestenberg"],
|
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/devialet",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
|
||||||
"requirements": ["devialet==1.4.3"],
|
|
||||||
"zeroconf": ["_devialet-http._tcp.local."]
|
|
||||||
}
|
|
@@ -1,212 +0,0 @@
|
|||||||
"""Support for Devialet speakers."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from devialet.const import NORMAL_INPUTS
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
|
||||||
MediaPlayerEntity,
|
|
||||||
MediaPlayerEntityFeature,
|
|
||||||
MediaPlayerState,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_NAME
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER, SOUND_MODES
|
|
||||||
from .coordinator import DevialetCoordinator
|
|
||||||
|
|
||||||
SUPPORT_DEVIALET = (
|
|
||||||
MediaPlayerEntityFeature.VOLUME_SET
|
|
||||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
|
||||||
| MediaPlayerEntityFeature.TURN_OFF
|
|
||||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
|
||||||
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
|
||||||
)
|
|
||||||
|
|
||||||
DEVIALET_TO_HA_FEATURE_MAP = {
|
|
||||||
"play": MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP,
|
|
||||||
"pause": MediaPlayerEntityFeature.PAUSE,
|
|
||||||
"previous": MediaPlayerEntityFeature.PREVIOUS_TRACK,
|
|
||||||
"next": MediaPlayerEntityFeature.NEXT_TRACK,
|
|
||||||
"seek": MediaPlayerEntityFeature.SEEK,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Devialet entry."""
|
|
||||||
client = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
coordinator = DevialetCoordinator(hass, client)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
|
|
||||||
async_add_entities([DevialetMediaPlayerEntity(coordinator, entry)])
|
|
||||||
|
|
||||||
|
|
||||||
class DevialetMediaPlayerEntity(
|
|
||||||
CoordinatorEntity[DevialetCoordinator], MediaPlayerEntity
|
|
||||||
):
|
|
||||||
"""Devialet media player."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_name = None
|
|
||||||
|
|
||||||
def __init__(self, coordinator: DevialetCoordinator, entry: ConfigEntry) -> None:
|
|
||||||
"""Initialize the Devialet device."""
|
|
||||||
self.coordinator = coordinator
|
|
||||||
super().__init__(coordinator)
|
|
||||||
|
|
||||||
self._attr_unique_id = str(entry.unique_id)
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
|
||||||
manufacturer=MANUFACTURER,
|
|
||||||
model=self.coordinator.client.model,
|
|
||||||
name=entry.data[CONF_NAME],
|
|
||||||
sw_version=self.coordinator.client.version,
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
|
||||||
"""Handle updated data from the coordinator."""
|
|
||||||
if not self.coordinator.client.is_available:
|
|
||||||
self.async_write_ha_state()
|
|
||||||
return
|
|
||||||
|
|
||||||
self._attr_volume_level = self.coordinator.client.volume_level
|
|
||||||
self._attr_is_volume_muted = self.coordinator.client.is_volume_muted
|
|
||||||
self._attr_source_list = self.coordinator.client.source_list
|
|
||||||
self._attr_sound_mode_list = sorted(SOUND_MODES)
|
|
||||||
self._attr_media_artist = self.coordinator.client.media_artist
|
|
||||||
self._attr_media_album_name = self.coordinator.client.media_album_name
|
|
||||||
self._attr_media_artist = self.coordinator.client.media_artist
|
|
||||||
self._attr_media_image_url = self.coordinator.client.media_image_url
|
|
||||||
self._attr_media_duration = self.coordinator.client.media_duration
|
|
||||||
self._attr_media_position = self.coordinator.client.current_position
|
|
||||||
self._attr_media_position_updated_at = (
|
|
||||||
self.coordinator.client.position_updated_at
|
|
||||||
)
|
|
||||||
self._attr_media_title = (
|
|
||||||
self.coordinator.client.media_title
|
|
||||||
if self.coordinator.client.media_title
|
|
||||||
else self.source
|
|
||||||
)
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> MediaPlayerState | None:
|
|
||||||
"""Return the state of the device."""
|
|
||||||
playing_state = self.coordinator.client.playing_state
|
|
||||||
|
|
||||||
if not playing_state:
|
|
||||||
return MediaPlayerState.IDLE
|
|
||||||
if playing_state == "playing":
|
|
||||||
return MediaPlayerState.PLAYING
|
|
||||||
if playing_state == "paused":
|
|
||||||
return MediaPlayerState.PAUSED
|
|
||||||
return MediaPlayerState.ON
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if the media player is available."""
|
|
||||||
return self.coordinator.client.is_available
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
|
||||||
"""Flag media player features that are supported."""
|
|
||||||
features = SUPPORT_DEVIALET
|
|
||||||
|
|
||||||
if self.coordinator.client.source_state is None:
|
|
||||||
return features
|
|
||||||
|
|
||||||
if not self.coordinator.client.available_options:
|
|
||||||
return features
|
|
||||||
|
|
||||||
for option in self.coordinator.client.available_options:
|
|
||||||
features |= DEVIALET_TO_HA_FEATURE_MAP.get(option, 0)
|
|
||||||
return features
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self) -> str | None:
|
|
||||||
"""Return the current input source."""
|
|
||||||
source = self.coordinator.client.source
|
|
||||||
|
|
||||||
for pretty_name, name in NORMAL_INPUTS.items():
|
|
||||||
if source == name:
|
|
||||||
return pretty_name
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sound_mode(self) -> str | None:
|
|
||||||
"""Return the current sound mode."""
|
|
||||||
if self.coordinator.client.equalizer is not None:
|
|
||||||
sound_mode = self.coordinator.client.equalizer
|
|
||||||
elif self.coordinator.client.night_mode:
|
|
||||||
sound_mode = "night mode"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for pretty_name, mode in SOUND_MODES.items():
|
|
||||||
if sound_mode == mode:
|
|
||||||
return pretty_name
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
|
||||||
"""Volume up media player."""
|
|
||||||
await self.coordinator.client.async_volume_up()
|
|
||||||
|
|
||||||
async def async_volume_down(self) -> None:
|
|
||||||
"""Volume down media player."""
|
|
||||||
await self.coordinator.client.async_volume_down()
|
|
||||||
|
|
||||||
async def async_set_volume_level(self, volume: float) -> None:
|
|
||||||
"""Set volume level, range 0..1."""
|
|
||||||
await self.coordinator.client.async_set_volume_level(volume)
|
|
||||||
|
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
|
||||||
"""Mute (true) or unmute (false) media player."""
|
|
||||||
await self.coordinator.client.async_mute_volume(mute)
|
|
||||||
|
|
||||||
async def async_media_play(self) -> None:
|
|
||||||
"""Play media player."""
|
|
||||||
await self.coordinator.client.async_media_play()
|
|
||||||
|
|
||||||
async def async_media_pause(self) -> None:
|
|
||||||
"""Pause media player."""
|
|
||||||
await self.coordinator.client.async_media_pause()
|
|
||||||
|
|
||||||
async def async_media_stop(self) -> None:
|
|
||||||
"""Pause media player."""
|
|
||||||
await self.coordinator.client.async_media_stop()
|
|
||||||
|
|
||||||
async def async_media_next_track(self) -> None:
|
|
||||||
"""Send the next track command."""
|
|
||||||
await self.coordinator.client.async_media_next_track()
|
|
||||||
|
|
||||||
async def async_media_previous_track(self) -> None:
|
|
||||||
"""Send the previous track command."""
|
|
||||||
await self.coordinator.client.async_media_previous_track()
|
|
||||||
|
|
||||||
async def async_media_seek(self, position: float) -> None:
|
|
||||||
"""Send seek command."""
|
|
||||||
await self.coordinator.client.async_media_seek(position)
|
|
||||||
|
|
||||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
|
||||||
"""Send sound mode command."""
|
|
||||||
for pretty_name, mode in SOUND_MODES.items():
|
|
||||||
if sound_mode == pretty_name:
|
|
||||||
if mode == "night mode":
|
|
||||||
await self.coordinator.client.async_set_night_mode(True)
|
|
||||||
else:
|
|
||||||
await self.coordinator.client.async_set_night_mode(False)
|
|
||||||
await self.coordinator.client.async_set_equalizer(mode)
|
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
|
||||||
"""Turn off media player."""
|
|
||||||
await self.coordinator.client.async_turn_off()
|
|
||||||
|
|
||||||
async def async_select_source(self, source: str) -> None:
|
|
||||||
"""Select input source."""
|
|
||||||
await self.coordinator.client.async_select_source(source)
|
|
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"flow_title": "{title}",
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"description": "Please enter the host name or IP address of the Devialet device.",
|
|
||||||
"data": {
|
|
||||||
"host": "Host"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"confirm": {
|
|
||||||
"description": "Do you want to set up Devialet device {device}?"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "Service is already configured"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "Failed to connect"
|
|
||||||
},
|
|
||||||
"flow_title": "{title}",
|
|
||||||
"step": {
|
|
||||||
"confirm": {
|
|
||||||
"description": "Do you want to set up Devialet device {device}?"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"host": "Host"
|
|
||||||
},
|
|
||||||
"description": "Please enter the host name or IP address of the Devialet device."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1033,19 +1033,6 @@ def update_config(path: str, dev_id: str, device: Device) -> None:
|
|||||||
out.write(dump(device_config))
|
out.write(dump(device_config))
|
||||||
|
|
||||||
|
|
||||||
def remove_device_from_config(hass: HomeAssistant, device_id: str) -> None:
|
|
||||||
"""Remove device from YAML configuration file."""
|
|
||||||
path = hass.config.path(YAML_DEVICES)
|
|
||||||
devices = load_yaml_config_file(path)
|
|
||||||
devices.pop(device_id)
|
|
||||||
dumped = dump(devices)
|
|
||||||
|
|
||||||
with open(path, "r+", encoding="utf8") as out:
|
|
||||||
out.seek(0)
|
|
||||||
out.truncate()
|
|
||||||
out.write(dumped)
|
|
||||||
|
|
||||||
|
|
||||||
def get_gravatar_for_email(email: str) -> str:
|
def get_gravatar_for_email(email: str) -> str:
|
||||||
"""Return an 80px Gravatar for the given email address.
|
"""Return an 80px Gravatar for the given email address.
|
||||||
|
|
||||||
|
@@ -8,9 +8,6 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your DirectTV device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -183,7 +183,6 @@ async def async_setup_entry(
|
|||||||
for description in sensors
|
for description in sensors
|
||||||
for value_key in {description.key, *description.alternative_keys}
|
for value_key in {description.key, *description.alternative_keys}
|
||||||
if description.value_fn(coordinator.data, value_key, description.scale)
|
if description.value_fn(coordinator.data, value_key, description.scale)
|
||||||
is not None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
@@ -9,7 +9,6 @@
|
|||||||
"use_legacy_protocol": "Use legacy protocol"
|
"use_legacy_protocol": "Use legacy protocol"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your D-Link device",
|
|
||||||
"password": "Default: PIN code on the back."
|
"password": "Default: PIN code on the back."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -17,11 +17,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"name": "Device name",
|
"name": "Device Name",
|
||||||
"username": "[%key:common::config_flow::data::username%]"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your DoorBird device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -4,9 +4,6 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Dremel 3D printer."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -12,6 +12,7 @@ LOGGER = logging.getLogger(__package__)
|
|||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
CONF_DSMR_VERSION = "dsmr_version"
|
CONF_DSMR_VERSION = "dsmr_version"
|
||||||
CONF_PROTOCOL = "protocol"
|
CONF_PROTOCOL = "protocol"
|
||||||
|
CONF_RECONNECT_INTERVAL = "reconnect_interval"
|
||||||
CONF_PRECISION = "precision"
|
CONF_PRECISION = "precision"
|
||||||
CONF_TIME_BETWEEN_UPDATE = "time_between_update"
|
CONF_TIME_BETWEEN_UPDATE = "time_between_update"
|
||||||
|
|
||||||
@@ -28,7 +29,6 @@ DATA_TASK = "task"
|
|||||||
|
|
||||||
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
|
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
|
||||||
DEVICE_NAME_GAS = "Gas Meter"
|
DEVICE_NAME_GAS = "Gas Meter"
|
||||||
DEVICE_NAME_WATER = "Water Meter"
|
|
||||||
|
|
||||||
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
|
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
|
||||||
|
|
||||||
|
@@ -34,7 +34,6 @@ from homeassistant.const import (
|
|||||||
UnitOfVolume,
|
UnitOfVolume,
|
||||||
)
|
)
|
||||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect,
|
async_dispatcher_connect,
|
||||||
@@ -48,6 +47,7 @@ from .const import (
|
|||||||
CONF_DSMR_VERSION,
|
CONF_DSMR_VERSION,
|
||||||
CONF_PRECISION,
|
CONF_PRECISION,
|
||||||
CONF_PROTOCOL,
|
CONF_PROTOCOL,
|
||||||
|
CONF_RECONNECT_INTERVAL,
|
||||||
CONF_SERIAL_ID,
|
CONF_SERIAL_ID,
|
||||||
CONF_SERIAL_ID_GAS,
|
CONF_SERIAL_ID_GAS,
|
||||||
CONF_TIME_BETWEEN_UPDATE,
|
CONF_TIME_BETWEEN_UPDATE,
|
||||||
@@ -57,7 +57,6 @@ from .const import (
|
|||||||
DEFAULT_TIME_BETWEEN_UPDATE,
|
DEFAULT_TIME_BETWEEN_UPDATE,
|
||||||
DEVICE_NAME_ELECTRICITY,
|
DEVICE_NAME_ELECTRICITY,
|
||||||
DEVICE_NAME_GAS,
|
DEVICE_NAME_GAS,
|
||||||
DEVICE_NAME_WATER,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DSMR_PROTOCOL,
|
DSMR_PROTOCOL,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
@@ -74,7 +73,6 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
|
|||||||
|
|
||||||
dsmr_versions: set[str] | None = None
|
dsmr_versions: set[str] | None = None
|
||||||
is_gas: bool = False
|
is_gas: bool = False
|
||||||
is_water: bool = False
|
|
||||||
obis_reference: str
|
obis_reference: str
|
||||||
|
|
||||||
|
|
||||||
@@ -376,138 +374,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_mbus_entity(
|
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
|
||||||
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
|
"""Return correct entity for 5B Gas meter."""
|
||||||
) -> DSMRSensorEntityDescription | None:
|
ref = None
|
||||||
"""Create a new MBUS Entity."""
|
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
|
||||||
if (
|
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||||
mtype == 3
|
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
|
||||||
and (
|
ref = obis_references.BELGIUM_MBUS2_METER_READING2
|
||||||
obis_reference := getattr(
|
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
|
||||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2"
|
ref = obis_references.BELGIUM_MBUS3_METER_READING2
|
||||||
)
|
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
|
||||||
)
|
ref = obis_references.BELGIUM_MBUS4_METER_READING2
|
||||||
in telegram
|
elif ref is None:
|
||||||
):
|
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||||
return DSMRSensorEntityDescription(
|
return DSMRSensorEntityDescription(
|
||||||
key=f"mbus{mbus}_gas_reading",
|
key="belgium_5min_gas_meter_reading",
|
||||||
translation_key="gas_meter_reading",
|
translation_key="gas_meter_reading",
|
||||||
obis_reference=obis_reference,
|
obis_reference=ref,
|
||||||
is_gas=True,
|
dsmr_versions={"5B"},
|
||||||
device_class=SensorDeviceClass.GAS,
|
is_gas=True,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
device_class=SensorDeviceClass.GAS,
|
||||||
)
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
if (
|
)
|
||||||
mtype == 7
|
|
||||||
and (
|
|
||||||
obis_reference := getattr(
|
|
||||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
in telegram
|
|
||||||
):
|
|
||||||
return DSMRSensorEntityDescription(
|
|
||||||
key=f"mbus{mbus}_water_reading",
|
|
||||||
translation_key="water_meter_reading",
|
|
||||||
obis_reference=obis_reference,
|
|
||||||
is_water=True,
|
|
||||||
device_class=SensorDeviceClass.WATER,
|
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def device_class_and_uom(
|
|
||||||
telegram: dict[str, DSMRObject],
|
|
||||||
entity_description: DSMRSensorEntityDescription,
|
|
||||||
) -> tuple[SensorDeviceClass | None, str | None]:
|
|
||||||
"""Get native unit of measurement from telegram,."""
|
|
||||||
dsmr_object = telegram[entity_description.obis_reference]
|
|
||||||
uom: str | None = getattr(dsmr_object, "unit") or None
|
|
||||||
with suppress(ValueError):
|
|
||||||
if entity_description.device_class == SensorDeviceClass.GAS and (
|
|
||||||
enery_uom := UnitOfEnergy(str(uom))
|
|
||||||
):
|
|
||||||
return (SensorDeviceClass.ENERGY, enery_uom)
|
|
||||||
if uom in UNIT_CONVERSION:
|
|
||||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
|
||||||
return (entity_description.device_class, uom)
|
|
||||||
|
|
||||||
|
|
||||||
def rename_old_gas_to_mbus(
|
|
||||||
hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
|
|
||||||
) -> None:
|
|
||||||
"""Rename old gas sensor to mbus variant."""
|
|
||||||
dev_reg = dr.async_get(hass)
|
|
||||||
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
|
||||||
if device_entry_v1 is not None:
|
|
||||||
device_id = device_entry_v1.id
|
|
||||||
|
|
||||||
ent_reg = er.async_get(hass)
|
|
||||||
entries = er.async_entries_for_device(ent_reg, device_id)
|
|
||||||
|
|
||||||
for entity in entries:
|
|
||||||
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
|
|
||||||
try:
|
|
||||||
ent_reg.async_update_entity(
|
|
||||||
entity.entity_id,
|
|
||||||
new_unique_id=mbus_device_id,
|
|
||||||
device_id=mbus_device_id,
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
LOGGER.debug(
|
|
||||||
"Skip migration of %s because it already exists",
|
|
||||||
entity.entity_id,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(
|
|
||||||
"Migrated entity %s from unique id %s to %s",
|
|
||||||
entity.entity_id,
|
|
||||||
entity.unique_id,
|
|
||||||
mbus_device_id,
|
|
||||||
)
|
|
||||||
# Cleanup old device
|
|
||||||
dev_entities = er.async_entries_for_device(
|
|
||||||
ent_reg, device_id, include_disabled_entities=True
|
|
||||||
)
|
|
||||||
if not dev_entities:
|
|
||||||
dev_reg.async_remove_device(device_id)
|
|
||||||
|
|
||||||
|
|
||||||
def create_mbus_entities(
|
|
||||||
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
|
|
||||||
) -> list[DSMREntity]:
|
|
||||||
"""Create MBUS Entities."""
|
|
||||||
entities = []
|
|
||||||
for idx in range(1, 5):
|
|
||||||
if (
|
|
||||||
device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE")
|
|
||||||
) not in telegram:
|
|
||||||
continue
|
|
||||||
if (type_ := int(telegram[device_type].value)) not in (3, 7):
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
identifier := getattr(
|
|
||||||
obis_references,
|
|
||||||
f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER",
|
|
||||||
)
|
|
||||||
) in telegram:
|
|
||||||
serial_ = telegram[identifier].value
|
|
||||||
rename_old_gas_to_mbus(hass, entry, serial_)
|
|
||||||
else:
|
|
||||||
serial_ = ""
|
|
||||||
if description := create_mbus_entity(idx, type_, telegram):
|
|
||||||
entities.append(
|
|
||||||
DSMREntity(
|
|
||||||
description,
|
|
||||||
entry,
|
|
||||||
telegram,
|
|
||||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
|
||||||
serial_,
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return entities
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -527,10 +415,25 @@ async def async_setup_entry(
|
|||||||
add_entities_handler()
|
add_entities_handler()
|
||||||
add_entities_handler = None
|
add_entities_handler = None
|
||||||
|
|
||||||
|
def device_class_and_uom(
|
||||||
|
telegram: dict[str, DSMRObject],
|
||||||
|
entity_description: DSMRSensorEntityDescription,
|
||||||
|
) -> tuple[SensorDeviceClass | None, str | None]:
|
||||||
|
"""Get native unit of measurement from telegram,."""
|
||||||
|
dsmr_object = telegram[entity_description.obis_reference]
|
||||||
|
uom: str | None = getattr(dsmr_object, "unit") or None
|
||||||
|
with suppress(ValueError):
|
||||||
|
if entity_description.device_class == SensorDeviceClass.GAS and (
|
||||||
|
enery_uom := UnitOfEnergy(str(uom))
|
||||||
|
):
|
||||||
|
return (SensorDeviceClass.ENERGY, enery_uom)
|
||||||
|
if uom in UNIT_CONVERSION:
|
||||||
|
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||||
|
return (entity_description.device_class, uom)
|
||||||
|
|
||||||
|
all_sensors = SENSORS
|
||||||
if dsmr_version == "5B":
|
if dsmr_version == "5B":
|
||||||
mbus_entities = create_mbus_entities(hass, telegram, entry)
|
all_sensors += (add_gas_sensor_5B(telegram),)
|
||||||
for mbus_entity in mbus_entities:
|
|
||||||
entities.append(mbus_entity)
|
|
||||||
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
@@ -540,7 +443,7 @@ async def async_setup_entry(
|
|||||||
telegram,
|
telegram,
|
||||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
for description in SENSORS
|
for description in all_sensors
|
||||||
if (
|
if (
|
||||||
description.dsmr_versions is None
|
description.dsmr_versions is None
|
||||||
or dsmr_version in description.dsmr_versions
|
or dsmr_version in description.dsmr_versions
|
||||||
@@ -646,7 +549,9 @@ async def async_setup_entry(
|
|||||||
update_entities_telegram(None)
|
update_entities_telegram(None)
|
||||||
|
|
||||||
# throttle reconnect attempts
|
# throttle reconnect attempts
|
||||||
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
|
await asyncio.sleep(
|
||||||
|
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
|
||||||
|
)
|
||||||
|
|
||||||
except (serial.serialutil.SerialException, OSError):
|
except (serial.serialutil.SerialException, OSError):
|
||||||
# Log any error while establishing connection and drop to retry
|
# Log any error while establishing connection and drop to retry
|
||||||
@@ -660,7 +565,9 @@ async def async_setup_entry(
|
|||||||
update_entities_telegram(None)
|
update_entities_telegram(None)
|
||||||
|
|
||||||
# throttle reconnect attempts
|
# throttle reconnect attempts
|
||||||
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
|
await asyncio.sleep(
|
||||||
|
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
|
||||||
|
)
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
# Reflect disconnect state in devices state by setting an
|
# Reflect disconnect state in devices state by setting an
|
||||||
# None telegram resulting in `unavailable` states
|
# None telegram resulting in `unavailable` states
|
||||||
@@ -711,8 +618,6 @@ class DSMREntity(SensorEntity):
|
|||||||
telegram: dict[str, DSMRObject],
|
telegram: dict[str, DSMRObject],
|
||||||
device_class: SensorDeviceClass,
|
device_class: SensorDeviceClass,
|
||||||
native_unit_of_measurement: str | None,
|
native_unit_of_measurement: str | None,
|
||||||
serial_id: str = "",
|
|
||||||
mbus_id: int = 0,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize entity."""
|
"""Initialize entity."""
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
@@ -724,15 +629,8 @@ class DSMREntity(SensorEntity):
|
|||||||
device_serial = entry.data[CONF_SERIAL_ID]
|
device_serial = entry.data[CONF_SERIAL_ID]
|
||||||
device_name = DEVICE_NAME_ELECTRICITY
|
device_name = DEVICE_NAME_ELECTRICITY
|
||||||
if entity_description.is_gas:
|
if entity_description.is_gas:
|
||||||
if serial_id:
|
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
||||||
device_serial = serial_id
|
|
||||||
else:
|
|
||||||
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
|
||||||
device_name = DEVICE_NAME_GAS
|
device_name = DEVICE_NAME_GAS
|
||||||
if entity_description.is_water:
|
|
||||||
if serial_id:
|
|
||||||
device_serial = serial_id
|
|
||||||
device_name = DEVICE_NAME_WATER
|
|
||||||
if device_serial is None:
|
if device_serial is None:
|
||||||
device_serial = entry.entry_id
|
device_serial = entry.entry_id
|
||||||
|
|
||||||
@@ -740,13 +638,7 @@ class DSMREntity(SensorEntity):
|
|||||||
identifiers={(DOMAIN, device_serial)},
|
identifiers={(DOMAIN, device_serial)},
|
||||||
name=device_name,
|
name=device_name,
|
||||||
)
|
)
|
||||||
if mbus_id != 0:
|
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||||
if serial_id:
|
|
||||||
self._attr_unique_id = f"{device_serial}"
|
|
||||||
else:
|
|
||||||
self._attr_unique_id = f"{device_serial}_{mbus_id}"
|
|
||||||
else:
|
|
||||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
||||||
@@ -794,10 +686,6 @@ class DSMREntity(SensorEntity):
|
|||||||
float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
|
float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make sure we do not return a zero value for an energy sensor
|
|
||||||
if not value and self.state_class == SensorStateClass.TOTAL_INCREASING:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@@ -147,9 +147,6 @@
|
|||||||
},
|
},
|
||||||
"voltage_swell_l3_count": {
|
"voltage_swell_l3_count": {
|
||||||
"name": "Voltage swells phase L3"
|
"name": "Voltage swells phase L3"
|
||||||
},
|
|
||||||
"water_meter_reading": {
|
|
||||||
"name": "Water consumption"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -5,9 +5,6 @@
|
|||||||
"description": "Ensure that your player is turned on.",
|
"description": "Ensure that your player is turned on.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Dune HD device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Duotecno device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Ecoforest device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,9 +7,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Elgato device."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zeroconf_confirm": {
|
"zeroconf_confirm": {
|
||||||
|
@@ -5,9 +5,6 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your SiteSage Emonitor device."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
|
@@ -6,6 +6,7 @@ import logging
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.http import HomeAssistantAccessLogger
|
||||||
from homeassistant.components.network import async_get_source_ip
|
from homeassistant.components.network import async_get_source_ip
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
@@ -100,7 +101,7 @@ async def start_emulated_hue_bridge(
|
|||||||
config.advertise_port or config.listen_port,
|
config.advertise_port or config.listen_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
runner = web.AppRunner(app)
|
runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger)
|
||||||
await runner.setup()
|
await runner.setup()
|
||||||
|
|
||||||
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
|
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
|
||||||
|
@@ -317,11 +317,6 @@ class EnergyCostSensor(SensorEntity):
|
|||||||
try:
|
try:
|
||||||
energy_price = float(energy_price_state.state)
|
energy_price = float(energy_price_state.state)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if self._last_energy_sensor_state is None:
|
|
||||||
# Initialize as it's the first time all required entities except
|
|
||||||
# price are in place. This means that the cost will update the first
|
|
||||||
# time the energy is updated after the price entity is in place.
|
|
||||||
self._reset(energy_state)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||||
|
@@ -8,9 +8,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Enphase Envoy gateway."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -5,9 +5,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"name": "[%key:common::config_flow::data::name%]"
|
"name": "[%key:common::config_flow::data::name%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Epson projector."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -22,7 +22,6 @@ from aioesphomeapi import (
|
|||||||
APIClient,
|
APIClient,
|
||||||
APIVersion,
|
APIVersion,
|
||||||
BLEConnectionError,
|
BLEConnectionError,
|
||||||
BluetoothConnectionDroppedError,
|
|
||||||
BluetoothProxyFeature,
|
BluetoothProxyFeature,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
)
|
)
|
||||||
@@ -31,6 +30,7 @@ from aioesphomeapi.core import (
|
|||||||
BluetoothGATTAPIError,
|
BluetoothGATTAPIError,
|
||||||
TimeoutAPIError,
|
TimeoutAPIError,
|
||||||
)
|
)
|
||||||
|
from async_interrupt import interrupt
|
||||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||||
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
@@ -68,25 +68,39 @@ def mac_to_int(address: str) -> int:
|
|||||||
return int(address.replace(":", ""), 16)
|
return int(address.replace(":", ""), 16)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
|
||||||
|
"""Define a wrapper throw BleakError if not connected."""
|
||||||
|
|
||||||
|
async def _async_wrap_bluetooth_connected_operation(
|
||||||
|
self: ESPHomeClient, *args: Any, **kwargs: Any
|
||||||
|
) -> Any:
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
if not self._is_connected:
|
||||||
|
raise BleakError(f"{self._description} is not connected")
|
||||||
|
loop = self._loop
|
||||||
|
disconnected_futures = self._disconnected_futures
|
||||||
|
disconnected_future = loop.create_future()
|
||||||
|
disconnected_futures.add(disconnected_future)
|
||||||
|
disconnect_message = f"{self._description}: Disconnected during operation"
|
||||||
|
try:
|
||||||
|
async with interrupt(disconnected_future, BleakError, disconnect_message):
|
||||||
|
return await func(self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
disconnected_futures.discard(disconnected_future)
|
||||||
|
|
||||||
|
return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation)
|
||||||
|
|
||||||
|
|
||||||
def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
||||||
"""Define a wrapper throw esphome api errors as BleakErrors."""
|
"""Define a wrapper throw esphome api errors as BleakErrors."""
|
||||||
|
|
||||||
async def _async_wrap_bluetooth_operation(
|
async def _async_wrap_bluetooth_operation(
|
||||||
self: ESPHomeClient, *args: Any, **kwargs: Any
|
self: ESPHomeClient, *args: Any, **kwargs: Any
|
||||||
) -> Any:
|
) -> Any:
|
||||||
# pylint: disable=protected-access
|
|
||||||
try:
|
try:
|
||||||
return await func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
except TimeoutAPIError as err:
|
except TimeoutAPIError as err:
|
||||||
raise asyncio.TimeoutError(str(err)) from err
|
raise asyncio.TimeoutError(str(err)) from err
|
||||||
except BluetoothConnectionDroppedError as ex:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s: BLE device disconnected during %s operation",
|
|
||||||
self._description,
|
|
||||||
func.__name__,
|
|
||||||
)
|
|
||||||
self._async_ble_device_disconnected()
|
|
||||||
raise BleakError(str(ex)) from ex
|
|
||||||
except BluetoothGATTAPIError as ex:
|
except BluetoothGATTAPIError as ex:
|
||||||
# If the device disconnects in the middle of an operation
|
# If the device disconnects in the middle of an operation
|
||||||
# be sure to mark it as disconnected so any library using
|
# be sure to mark it as disconnected so any library using
|
||||||
@@ -97,6 +111,7 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
|||||||
# before the callback is delivered.
|
# before the callback is delivered.
|
||||||
|
|
||||||
if ex.error.error == -1:
|
if ex.error.error == -1:
|
||||||
|
# pylint: disable=protected-access
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s: BLE device disconnected during %s operation",
|
"%s: BLE device disconnected during %s operation",
|
||||||
self._description,
|
self._description,
|
||||||
@@ -154,6 +169,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
self._notify_cancels: dict[
|
self._notify_cancels: dict[
|
||||||
int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]]
|
int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]]
|
||||||
] = {}
|
] = {}
|
||||||
|
self._disconnected_futures: set[asyncio.Future[None]] = set()
|
||||||
self._device_info = client_data.device_info
|
self._device_info = client_data.device_info
|
||||||
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
|
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
|
||||||
client_data.api_version
|
client_data.api_version
|
||||||
@@ -169,7 +185,24 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return the string representation of the client."""
|
"""Return the string representation of the client."""
|
||||||
return f"ESPHomeClient ({self._description})"
|
return f"ESPHomeClient ({self.address})"
|
||||||
|
|
||||||
|
def _unsubscribe_connection_state(self) -> None:
|
||||||
|
"""Unsubscribe from connection state updates."""
|
||||||
|
if not self._cancel_connection_state:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._cancel_connection_state()
|
||||||
|
except (AssertionError, ValueError) as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
(
|
||||||
|
"%s: Failed to unsubscribe from connection state (likely"
|
||||||
|
" connection dropped): %s"
|
||||||
|
),
|
||||||
|
self._description,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
self._cancel_connection_state = None
|
||||||
|
|
||||||
def _async_disconnected_cleanup(self) -> None:
|
def _async_disconnected_cleanup(self) -> None:
|
||||||
"""Clean up on disconnect."""
|
"""Clean up on disconnect."""
|
||||||
@@ -178,10 +211,12 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
for _, notify_abort in self._notify_cancels.values():
|
for _, notify_abort in self._notify_cancels.values():
|
||||||
notify_abort()
|
notify_abort()
|
||||||
self._notify_cancels.clear()
|
self._notify_cancels.clear()
|
||||||
|
for future in self._disconnected_futures:
|
||||||
|
if not future.done():
|
||||||
|
future.set_result(None)
|
||||||
|
self._disconnected_futures.clear()
|
||||||
self._disconnect_callbacks.discard(self._async_esp_disconnected)
|
self._disconnect_callbacks.discard(self._async_esp_disconnected)
|
||||||
if self._cancel_connection_state:
|
self._unsubscribe_connection_state()
|
||||||
self._cancel_connection_state()
|
|
||||||
self._cancel_connection_state = None
|
|
||||||
|
|
||||||
def _async_ble_device_disconnected(self) -> None:
|
def _async_ble_device_disconnected(self) -> None:
|
||||||
"""Handle the BLE device disconnecting from the ESP."""
|
"""Handle the BLE device disconnecting from the ESP."""
|
||||||
@@ -371,6 +406,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
"""Get ATT MTU size for active connection."""
|
"""Get ATT MTU size for active connection."""
|
||||||
return self._mtu or DEFAULT_MTU
|
return self._mtu or DEFAULT_MTU
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
||||||
"""Attempt to pair."""
|
"""Attempt to pair."""
|
||||||
@@ -379,7 +415,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
"Pairing is not available in this version ESPHome; "
|
"Pairing is not available in this version ESPHome; "
|
||||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||||
)
|
)
|
||||||
self._raise_if_not_connected()
|
|
||||||
response = await self._client.bluetooth_device_pair(self._address_as_int)
|
response = await self._client.bluetooth_device_pair(self._address_as_int)
|
||||||
if response.paired:
|
if response.paired:
|
||||||
return True
|
return True
|
||||||
@@ -388,6 +423,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def unpair(self) -> bool:
|
async def unpair(self) -> bool:
|
||||||
"""Attempt to unpair."""
|
"""Attempt to unpair."""
|
||||||
@@ -396,7 +432,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
"Unpairing is not available in this version ESPHome; "
|
"Unpairing is not available in this version ESPHome; "
|
||||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||||
)
|
)
|
||||||
self._raise_if_not_connected()
|
|
||||||
response = await self._client.bluetooth_device_unpair(self._address_as_int)
|
response = await self._client.bluetooth_device_unpair(self._address_as_int)
|
||||||
if response.success:
|
if response.success:
|
||||||
return True
|
return True
|
||||||
@@ -419,6 +454,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
|
dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
async def _get_services(
|
async def _get_services(
|
||||||
self, dangerous_use_bleak_cache: bool = False, **kwargs: Any
|
self, dangerous_use_bleak_cache: bool = False, **kwargs: Any
|
||||||
) -> BleakGATTServiceCollection:
|
) -> BleakGATTServiceCollection:
|
||||||
@@ -426,7 +462,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
|
|
||||||
Must only be called from get_services or connected
|
Must only be called from get_services or connected
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
address_as_int = self._address_as_int
|
address_as_int = self._address_as_int
|
||||||
cache = self._cache
|
cache = self._cache
|
||||||
# If the connection version >= 3, we must use the cache
|
# If the connection version >= 3, we must use the cache
|
||||||
@@ -492,6 +527,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
)
|
)
|
||||||
return characteristic
|
return characteristic
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def clear_cache(self) -> bool:
|
async def clear_cache(self) -> bool:
|
||||||
"""Clear the GATT cache."""
|
"""Clear the GATT cache."""
|
||||||
@@ -505,7 +541,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
self._device_info.name,
|
self._device_info.name,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
self._raise_if_not_connected()
|
|
||||||
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
||||||
if response.success:
|
if response.success:
|
||||||
return True
|
return True
|
||||||
@@ -516,6 +551,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def read_gatt_char(
|
async def read_gatt_char(
|
||||||
self,
|
self,
|
||||||
@@ -534,12 +570,12 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
Returns:
|
Returns:
|
||||||
(bytearray) The read data.
|
(bytearray) The read data.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
characteristic = self._resolve_characteristic(char_specifier)
|
characteristic = self._resolve_characteristic(char_specifier)
|
||||||
return await self._client.bluetooth_gatt_read(
|
return await self._client.bluetooth_gatt_read(
|
||||||
self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT
|
self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray:
|
async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray:
|
||||||
"""Perform read operation on the specified GATT descriptor.
|
"""Perform read operation on the specified GATT descriptor.
|
||||||
@@ -551,11 +587,11 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
Returns:
|
Returns:
|
||||||
(bytearray) The read data.
|
(bytearray) The read data.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
return await self._client.bluetooth_gatt_read_descriptor(
|
return await self._client.bluetooth_gatt_read_descriptor(
|
||||||
self._address_as_int, handle, GATT_READ_TIMEOUT
|
self._address_as_int, handle, GATT_READ_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def write_gatt_char(
|
async def write_gatt_char(
|
||||||
self,
|
self,
|
||||||
@@ -574,12 +610,12 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
response (bool): If write-with-response operation should be done.
|
response (bool): If write-with-response operation should be done.
|
||||||
Defaults to `False`.
|
Defaults to `False`.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
characteristic = self._resolve_characteristic(characteristic)
|
characteristic = self._resolve_characteristic(characteristic)
|
||||||
await self._client.bluetooth_gatt_write(
|
await self._client.bluetooth_gatt_write(
|
||||||
self._address_as_int, characteristic.handle, bytes(data), response
|
self._address_as_int, characteristic.handle, bytes(data), response
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None:
|
async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None:
|
||||||
"""Perform a write operation on the specified GATT descriptor.
|
"""Perform a write operation on the specified GATT descriptor.
|
||||||
@@ -588,11 +624,11 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
handle (int): The handle of the descriptor to read from.
|
handle (int): The handle of the descriptor to read from.
|
||||||
data (bytes or bytearray): The data to send.
|
data (bytes or bytearray): The data to send.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
await self._client.bluetooth_gatt_write_descriptor(
|
await self._client.bluetooth_gatt_write_descriptor(
|
||||||
self._address_as_int, handle, bytes(data)
|
self._address_as_int, handle, bytes(data)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def start_notify(
|
async def start_notify(
|
||||||
self,
|
self,
|
||||||
@@ -619,7 +655,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
callback (function): The function to be called on notification.
|
callback (function): The function to be called on notification.
|
||||||
kwargs: Unused.
|
kwargs: Unused.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
ble_handle = characteristic.handle
|
ble_handle = characteristic.handle
|
||||||
if ble_handle in self._notify_cancels:
|
if ble_handle in self._notify_cancels:
|
||||||
raise BleakError(
|
raise BleakError(
|
||||||
@@ -674,6 +709,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
wait_for_response=False,
|
wait_for_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@verify_connected
|
||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def stop_notify(
|
async def stop_notify(
|
||||||
self,
|
self,
|
||||||
@@ -687,7 +723,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
specified by either integer handle, UUID or directly by the
|
specified by either integer handle, UUID or directly by the
|
||||||
BleakGATTCharacteristic object representing it.
|
BleakGATTCharacteristic object representing it.
|
||||||
"""
|
"""
|
||||||
self._raise_if_not_connected()
|
|
||||||
characteristic = self._resolve_characteristic(char_specifier)
|
characteristic = self._resolve_characteristic(char_specifier)
|
||||||
# Do not raise KeyError if notifications are not enabled on this characteristic
|
# Do not raise KeyError if notifications are not enabled on this characteristic
|
||||||
# to be consistent with the behavior of the BlueZ backend
|
# to be consistent with the behavior of the BlueZ backend
|
||||||
@@ -695,11 +730,6 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
notify_stop, _ = notify_cancel
|
notify_stop, _ = notify_cancel
|
||||||
await notify_stop()
|
await notify_stop()
|
||||||
|
|
||||||
def _raise_if_not_connected(self) -> None:
|
|
||||||
"""Raise a BleakError if not connected."""
|
|
||||||
if not self._is_connected:
|
|
||||||
raise BleakError(f"{self._description} is not connected")
|
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
"""Destructor to make sure the connection state is unsubscribed."""
|
"""Destructor to make sure the connection state is unsubscribed."""
|
||||||
if self._cancel_connection_state:
|
if self._cancel_connection_state:
|
||||||
|
@@ -164,15 +164,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
|||||||
)
|
)
|
||||||
self._attr_min_temp = static_info.visual_min_temperature
|
self._attr_min_temp = static_info.visual_min_temperature
|
||||||
self._attr_max_temp = static_info.visual_max_temperature
|
self._attr_max_temp = static_info.visual_max_temperature
|
||||||
self._attr_min_humidity = round(static_info.visual_min_humidity)
|
|
||||||
self._attr_max_humidity = round(static_info.visual_max_humidity)
|
|
||||||
features = ClimateEntityFeature(0)
|
features = ClimateEntityFeature(0)
|
||||||
if self._static_info.supports_two_point_target_temperature:
|
if self._static_info.supports_two_point_target_temperature:
|
||||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
else:
|
else:
|
||||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
if self._static_info.supports_target_humidity:
|
|
||||||
features |= ClimateEntityFeature.TARGET_HUMIDITY
|
|
||||||
if self.preset_modes:
|
if self.preset_modes:
|
||||||
features |= ClimateEntityFeature.PRESET_MODE
|
features |= ClimateEntityFeature.PRESET_MODE
|
||||||
if self.fan_modes:
|
if self.fan_modes:
|
||||||
@@ -238,14 +234,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
|||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._state.current_temperature
|
return self._state.current_temperature
|
||||||
|
|
||||||
@property
|
|
||||||
@esphome_state_property
|
|
||||||
def current_humidity(self) -> int | None:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
if not self._static_info.supports_current_humidity:
|
|
||||||
return None
|
|
||||||
return round(self._state.current_humidity)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@esphome_state_property
|
@esphome_state_property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
@@ -264,12 +252,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
|||||||
"""Return the highbound target temperature we try to reach."""
|
"""Return the highbound target temperature we try to reach."""
|
||||||
return self._state.target_temperature_high
|
return self._state.target_temperature_high
|
||||||
|
|
||||||
@property
|
|
||||||
@esphome_state_property
|
|
||||||
def target_humidity(self) -> int:
|
|
||||||
"""Return the humidity we try to reach."""
|
|
||||||
return round(self._state.target_humidity)
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature (and operation mode if set)."""
|
"""Set new target temperature (and operation mode if set)."""
|
||||||
data: dict[str, Any] = {"key": self._key}
|
data: dict[str, Any] = {"key": self._key}
|
||||||
@@ -285,10 +267,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
|||||||
data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||||
await self._client.climate_command(**data)
|
await self._client.climate_command(**data)
|
||||||
|
|
||||||
async def async_set_humidity(self, humidity: int) -> None:
|
|
||||||
"""Set new target humidity."""
|
|
||||||
await self._client.climate_command(key=self._key, target_humidity=humidity)
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
await self._client.climate_command(
|
await self._client.climate_command(
|
||||||
|
@@ -15,7 +15,8 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==19.2.1",
|
"async-interrupt==1.1.1",
|
||||||
|
"aioesphomeapi==19.1.4",
|
||||||
"bluetooth-data-tools==1.15.0",
|
"bluetooth-data-tools==1.15.0",
|
||||||
"esphome-dashboard-api==1.2.3"
|
"esphome-dashboard-api==1.2.3"
|
||||||
],
|
],
|
||||||
|
@@ -186,22 +186,16 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
|||||||
data_to_send = {"text": event.data["tts_input"]}
|
data_to_send = {"text": event.data["tts_input"]}
|
||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END:
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END:
|
||||||
assert event.data is not None
|
assert event.data is not None
|
||||||
tts_output = event.data["tts_output"]
|
path = event.data["tts_output"]["url"]
|
||||||
if tts_output:
|
url = async_process_play_media_url(self.hass, path)
|
||||||
path = tts_output["url"]
|
data_to_send = {"url": url}
|
||||||
url = async_process_play_media_url(self.hass, path)
|
|
||||||
data_to_send = {"url": url}
|
|
||||||
|
|
||||||
if self.device_info.voice_assistant_version >= 2:
|
if self.device_info.voice_assistant_version >= 2:
|
||||||
media_id = tts_output["media_id"]
|
media_id = event.data["tts_output"]["media_id"]
|
||||||
self._tts_task = self.hass.async_create_background_task(
|
self._tts_task = self.hass.async_create_background_task(
|
||||||
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self._tts_done.set()
|
|
||||||
else:
|
else:
|
||||||
# Empty TTS response
|
|
||||||
data_to_send = {}
|
|
||||||
self._tts_done.set()
|
self._tts_done.set()
|
||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END:
|
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END:
|
||||||
assert event.data is not None
|
assert event.data is not None
|
||||||
|
@@ -4,9 +4,6 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Evil Genius Labs device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -18,8 +18,7 @@ from homeassistant.const import (
|
|||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
@@ -78,19 +77,8 @@ ATTR_PRESET_MODES = "preset_modes"
|
|||||||
# mypy: disallow-any-generics
|
# mypy: disallow-any-generics
|
||||||
|
|
||||||
|
|
||||||
class NotValidPresetModeError(ServiceValidationError):
|
class NotValidPresetModeError(ValueError):
|
||||||
"""Raised when the preset_mode is not in the preset_modes list."""
|
"""Exception class when the preset_mode in not in the preset_modes list."""
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, *args: object, translation_placeholders: dict[str, str] | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the exception."""
|
|
||||||
super().__init__(
|
|
||||||
*args,
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="not_valid_preset_mode",
|
|
||||||
translation_placeholders=translation_placeholders,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
@@ -119,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
),
|
),
|
||||||
vol.Optional(ATTR_PRESET_MODE): cv.string,
|
vol.Optional(ATTR_PRESET_MODE): cv.string,
|
||||||
},
|
},
|
||||||
"async_handle_turn_on_service",
|
"async_turn_on",
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
||||||
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
||||||
@@ -168,7 +156,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_SET_PRESET_MODE,
|
SERVICE_SET_PRESET_MODE,
|
||||||
{vol.Required(ATTR_PRESET_MODE): cv.string},
|
{vol.Required(ATTR_PRESET_MODE): cv.string},
|
||||||
"async_handle_set_preset_mode_service",
|
"async_set_preset_mode",
|
||||||
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
|
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -249,30 +237,17 @@ class FanEntity(ToggleEntity):
|
|||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@final
|
|
||||||
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
|
|
||||||
"""Validate and set new preset mode."""
|
|
||||||
self._valid_preset_mode_or_raise(preset_mode)
|
|
||||||
await self.async_set_preset_mode(preset_mode)
|
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
|
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
|
||||||
|
|
||||||
@final
|
|
||||||
@callback
|
|
||||||
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
|
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
|
||||||
"""Raise NotValidPresetModeError on invalid preset_mode."""
|
"""Raise NotValidPresetModeError on invalid preset_mode."""
|
||||||
preset_modes = self.preset_modes
|
preset_modes = self.preset_modes
|
||||||
if not preset_modes or preset_mode not in preset_modes:
|
if not preset_modes or preset_mode not in preset_modes:
|
||||||
preset_modes_str: str = ", ".join(preset_modes or [])
|
|
||||||
raise NotValidPresetModeError(
|
raise NotValidPresetModeError(
|
||||||
f"The preset_mode {preset_mode} is not a valid preset_mode:"
|
f"The preset_mode {preset_mode} is not a valid preset_mode:"
|
||||||
f" {preset_modes}",
|
f" {preset_modes}"
|
||||||
translation_placeholders={
|
|
||||||
"preset_mode": preset_mode,
|
|
||||||
"preset_modes": preset_modes_str,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_direction(self, direction: str) -> None:
|
def set_direction(self, direction: str) -> None:
|
||||||
@@ -292,18 +267,6 @@ class FanEntity(ToggleEntity):
|
|||||||
"""Turn on the fan."""
|
"""Turn on the fan."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@final
|
|
||||||
async def async_handle_turn_on_service(
|
|
||||||
self,
|
|
||||||
percentage: int | None = None,
|
|
||||||
preset_mode: str | None = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""Validate and turn on the fan."""
|
|
||||||
if preset_mode is not None:
|
|
||||||
self._valid_preset_mode_or_raise(preset_mode)
|
|
||||||
await self.async_turn_on(percentage, preset_mode, **kwargs)
|
|
||||||
|
|
||||||
async def async_turn_on(
|
async def async_turn_on(
|
||||||
self,
|
self,
|
||||||
percentage: int | None = None,
|
percentage: int | None = None,
|
||||||
|
@@ -144,10 +144,5 @@
|
|||||||
"reverse": "Reverse"
|
"reverse": "Reverse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"not_valid_preset_mode": {
|
|
||||||
"message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Fast.com sensor."""
|
"""Set up the Fast.com sensor."""
|
||||||
async_add_entities([SpeedtestSensor(entry.entry_id, hass.data[DOMAIN])])
|
async_add_entities([SpeedtestSensor(hass.data[DOMAIN])])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||||
@@ -38,10 +38,9 @@ class SpeedtestSensor(RestoreEntity, SensorEntity):
|
|||||||
_attr_icon = "mdi:speedometer"
|
_attr_icon = "mdi:speedometer"
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(self, entry_id: str, speedtest_data: dict[str, Any]) -> None:
|
def __init__(self, speedtest_data: dict[str, Any]) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._speedtest_data = speedtest_data
|
self._speedtest_data = speedtest_data
|
||||||
self._attr_unique_id = entry_id
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle entity which will be added."""
|
"""Handle entity which will be added."""
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"name": "[%key:common::config_flow::data::name%]",
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your FiveM server."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -131,9 +131,11 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
|
|||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
command = PRESET_TO_COMMAND[preset_mode]
|
if command := PRESET_TO_COMMAND.get(preset_mode):
|
||||||
async with self.coordinator.async_connect_and_update() as device:
|
async with self.coordinator.async_connect_and_update() as device:
|
||||||
await device.send_command(command)
|
await device.send_command(command)
|
||||||
|
else:
|
||||||
|
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Flo device."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -9,9 +9,6 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"rtsp_port": "RTSP port",
|
"rtsp_port": "RTSP port",
|
||||||
"stream": "Stream"
|
"stream": "Stream"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Foscam camera."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -5,9 +5,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Freebox router."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
|
@@ -26,9 +26,6 @@
|
|||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -38,9 +38,11 @@ async def async_setup_entry(
|
|||||||
FritzboxLight(
|
FritzboxLight(
|
||||||
coordinator,
|
coordinator,
|
||||||
ain,
|
ain,
|
||||||
|
device.get_colors(),
|
||||||
|
device.get_color_temps(),
|
||||||
)
|
)
|
||||||
for ain in coordinator.new_devices
|
for ain in coordinator.new_devices
|
||||||
if (coordinator.data.devices[ain]).has_lightbulb
|
if (device := coordinator.data.devices[ain]).has_lightbulb
|
||||||
)
|
)
|
||||||
|
|
||||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||||
@@ -55,10 +57,27 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
|||||||
self,
|
self,
|
||||||
coordinator: FritzboxDataUpdateCoordinator,
|
coordinator: FritzboxDataUpdateCoordinator,
|
||||||
ain: str,
|
ain: str,
|
||||||
|
supported_colors: dict,
|
||||||
|
supported_color_temps: list[int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the FritzboxLight entity."""
|
"""Initialize the FritzboxLight entity."""
|
||||||
super().__init__(coordinator, ain, None)
|
super().__init__(coordinator, ain, None)
|
||||||
|
|
||||||
|
if supported_color_temps:
|
||||||
|
# only available for color bulbs
|
||||||
|
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
||||||
|
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
||||||
|
|
||||||
|
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||||
|
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||||
self._supported_hs: dict[int, list[int]] = {}
|
self._supported_hs: dict[int, list[int]] = {}
|
||||||
|
for values in supported_colors.values():
|
||||||
|
hue = int(values[0][0])
|
||||||
|
self._supported_hs[hue] = [
|
||||||
|
int(values[0][1]),
|
||||||
|
int(values[1][1]),
|
||||||
|
int(values[2][1]),
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@@ -154,28 +173,3 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
|||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
await self.hass.async_add_executor_job(self.data.set_state_off)
|
await self.hass.async_add_executor_job(self.data.set_state_off)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Get light attributes from device after entity is added to hass."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
supported_colors = await self.hass.async_add_executor_job(
|
|
||||||
self.coordinator.data.devices[self.ain].get_colors
|
|
||||||
)
|
|
||||||
supported_color_temps = await self.hass.async_add_executor_job(
|
|
||||||
self.coordinator.data.devices[self.ain].get_color_temps
|
|
||||||
)
|
|
||||||
|
|
||||||
if supported_color_temps:
|
|
||||||
# only available for color bulbs
|
|
||||||
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
|
||||||
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
|
||||||
|
|
||||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
|
||||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
|
||||||
for values in supported_colors.values():
|
|
||||||
hue = int(values[0][0])
|
|
||||||
self._supported_hs[hue] = [
|
|
||||||
int(values[0][1]),
|
|
||||||
int(values[1][1]),
|
|
||||||
int(values[2][1]),
|
|
||||||
]
|
|
||||||
|
@@ -8,9 +8,6 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
|
@@ -8,9 +8,6 @@
|
|||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"phonebook": {
|
"phonebook": {
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20231208.2"]
|
"requirements": ["home-assistant-frontend==20231030.2"]
|
||||||
}
|
}
|
||||||
|
@@ -5,13 +5,10 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Frontier Silicon device."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"device_config": {
|
"device_config": {
|
||||||
"title": "Device configuration",
|
"title": "Device Configuration",
|
||||||
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
|
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
|
||||||
"data": {
|
"data": {
|
||||||
"pin": "[%key:common::config_flow::data::pin%]"
|
"pin": "[%key:common::config_flow::data::pin%]"
|
||||||
|
@@ -19,14 +19,13 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.use_ssl = entry.data.get(CONF_SSL, False)
|
|
||||||
self.fully = FullyKiosk(
|
self.fully = FullyKiosk(
|
||||||
async_get_clientsession(hass),
|
async_get_clientsession(hass),
|
||||||
entry.data[CONF_HOST],
|
entry.data[CONF_HOST],
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
use_ssl=self.use_ssl,
|
use_ssl=entry.data[CONF_SSL],
|
||||||
verify_ssl=entry.data.get(CONF_VERIFY_SSL, False),
|
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||||
)
|
)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -34,6 +33,7 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
name=entry.data[CONF_HOST],
|
name=entry.data[CONF_HOST],
|
||||||
update_interval=UPDATE_INTERVAL,
|
update_interval=UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
|
self.use_ssl = entry.data[CONF_SSL]
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
|
@@ -13,9 +13,6 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the device running your Fully Kiosk Browser application."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -68,12 +68,9 @@ class GeniusSwitch(GeniusZone, SwitchEntity):
|
|||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return the current state of the on/off zone.
|
"""Return the current state of the on/off zone.
|
||||||
|
|
||||||
The zone is considered 'on' if the mode is either 'override' or 'timer'.
|
The zone is considered 'on' if & only if it is override/on (e.g. timer/on is 'off').
|
||||||
"""
|
"""
|
||||||
return (
|
return self._zone.data["mode"] == "override" and self._zone.data["setpoint"]
|
||||||
self._zone.data["mode"] in ["override", "timer"]
|
|
||||||
and self._zone.data["setpoint"]
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Send the zone to Timer mode.
|
"""Send the zone to Timer mode.
|
||||||
|
@@ -10,9 +10,6 @@
|
|||||||
"version": "Glances API Version (2 or 3)",
|
"version": "Glances API Version (2 or 3)",
|
||||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the system running your Glances system monitor."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
|
@@ -6,9 +6,6 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"name": "[%key:common::config_flow::data::name%]"
|
"name": "[%key:common::config_flow::data::name%]"
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of your Goal Zero Yeti."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"confirm_discovery": {
|
"confirm_discovery": {
|
||||||
|
@@ -686,12 +686,8 @@ class GoogleEntity:
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
# Add Matter info
|
# Add Matter info
|
||||||
if (
|
if "matter" in self.hass.config.components and (
|
||||||
"matter" in self.hass.config.components
|
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
||||||
and any(x for x in device_entry.identifiers if x[0] == "matter")
|
|
||||||
and (
|
|
||||||
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
device["matterUniqueId"] = matter_info["unique_id"]
|
device["matterUniqueId"] = matter_info["unique_id"]
|
||||||
device["matterOriginalVendorId"] = matter_info["vendor_id"]
|
device["matterOriginalVendorId"] = matter_info["vendor_id"]
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Google Tasks todo platform."""
|
"""Google Tasks todo platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
from .api import AsyncConfigEntryAuth
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -36,31 +35,9 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
|||||||
result["title"] = item.summary
|
result["title"] = item.summary
|
||||||
if item.status is not None:
|
if item.status is not None:
|
||||||
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
||||||
if (due := item.due) is not None:
|
|
||||||
# due API field is a timestamp string, but with only date resolution
|
|
||||||
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
|
||||||
if (description := item.description) is not None:
|
|
||||||
result["notes"] = description
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
|
||||||
"""Convert tasks API items into a TodoItem."""
|
|
||||||
due: date | None = None
|
|
||||||
if (due_str := item.get("due")) is not None:
|
|
||||||
due = datetime.fromisoformat(due_str).date()
|
|
||||||
return TodoItem(
|
|
||||||
summary=item["title"],
|
|
||||||
uid=item["id"],
|
|
||||||
status=TODO_STATUS_MAP.get(
|
|
||||||
item.get("status", ""),
|
|
||||||
TodoItemStatus.NEEDS_ACTION,
|
|
||||||
),
|
|
||||||
due=due,
|
|
||||||
description=item.get("notes"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -91,8 +68,6 @@ class GoogleTaskTodoListEntity(
|
|||||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
|
||||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -113,7 +88,17 @@ class GoogleTaskTodoListEntity(
|
|||||||
"""Get the current set of To-do items."""
|
"""Get the current set of To-do items."""
|
||||||
if self.coordinator.data is None:
|
if self.coordinator.data is None:
|
||||||
return None
|
return None
|
||||||
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
|
return [
|
||||||
|
TodoItem(
|
||||||
|
summary=item["title"],
|
||||||
|
uid=item["id"],
|
||||||
|
status=TODO_STATUS_MAP.get(
|
||||||
|
item.get("status"), # type: ignore[arg-type]
|
||||||
|
TodoItemStatus.NEEDS_ACTION,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for item in _order_tasks(self.coordinator.data)
|
||||||
|
]
|
||||||
|
|
||||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||||
"""Add an item to the To-do list."""
|
"""Add an item to the To-do list."""
|
||||||
|
@@ -11,6 +11,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
|||||||
from .bridge import DiscoveryService
|
from .bridge import DiscoveryService
|
||||||
from .const import (
|
from .const import (
|
||||||
COORDINATORS,
|
COORDINATORS,
|
||||||
|
DATA_DISCOVERY_INTERVAL,
|
||||||
DATA_DISCOVERY_SERVICE,
|
DATA_DISCOVERY_SERVICE,
|
||||||
DISCOVERY_SCAN_INTERVAL,
|
DISCOVERY_SCAN_INTERVAL,
|
||||||
DISPATCHERS,
|
DISPATCHERS,
|
||||||
@@ -28,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
gree_discovery = DiscoveryService(hass)
|
gree_discovery = DiscoveryService(hass)
|
||||||
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
|
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
|
||||||
|
|
||||||
|
hass.data[DOMAIN].setdefault(DISPATCHERS, [])
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
async def _async_scan_update(_=None):
|
async def _async_scan_update(_=None):
|
||||||
@@ -37,10 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
_LOGGER.debug("Scanning network for Gree devices")
|
_LOGGER.debug("Scanning network for Gree devices")
|
||||||
await _async_scan_update()
|
await _async_scan_update()
|
||||||
|
|
||||||
entry.async_on_unload(
|
hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval(
|
||||||
async_track_time_interval(
|
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
||||||
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -48,6 +48,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
|
if hass.data[DOMAIN].get(DISPATCHERS) is not None:
|
||||||
|
for cleanup in hass.data[DOMAIN][DISPATCHERS]:
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None:
|
||||||
|
hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)()
|
||||||
|
|
||||||
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
|
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
|
||||||
hass.data.pop(DATA_DISCOVERY_SERVICE)
|
hass.data.pop(DATA_DISCOVERY_SERVICE)
|
||||||
|
|
||||||
|
@@ -47,6 +47,7 @@ from .bridge import DeviceDataUpdateCoordinator
|
|||||||
from .const import (
|
from .const import (
|
||||||
COORDINATORS,
|
COORDINATORS,
|
||||||
DISPATCH_DEVICE_DISCOVERED,
|
DISPATCH_DEVICE_DISCOVERED,
|
||||||
|
DISPATCHERS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FAN_MEDIUM_HIGH,
|
FAN_MEDIUM_HIGH,
|
||||||
FAN_MEDIUM_LOW,
|
FAN_MEDIUM_LOW,
|
||||||
@@ -87,7 +88,7 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Gree HVAC device from a config entry."""
|
"""Set up the Gree HVAC device from a config entry."""
|
||||||
@@ -100,7 +101,7 @@ async def async_setup_entry(
|
|||||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||||
init_device(coordinator)
|
init_device(coordinator)
|
||||||
|
|
||||||
entry.async_on_unload(
|
hass.data[DOMAIN][DISPATCHERS].append(
|
||||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
COORDINATORS = "coordinators"
|
COORDINATORS = "coordinators"
|
||||||
|
|
||||||
DATA_DISCOVERY_SERVICE = "gree_discovery"
|
DATA_DISCOVERY_SERVICE = "gree_discovery"
|
||||||
|
DATA_DISCOVERY_INTERVAL = "gree_discovery_interval"
|
||||||
|
|
||||||
DISCOVERY_SCAN_INTERVAL = 300
|
DISCOVERY_SCAN_INTERVAL = 300
|
||||||
DISCOVERY_TIMEOUT = 8
|
DISCOVERY_TIMEOUT = 8
|
||||||
|
@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
|
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN
|
||||||
from .entity import GreeEntity
|
from .entity import GreeEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Gree HVAC device from a config entry."""
|
"""Set up the Gree HVAC device from a config entry."""
|
||||||
@@ -119,7 +119,7 @@ async def async_setup_entry(
|
|||||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||||
init_device(coordinator)
|
init_device(coordinator)
|
||||||
|
|
||||||
entry.async_on_unload(
|
hass.data[DOMAIN][DISPATCHERS].append(
|
||||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user