forked from home-assistant/core
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17a0b4f3d0 | |||
| d0d228d9f4 | |||
| 309acb961b | |||
| 12f8ebb3ea | |||
| 612861061c | |||
| 83af5ec36b | |||
| 74102d0319 | |||
| fbd05a0fcf | |||
| a53c786fe0 | |||
| eb2728e5b9 | |||
| 3f17223387 | |||
| 74104cf107 | |||
| 13b4879723 | |||
| f1ec0b2c59 | |||
| 6d44daf599 | |||
| 644a6f5569 | |||
| fb83396522 | |||
| e825bd0bdb | |||
| 61823ec7e2 | |||
| cd133cbbe3 | |||
| 0e7a1bb76c | |||
| f86bf69ebc | |||
| adddf330fd | |||
| 10adb57b83 | |||
| 3160fe9abc | |||
| 6adb27d173 | |||
| 6e6aae2ea3 | |||
| 41a140d16c | |||
| 8880ab6498 | |||
| 389becc4f6 | |||
| 923530972a | |||
| b84850df9f | |||
| 9e7dc1d11d | |||
| 2830ed6147 | |||
| bfa919d078 | |||
| f09c28e61f | |||
| bfdba7713e | |||
| d6cadc1e3f | |||
| 20a6a3f195 | |||
| f60de45b52 | |||
| 77031d1ae4 | |||
| 9483a88ee1 | |||
| 3438a4f063 |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.7"
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
@@ -171,6 +171,8 @@ FRONTEND_INTEGRATIONS = {
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# The substages preceding it should also have no timeout, until we ensure that the recorder
|
||||
# is not accidentally promoted as a dependency of any of the integrations in them.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
@@ -927,11 +929,7 @@ async def _async_set_up_integrations(
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
timeout,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message=f"Bootstrap stage {name} timeout",
|
||||
):
|
||||
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
@@ -943,11 +941,7 @@ async def _async_set_up_integrations(
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
WRAP_UP_TIMEOUT,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message="Bootstrap startup wrap up timeout",
|
||||
):
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
|
||||
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
manufacturer="Agent",
|
||||
model="Camera",
|
||||
name=f"{device.client.name} {device.name}",
|
||||
|
||||
@@ -5,22 +5,23 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
@@ -31,8 +32,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
async def _update_method() -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""The Airthings integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.airthings = airthings
|
||||
|
||||
async def _update_method(self) -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await self.airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,9 +27,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirthingsConfigEntry
|
||||
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
@@ -56,12 +54,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@@ -148,7 +140,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class AirthingsHeaterEnergySensor(
|
||||
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
||||
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
|
||||
):
|
||||
"""Representation of a Airthings Sensor device."""
|
||||
|
||||
@@ -157,7 +149,7 @@ class AirthingsHeaterEnergySensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirthingsDataUpdateCoordinator,
|
||||
coordinator: AirthingsDataCoordinatorType,
|
||||
airthings_device: AirthingsDevice,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Diagnostics support for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
devices: list[dict[str, dict[str, Any]]] = [
|
||||
build_device_data(device) for device in coordinator.data.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": devices,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
assert device_entry.serial_number
|
||||
|
||||
return build_device_data(coordinator.data[device_entry.serial_number])
|
||||
|
||||
|
||||
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"""Build device data for diagnostics."""
|
||||
return {
|
||||
"account name": device.account_name,
|
||||
"capabilities": device.capabilities,
|
||||
"device family": device.device_family,
|
||||
"device type": device.device_type,
|
||||
"device cluster members": device.device_cluster_members,
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
}
|
||||
@@ -4,119 +4,32 @@
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
{ "macaddress": "F02F9E*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.4"]
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -225,8 +225,7 @@ class Analytics:
|
||||
LOGGER.error(err)
|
||||
return
|
||||
|
||||
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
||||
|
||||
configuration_set = set(yaml_configuration)
|
||||
er_platforms = {
|
||||
entity.platform
|
||||
for entity in ent_reg.entities.values()
|
||||
@@ -371,13 +370,3 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
"""Extract domains from the YAML configuration."""
|
||||
domains = set(yaml_configuration)
|
||||
for platforms in conf_util.extract_platform_integrations(
|
||||
yaml_configuration, BASE_PLATFORMS
|
||||
).values():
|
||||
domains.update(platforms)
|
||||
return domains
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key="radiation_rate",
|
||||
translation_key="radiation_rate",
|
||||
name="Radiation Dose Rate",
|
||||
native_unit_of_measurement="µSv/h",
|
||||
native_unit_of_measurement="μSv/h",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
scale=0.001,
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
|
||||
from .config import AxisConfig
|
||||
from .entity_loader import AxisEntityLoader
|
||||
from .event_source import AxisEventSource
|
||||
@@ -79,7 +79,7 @@ class AxisHub:
|
||||
config_entry_id=self.config.entry.entry_id,
|
||||
configuration_url=self.api.config.url,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
identifiers={(AXIS_DOMAIN, self.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=f"{self.config.model} {self.product_type}",
|
||||
name=self.config.name,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.6"]
|
||||
"requirements": ["numpy==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_GESTURE, DOMAIN
|
||||
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
|
||||
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
|
||||
from .device_trigger import (
|
||||
CONF_BOTH_BUTTONS,
|
||||
@@ -200,6 +200,6 @@ def async_describe_events(
|
||||
}
|
||||
|
||||
async_describe_event(
|
||||
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
)
|
||||
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
||||
@@ -21,11 +21,7 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -94,21 +90,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
"requirements": ["home-assistant-frontend==20250528.0"]
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
||||
FytaSensorEntityDescription(
|
||||
key="light",
|
||||
translation_key="light",
|
||||
native_unit_of_measurement="µmol/s⋅m²",
|
||||
native_unit_of_measurement="μmol/s⋅m²",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda plant: plant.light,
|
||||
),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
|
||||
from .entity import GroupEntity
|
||||
|
||||
DEFAULT_NAME = "Sensor Group"
|
||||
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return state_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_state_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return device_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_device_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if device_class:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
else:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
GROUP_DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_no_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,14 +22,11 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
@@ -59,21 +56,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the HDDTemp sensor."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "hddtemp",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
|
||||
@@ -27,7 +27,6 @@ PLATFORMS = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
Platform.VALVE,
|
||||
]
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""The homee siren platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
|
||||
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add siren entities for homee."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeSiren(attribute, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type == AttributeType.SIREN
|
||||
)
|
||||
|
||||
|
||||
class HomeeSiren(HomeeEntity, SirenEntity):
|
||||
"""Representation of a homee siren device."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the siren."""
|
||||
return self._attribute.current_value == 1.0
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on."""
|
||||
await self.async_set_homee_value(1)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren off."""
|
||||
await self.async_set_homee_value(0)
|
||||
@@ -39,14 +39,14 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
cors = aiohttp_cors.setup(
|
||||
app,
|
||||
defaults={
|
||||
host: aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
host: aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
for host in origins
|
||||
},
|
||||
)
|
||||
|
||||
cors_added: set[str] = set()
|
||||
cors_added = set()
|
||||
|
||||
def _allow_cors(
|
||||
route: AbstractRoute | AbstractResource,
|
||||
@@ -69,13 +69,13 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
if path_str in cors_added:
|
||||
return
|
||||
|
||||
cors.add(route, config) # type: ignore[arg-type]
|
||||
cors.add(route, config)
|
||||
cors_added.add(path_str)
|
||||
|
||||
app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors(
|
||||
route,
|
||||
{
|
||||
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
"*": aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
},
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.8.0"]
|
||||
"requirements": ["aioimmich==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
import mimetypes
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.exceptions import ImmichError
|
||||
@@ -38,14 +39,13 @@ class ImmichMediaSourceIdentifier:
|
||||
|
||||
def __init__(self, identifier: str) -> None:
|
||||
"""Split identifier into parts."""
|
||||
parts = identifier.split("|")
|
||||
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
|
||||
parts = identifier.split("/")
|
||||
# config_entry.unique_id/collection/collection_id/asset_id/file_name
|
||||
self.unique_id = parts[0]
|
||||
self.collection = parts[1] if len(parts) > 1 else None
|
||||
self.collection_id = parts[2] if len(parts) > 2 else None
|
||||
self.asset_id = parts[3] if len(parts) > 3 else None
|
||||
self.file_name = parts[4] if len(parts) > 3 else None
|
||||
self.mime_type = parts[5] if len(parts) > 3 else None
|
||||
|
||||
|
||||
class ImmichMediaSource(MediaSource):
|
||||
@@ -111,7 +111,7 @@ class ImmichMediaSource(MediaSource):
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{identifier.unique_id}|albums",
|
||||
identifier=f"{identifier.unique_id}/albums",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title="albums",
|
||||
@@ -130,13 +130,13 @@ class ImmichMediaSource(MediaSource):
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||
identifier=f"{identifier.unique_id}/albums/{album.album_id}",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title=album.album_name,
|
||||
title=album.name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
@@ -157,67 +157,57 @@ class ImmichMediaSource(MediaSource):
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.original_file_name}|"
|
||||
f"{mime_type}"
|
||||
f"{identifier.unique_id}/albums/"
|
||||
f"{identifier.collection_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=mime_type,
|
||||
title=asset.original_file_name,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{mime_type}",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if (mime_type := asset.original_mime_type)
|
||||
and mime_type.startswith("image/")
|
||||
if asset.mime_type.startswith("image/")
|
||||
]
|
||||
|
||||
ret.extend(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.original_file_name}|"
|
||||
f"{mime_type}"
|
||||
f"{identifier.unique_id}/albums/"
|
||||
f"{identifier.collection_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=mime_type,
|
||||
title=asset.original_file_name,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if (mime_type := asset.original_mime_type)
|
||||
and mime_type.startswith("video/")
|
||||
if asset.mime_type.startswith("video/")
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
try:
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
except IndexError as err:
|
||||
raise Unresolvable(
|
||||
f"Could not parse identifier: {item.identifier}"
|
||||
) from err
|
||||
|
||||
if identifier.mime_type is None:
|
||||
raise Unresolvable(
|
||||
f"Could not resolve identifier that has no mime-type: {item.identifier}"
|
||||
)
|
||||
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
if identifier.file_name is None:
|
||||
raise Unresolvable("No file name")
|
||||
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise Unresolvable("No file extension")
|
||||
return PlayMedia(
|
||||
(
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
|
||||
),
|
||||
identifier.mime_type,
|
||||
mime_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -238,10 +228,10 @@ class ImmichMediaView(HomeAssistantView):
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
raise HTTPNotFound
|
||||
|
||||
try:
|
||||
asset_id, size, mime_type_base, mime_type_format = location.split("/")
|
||||
except ValueError as err:
|
||||
raise HTTPNotFound from err
|
||||
asset_id, file_name, size = location.split("/")
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise HTTPNotFound
|
||||
|
||||
entry: ImmichConfigEntry | None = (
|
||||
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
@@ -252,7 +242,7 @@ class ImmichMediaView(HomeAssistantView):
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
# stream response for videos
|
||||
if mime_type_base == "video":
|
||||
if mime_type.startswith("video/"):
|
||||
try:
|
||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||
except ImmichError as exc:
|
||||
@@ -269,4 +259,4 @@ class ImmichMediaView(HomeAssistantView):
|
||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||
except ImmichError as exc:
|
||||
raise HTTPNotFound from exc
|
||||
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")
|
||||
return Response(body=image, content_type=mime_type)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyiqvia"],
|
||||
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
|
||||
"requirements": ["numpy==2.2.2", "pyiqvia==2022.04.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
"requirements": ["pyiskra==0.1.15"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -35,17 +35,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
|
||||
coordinator = JellyfinDataUpdateCoordinator(
|
||||
hass, entry, client, server_info, user_id
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.async_on_unload(client.stop)
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .coordinator import JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ class JellyfinServerEntity(JellyfinEntity):
|
||||
"""Initialize the Jellyfin entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
"requirements": ["pylamarzocco==2.0.7"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "LG ThinQ",
|
||||
"codeowners": ["@LG-ThinQ-Integration"],
|
||||
"config_flow": true,
|
||||
"dhcp": [{ "macaddress": "34E6E6*" }],
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_thinq",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["thinqconnect"],
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.9"],
|
||||
"requirements": ["python-linkplay==0.2.8"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
|
||||
assert level_control is not None
|
||||
|
||||
level = round(
|
||||
level = round( # type: ignore[unreachable]
|
||||
renormalize(
|
||||
brightness,
|
||||
(0, 255),
|
||||
@@ -249,7 +249,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
# We should not get here if brightness is not supported.
|
||||
assert level_control is not None
|
||||
|
||||
LOGGER.debug(
|
||||
LOGGER.debug( # type: ignore[unreachable]
|
||||
"Got brightness %s for %s",
|
||||
level_control.currentLevel,
|
||||
self.entity_id,
|
||||
|
||||
@@ -63,6 +63,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"title": "The MELCloud YAML configuration import failed",
|
||||
"description": "Configuring MELCloud using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The MELCloud YAML configuration import failed",
|
||||
"description": "Configuring MELCloud using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"room_temperature": {
|
||||
|
||||
@@ -58,3 +58,15 @@ class NikoHomeControlConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import a config entry."""
|
||||
self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]})
|
||||
error = await test_connection(import_info[CONF_HOST])
|
||||
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title="Niko Home Control",
|
||||
data={CONF_HOST: import_info[CONF_HOST]},
|
||||
)
|
||||
return self.async_abort(reason=error)
|
||||
|
||||
@@ -5,19 +5,80 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from nhc.light import NHCLight
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
brightness_supported,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import NHCController, NikoHomeControlConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import NikoHomeControlEntity
|
||||
|
||||
# delete after 2025.7.0
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Niko Home Control light platform."""
|
||||
# Start import flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Niko Home Control",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Niko Home Control",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -17,5 +17,11 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "YAML import failed due to a connection error",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ from pyrail.models import StationDetails
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
@@ -20,6 +22,7 @@ from .const import (
|
||||
CONF_EXCLUDE_VIAS,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATION_FROM,
|
||||
CONF_STATION_LIVE,
|
||||
CONF_STATION_TO,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -112,6 +115,68 @@ class NMBSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import configuration from yaml."""
|
||||
try:
|
||||
self.stations = await self._fetch_stations()
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="api_unavailable")
|
||||
|
||||
station_from = None
|
||||
station_to = None
|
||||
station_live = None
|
||||
for station in self.stations:
|
||||
if user_input[CONF_STATION_FROM] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_from = station
|
||||
if user_input[CONF_STATION_TO] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_to = station
|
||||
if CONF_STATION_LIVE in user_input and user_input[CONF_STATION_LIVE] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_live = station
|
||||
|
||||
if station_from is None or station_to is None:
|
||||
return self.async_abort(reason="invalid_station")
|
||||
if station_from == station_to:
|
||||
return self.async_abort(reason="same_station")
|
||||
|
||||
# config flow uses id and not the standard name
|
||||
user_input[CONF_STATION_FROM] = station_from.id
|
||||
user_input[CONF_STATION_TO] = station_to.id
|
||||
|
||||
if station_live:
|
||||
user_input[CONF_STATION_LIVE] = station_live.id
|
||||
entity_registry = er.async_get(self.hass)
|
||||
prefix = "live"
|
||||
vias = "_excl_vias" if user_input.get(CONF_EXCLUDE_VIAS, False) else ""
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{prefix}_{station_live.standard_name}_{station_from.standard_name}_{station_to.standard_name}",
|
||||
):
|
||||
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
|
||||
entity_registry.async_update_entity(
|
||||
entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{prefix}_{station_live.name}_{station_from.name}_{station_to.name}",
|
||||
):
|
||||
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
|
||||
entity_registry.async_update_entity(
|
||||
entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect to NMBS."""
|
||||
|
||||
@@ -8,19 +8,30 @@ from typing import Any
|
||||
|
||||
from pyrail import iRail
|
||||
from pyrail.models import ConnectionDetails, LiveboardDeparture, StationDetails
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_SHOW_ON_MAP,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
@@ -36,9 +47,22 @@ from .const import ( # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "NMBS"
|
||||
|
||||
DEFAULT_ICON = "mdi:train"
|
||||
DEFAULT_ICON_ALERT = "mdi:alert-octagon"
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_STATION_FROM): cv.string,
|
||||
vol.Required(CONF_STATION_TO): cv.string,
|
||||
vol.Optional(CONF_STATION_LIVE): cv.string,
|
||||
vol.Optional(CONF_EXCLUDE_VIAS, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_time_until(departure_time: datetime | None = None):
|
||||
"""Calculate the time between now and a train's departure time."""
|
||||
@@ -61,6 +85,71 @@ def get_ride_duration(departure_time: datetime, arrival_time: datetime, delay=0)
|
||||
return duration_time + get_delay_in_minutes(delay)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the NMBS sensor with iRail API."""
|
||||
|
||||
if config[CONF_PLATFORM] == DOMAIN:
|
||||
if CONF_SHOW_ON_MAP not in config:
|
||||
config[CONF_SHOW_ON_MAP] = False
|
||||
if CONF_EXCLUDE_VIAS not in config:
|
||||
config[CONF_EXCLUDE_VIAS] = False
|
||||
|
||||
station_types = [CONF_STATION_FROM, CONF_STATION_TO, CONF_STATION_LIVE]
|
||||
|
||||
for station_type in station_types:
|
||||
station = (
|
||||
find_station_by_name(hass, config[station_type])
|
||||
if station_type in config
|
||||
else None
|
||||
)
|
||||
if station is None and station_type in config:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_station_not_found",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_station_not_found",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "NMBS",
|
||||
"station_name": config[station_type],
|
||||
"url": "/config/integrations/dashboard/add?domain=nmbs",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "NMBS",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -247,6 +336,7 @@ class NMBSSensor(SensorEntity):
|
||||
|
||||
delay = get_delay_in_minutes(self._attrs.departure.delay)
|
||||
departure = get_time_until(self._attrs.departure.time)
|
||||
canceled = self._attrs.departure.canceled
|
||||
|
||||
attrs = {
|
||||
"destination": self._attrs.departure.station,
|
||||
@@ -256,13 +346,14 @@ class NMBSSensor(SensorEntity):
|
||||
"vehicle_id": self._attrs.departure.vehicle,
|
||||
}
|
||||
|
||||
attrs["canceled"] = self._attrs.departure.canceled
|
||||
if attrs["canceled"]:
|
||||
attrs["departure"] = None
|
||||
attrs["departure_minutes"] = None
|
||||
else:
|
||||
if not canceled:
|
||||
attrs["departure"] = f"In {departure} minutes"
|
||||
attrs["departure_minutes"] = departure
|
||||
attrs["canceled"] = False
|
||||
else:
|
||||
attrs["departure"] = None
|
||||
attrs["departure_minutes"] = None
|
||||
attrs["canceled"] = True
|
||||
|
||||
if self._show_on_map and self.station_coordinates:
|
||||
attrs[ATTR_LATITUDE] = self.station_coordinates[0]
|
||||
@@ -278,8 +369,9 @@ class NMBSSensor(SensorEntity):
|
||||
via.timebetween
|
||||
) + get_delay_in_minutes(via.departure.delay)
|
||||
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
if delay > 0:
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -25,5 +25,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_station_not_found": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a problem importing your YAML configuration.\n\nThe used station \"{station_name}\" could not be found. Fix it or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ SUPPORT_FLAGS = (
|
||||
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
|
||||
|
||||
MIN_TEMPERATURE = 7
|
||||
MAX_TEMPERATURE = 30
|
||||
MAX_TEMPERATURE = 40
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import NukiEntryData
|
||||
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN, ERROR_STATES
|
||||
from .const import ATTR_ENABLE, ATTR_UNLATCH, DOMAIN as NUKI_DOMAIN, ERROR_STATES
|
||||
from .entity import NukiEntity
|
||||
from .helpers import CannotConnect
|
||||
|
||||
@@ -29,7 +29,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Nuki lock platform."""
|
||||
entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||
entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id]
|
||||
coordinator = entry_data.coordinator
|
||||
|
||||
entities: list[NukiDeviceEntity] = [
|
||||
|
||||
@@ -268,19 +268,19 @@ class NumberDeviceClass(StrEnum):
|
||||
"""
|
||||
|
||||
PM1 = "pm1"
|
||||
"""Particulate matter <= 1 µm.
|
||||
"""Particulate matter <= 1 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM10 = "pm10"
|
||||
"""Particulate matter <= 10 µm.
|
||||
"""Particulate matter <= 10 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM25 = "pm25"
|
||||
"""Particulate matter <= 2.5 µm.
|
||||
"""Particulate matter <= 2.5 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
|
||||
from .entity import OneWireEntity, OneWireEntityDescription
|
||||
from .onewirehub import (
|
||||
SIGNAL_NEW_DEVICE_CONNECTED,
|
||||
@@ -37,14 +37,13 @@ class OneWireBinarySensorEntityDescription(
|
||||
):
|
||||
"""Class describing OneWire binary sensor entities."""
|
||||
|
||||
read_mode = READ_MODE_INT
|
||||
|
||||
|
||||
DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
|
||||
"12": tuple(
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -54,6 +53,7 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -63,6 +63,7 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"sensed.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="sensed_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -77,6 +78,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = {
|
||||
OneWireBinarySensorEntityDescription(
|
||||
key=f"hub/short.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
translation_key="hub_short_id",
|
||||
@@ -160,4 +162,4 @@ class OneWireBinarySensorEntity(OneWireEntity, BinarySensorEntity):
|
||||
"""Return true if sensor is on."""
|
||||
if self._state is None:
|
||||
return None
|
||||
return self._state == 1
|
||||
return bool(self._state)
|
||||
|
||||
@@ -51,5 +51,6 @@ MANUFACTURER_MAXIM = "Maxim Integrated"
|
||||
MANUFACTURER_HOBBYBOARDS = "Hobby Boards"
|
||||
MANUFACTURER_EDS = "Embedded Data Systems"
|
||||
|
||||
READ_MODE_BOOL = "bool"
|
||||
READ_MODE_FLOAT = "float"
|
||||
READ_MODE_INT = "int"
|
||||
|
||||
@@ -10,8 +10,9 @@ from pyownet import protocol
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import READ_MODE_INT
|
||||
from .const import READ_MODE_BOOL, READ_MODE_INT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -44,7 +45,7 @@ class OneWireEntity(Entity):
|
||||
self._attr_unique_id = f"/{device_id}/{description.key}"
|
||||
self._attr_device_info = device_info
|
||||
self._device_file = device_file
|
||||
self._state: int | float | None = None
|
||||
self._state: StateType = None
|
||||
self._value_raw: float | None = None
|
||||
self._owproxy = owproxy
|
||||
|
||||
@@ -81,5 +82,7 @@ class OneWireEntity(Entity):
|
||||
_LOGGER.debug("Fetching %s data recovered", self.name)
|
||||
if self.entity_description.read_mode == READ_MODE_INT:
|
||||
self._state = int(self._value_raw)
|
||||
elif self.entity_description.read_mode == READ_MODE_BOOL:
|
||||
self._state = int(self._value_raw) == 1
|
||||
else:
|
||||
self._state = self._value_raw
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_INT
|
||||
from .const import DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, READ_MODE_BOOL
|
||||
from .entity import OneWireEntity, OneWireEntityDescription
|
||||
from .onewirehub import (
|
||||
SIGNAL_NEW_DEVICE_CONNECTED,
|
||||
@@ -32,14 +32,13 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
class OneWireSwitchEntityDescription(OneWireEntityDescription, SwitchEntityDescription):
|
||||
"""Class describing OneWire switch entities."""
|
||||
|
||||
read_mode = READ_MODE_INT
|
||||
|
||||
|
||||
DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
"05": (
|
||||
OneWireSwitchEntityDescription(
|
||||
key="PIO",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio",
|
||||
),
|
||||
),
|
||||
@@ -48,6 +47,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -57,6 +57,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"latch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="latch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -68,6 +69,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
key="IAD",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="iad",
|
||||
),
|
||||
),
|
||||
@@ -76,6 +78,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -85,6 +88,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"latch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="latch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -95,6 +99,7 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"PIO.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
translation_key="pio_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
)
|
||||
@@ -110,6 +115,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"hub/branch.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="hub_branch_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -121,6 +127,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"moisture/is_leaf.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="leaf_sensor_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -131,6 +138,7 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = {
|
||||
OneWireSwitchEntityDescription(
|
||||
key=f"moisture/is_moisture.{device_key}",
|
||||
entity_registry_enabled_default=False,
|
||||
read_mode=READ_MODE_BOOL,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="moisture_sensor_id",
|
||||
translation_placeholders={"id": str(device_key)},
|
||||
@@ -218,7 +226,7 @@ class OneWireSwitchEntity(OneWireEntity, SwitchEntity):
|
||||
"""Return true if switch is on."""
|
||||
if self._state is None:
|
||||
return None
|
||||
return self._state == 1
|
||||
return bool(self._state)
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.12.3"]
|
||||
"requirements": ["opower==0.12.2"]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"message": "Error connecting to the Overseerr instance: {error}"
|
||||
},
|
||||
"auth_error": {
|
||||
"message": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
"message": "Invalid API key."
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded."
|
||||
|
||||
@@ -26,7 +26,7 @@ from .coordinator import (
|
||||
PaperlessStatusCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.UPDATE]
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool:
|
||||
|
||||
@@ -16,7 +16,6 @@ async def async_get_config_entry_diagnostics(
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"pngx_version": entry.runtime_data.status.api.host_version,
|
||||
"data": {
|
||||
"statistics": asdict(entry.runtime_data.statistics.data),
|
||||
"status": asdict(entry.runtime_data.status.data),
|
||||
|
||||
@@ -126,11 +126,6 @@
|
||||
"error": "[%key:common::state::error%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"paperless_update": {
|
||||
"name": "Software"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Update platform for Paperless-ngx."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from pypaperless.exceptions import PaperlessConnectionError
|
||||
|
||||
from homeassistant.components.update import (
|
||||
UpdateDeviceClass,
|
||||
UpdateEntity,
|
||||
UpdateEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import LOGGER
|
||||
from .coordinator import PaperlessConfigEntry, PaperlessStatusCoordinator
|
||||
from .entity import PaperlessEntity
|
||||
|
||||
PAPERLESS_CHANGELOGS = "https://docs.paperless-ngx.com/changelog/"
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=24)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: PaperlessConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Paperless-ngx update entities."""
|
||||
|
||||
description = UpdateEntityDescription(
|
||||
key="paperless_update",
|
||||
translation_key="paperless_update",
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
PaperlessUpdate(
|
||||
coordinator=entry.runtime_data.status,
|
||||
description=description,
|
||||
)
|
||||
],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
|
||||
class PaperlessUpdate(PaperlessEntity[PaperlessStatusCoordinator], UpdateEntity):
|
||||
"""Defines a Paperless-ngx update entity."""
|
||||
|
||||
release_url = PAPERLESS_CHANGELOGS
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True because we need to poll the latest version."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._attr_available
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str | None:
|
||||
"""Return the installed version."""
|
||||
return self.coordinator.api.host_version
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
remote_version = None
|
||||
try:
|
||||
remote_version = await self.coordinator.api.remote_version()
|
||||
except PaperlessConnectionError as err:
|
||||
if self._attr_available:
|
||||
LOGGER.warning("Could not fetch remote version: %s", err)
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
if remote_version.version is None or remote_version.version == "0.0.0":
|
||||
if self._attr_available:
|
||||
LOGGER.warning("Remote version is not available or invalid")
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
self._attr_latest_version = remote_version.version.lstrip("v")
|
||||
self._attr_available = True
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyprobeplus==1.0.1"]
|
||||
"requirements": ["pyprobeplus==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.const import ATTR_SW_VERSION
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
@@ -38,5 +40,7 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator[T]]):
|
||||
name=self.coordinator.config_entry.title,
|
||||
)
|
||||
if isinstance(self.coordinator, StatusDataUpdateCoordinator):
|
||||
device_info[ATTR_SW_VERSION] = self.coordinator.data.version
|
||||
device_info[ATTR_SW_VERSION] = cast(
|
||||
StatusDataUpdateCoordinator, self.coordinator
|
||||
).data.version
|
||||
return device_info
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.41",
|
||||
"SQLAlchemy==2.0.40",
|
||||
"fnv-hash-fast==1.5.0",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -150,10 +150,6 @@ async def async_setup_entry(
|
||||
|
||||
if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED:
|
||||
# Their are new cameras/chimes connected, reload to add them.
|
||||
_LOGGER.debug(
|
||||
"Reloading Reolink %s to add new device (capabilities)",
|
||||
host.api.nvr_name,
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_reload(config_entry.entry_id)
|
||||
)
|
||||
@@ -237,14 +233,6 @@ async def async_setup_entry(
|
||||
"privacy_mode_change", async_privacy_mode_change, 623
|
||||
)
|
||||
|
||||
# ensure host device is setup before connected camera devices that use via_device
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, host.unique_id)},
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, host.api.mac_address)},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
|
||||
@@ -194,13 +194,6 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
if existing_entry and existing_entry.data[CONF_HOST] != discovery_info.ip:
|
||||
_LOGGER.debug(
|
||||
"Reolink DHCP reported new IP '%s', updating from old IP '%s'",
|
||||
discovery_info.ip,
|
||||
existing_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
|
||||
@@ -462,12 +462,6 @@
|
||||
"doorbell_button_sound": {
|
||||
"default": "mdi:volume-high"
|
||||
},
|
||||
"hardwired_chime_enabled": {
|
||||
"default": "mdi:bell",
|
||||
"state": {
|
||||
"off": "mdi:bell-off"
|
||||
}
|
||||
},
|
||||
"hdr": {
|
||||
"default": "mdi:hdr"
|
||||
},
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.5"]
|
||||
"requirements": ["reolink-aio==0.13.4"]
|
||||
}
|
||||
|
||||
@@ -910,9 +910,6 @@
|
||||
"auto_focus": {
|
||||
"name": "Auto focus"
|
||||
},
|
||||
"hardwired_chime_enabled": {
|
||||
"name": "Hardwired chime enabled"
|
||||
},
|
||||
"guard_return": {
|
||||
"name": "Guard return"
|
||||
},
|
||||
|
||||
@@ -216,16 +216,6 @@ SWITCH_ENTITIES = (
|
||||
value=lambda api, ch: api.baichuan.privacy_mode(ch),
|
||||
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value),
|
||||
),
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="hardwired_chime_enabled",
|
||||
cmd_key="483",
|
||||
translation_key="hardwired_chime_enabled",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
supported=lambda api, ch: api.supported(ch, "hardwired_chime"),
|
||||
value=lambda api, ch: api.baichuan.hardwired_chime_enabled(ch),
|
||||
method=lambda api, ch, value: api.baichuan.set_ding_dong_ctrl(ch, enable=value),
|
||||
),
|
||||
)
|
||||
|
||||
NVR_SWITCH_ENTITIES = (
|
||||
|
||||
@@ -52,7 +52,6 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
verify_ssl=False,
|
||||
ssl_cipher=SSLCipherList.INSECURE,
|
||||
)
|
||||
self._vod_type: str | None = None
|
||||
|
||||
async def get(
|
||||
self,
|
||||
@@ -69,8 +68,6 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
|
||||
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
|
||||
ch = int(channel)
|
||||
if self._vod_type is not None:
|
||||
vod_type = self._vod_type
|
||||
try:
|
||||
host = get_host(self.hass, config_entry_id)
|
||||
except Unresolvable:
|
||||
@@ -130,25 +127,6 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
"apolication/octet-stream",
|
||||
]:
|
||||
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
|
||||
if (
|
||||
reolink_response.content_type == "video/x-flv"
|
||||
and vod_type == VodRequestType.PLAYBACK.value
|
||||
):
|
||||
# next time use DOWNLOAD immediately
|
||||
self._vod_type = VodRequestType.DOWNLOAD.value
|
||||
_LOGGER.debug(
|
||||
"%s, retrying using download instead of playback cmd", err_str
|
||||
)
|
||||
return await self.get(
|
||||
request,
|
||||
config_entry_id,
|
||||
channel,
|
||||
stream_res,
|
||||
self._vod_type,
|
||||
filename,
|
||||
retry,
|
||||
)
|
||||
|
||||
_LOGGER.error(err_str)
|
||||
if reolink_response.content_type == "text/html":
|
||||
text = await reolink_response.text()
|
||||
@@ -162,10 +140,7 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
reolink_response.reason,
|
||||
response_headers,
|
||||
)
|
||||
if "Content-Type" not in response_headers:
|
||||
response_headers["Content-Type"] = reolink_response.content_type
|
||||
if response_headers["Content-Type"] == "apolication/octet-stream":
|
||||
response_headers["Content-Type"] = "application/octet-stream"
|
||||
response_headers["Content-Type"] = "video/mp4"
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=reolink_response.status,
|
||||
|
||||
@@ -298,19 +298,19 @@ class SensorDeviceClass(StrEnum):
|
||||
"""
|
||||
|
||||
PM1 = "pm1"
|
||||
"""Particulate matter <= 1 µm.
|
||||
"""Particulate matter <= 1 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM10 = "pm10"
|
||||
"""Particulate matter <= 10 µm.
|
||||
"""Particulate matter <= 10 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
PM25 = "pm25"
|
||||
"""Particulate matter <= 2.5 µm.
|
||||
"""Particulate matter <= 2.5 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
"""
|
||||
|
||||
@@ -92,10 +92,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
"""Mark the first item with matching `name` as completed."""
|
||||
data = hass.data[DOMAIN]
|
||||
name = call.data[ATTR_NAME]
|
||||
|
||||
try:
|
||||
await data.async_complete(name)
|
||||
except NoMatchingShoppingListItem:
|
||||
_LOGGER.error("Completing of item failed: %s cannot be found", name)
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
except IndexError:
|
||||
_LOGGER.error("Updating of item failed: %s cannot be found", name)
|
||||
else:
|
||||
await data.async_update(item["id"], {"name": name, "complete": True})
|
||||
|
||||
async def incomplete_item_service(call: ServiceCall) -> None:
|
||||
"""Mark the first item with matching `name` as incomplete."""
|
||||
@@ -255,30 +258,6 @@ class ShoppingData:
|
||||
)
|
||||
return removed
|
||||
|
||||
async def async_complete(
|
||||
self, name: str, context: Context | None = None
|
||||
) -> list[dict[str, JsonValueType]]:
|
||||
"""Mark all shopping list items with the given name as complete."""
|
||||
complete_items = [
|
||||
item for item in self.items if item["name"] == name and not item["complete"]
|
||||
]
|
||||
|
||||
if len(complete_items) == 0:
|
||||
raise NoMatchingShoppingListItem
|
||||
|
||||
for item in complete_items:
|
||||
_LOGGER.debug("Completing %s", item)
|
||||
item["complete"] = True
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
for item in complete_items:
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
{"action": "complete", "item": item},
|
||||
context=context,
|
||||
)
|
||||
return complete_items
|
||||
|
||||
async def async_update(
|
||||
self, item_id: str | None, info: dict[str, Any], context: Context | None = None
|
||||
) -> dict[str, JsonValueType]:
|
||||
|
||||
@@ -5,17 +5,15 @@ from __future__ import annotations
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
|
||||
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED, NoMatchingShoppingListItem
|
||||
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED
|
||||
|
||||
INTENT_ADD_ITEM = "HassShoppingListAddItem"
|
||||
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
|
||||
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Set up the Shopping List intents."""
|
||||
intent.async_register(hass, AddItemIntent())
|
||||
intent.async_register(hass, CompleteItemIntent())
|
||||
intent.async_register(hass, ListTopItemsIntent())
|
||||
|
||||
|
||||
@@ -38,33 +36,6 @@ class AddItemIntent(intent.IntentHandler):
|
||||
return response
|
||||
|
||||
|
||||
class CompleteItemIntent(intent.IntentHandler):
|
||||
"""Handle CompleteItem intents."""
|
||||
|
||||
intent_type = INTENT_COMPLETE_ITEM
|
||||
description = "Marks an item as completed on the shopping list"
|
||||
slot_schema = {"item": cv.string}
|
||||
platforms = {DOMAIN}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
item = slots["item"]["value"].strip()
|
||||
|
||||
try:
|
||||
complete_items = await intent_obj.hass.data[DOMAIN].async_complete(item)
|
||||
except NoMatchingShoppingListItem:
|
||||
complete_items = []
|
||||
|
||||
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech_slots({"completed_items": complete_items})
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ListTopItemsIntent(intent.IntentHandler):
|
||||
"""Handle AddItem intents."""
|
||||
|
||||
@@ -76,7 +47,7 @@ class ListTopItemsIntent(intent.IntentHandler):
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
items = intent_obj.hass.data[DOMAIN].items[-5:]
|
||||
response: intent.IntentResponse = intent_obj.create_response()
|
||||
response = intent_obj.create_response()
|
||||
|
||||
if not items:
|
||||
response.async_set_speech("There are no items on your shopping list")
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
"""Common base for entities."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pysmarlaapi import Federwiege
|
||||
from pysmarlaapi.federwiege.classes import Property
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SmarlaEntityDescription(EntityDescription):
|
||||
"""Class describing Swing2Sleep Smarla entities."""
|
||||
|
||||
service: str
|
||||
property: str
|
||||
|
||||
|
||||
class SmarlaBaseEntity(Entity):
|
||||
"""Common Base Entity class for defining Smarla device."""
|
||||
|
||||
entity_description: SmarlaEntityDescription
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None:
|
||||
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
|
||||
"""Initialise the entity."""
|
||||
self.entity_description = desc
|
||||
self._property = federwiege.get_property(desc.service, desc.property)
|
||||
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
|
||||
self._property = prop
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, federwiege.serial_number)},
|
||||
name=DEVICE_MODEL_NAME,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pysmarlaapi import Federwiege
|
||||
from pysmarlaapi.federwiege.classes import Property
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
@@ -10,13 +11,16 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import FederwiegeConfigEntry
|
||||
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
|
||||
from .entity import SmarlaBaseEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SmarlaSwitchEntityDescription(SmarlaEntityDescription, SwitchEntityDescription):
|
||||
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Class describing Swing2Sleep Smarla switch entity."""
|
||||
|
||||
service: str
|
||||
property: str
|
||||
|
||||
|
||||
SWITCHES: list[SmarlaSwitchEntityDescription] = [
|
||||
SmarlaSwitchEntityDescription(
|
||||
@@ -51,6 +55,17 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||
|
||||
_property: Property[bool]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
federwiege: Federwiege,
|
||||
desc: SmarlaSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Smarla switch."""
|
||||
prop = federwiege.get_property(desc.service, desc.property)
|
||||
super().__init__(federwiege, prop)
|
||||
self.entity_description = desc
|
||||
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
|
||||
@@ -1096,7 +1096,7 @@ UNITS = {
|
||||
"ccf": UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
"lux": LIGHT_LUX,
|
||||
"mG": None,
|
||||
"µg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"kPa": UnitOfPressure.KPA,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.6"],
|
||||
"requirements": ["pysmlight==0.2.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -6,14 +6,9 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -46,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -58,19 +52,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Configure Gammu state machine."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
DEPRECATED_ISSUE_ID,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_config_flow_integration",
|
||||
translation_placeholders={
|
||||
"integration_title": "SMS notifications via GSM-modem",
|
||||
},
|
||||
)
|
||||
|
||||
device = entry.data[CONF_DEVICE]
|
||||
connection_mode = "at"
|
||||
@@ -120,7 +101,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
|
||||
await gateway.terminate_async()
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -7,13 +7,8 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "snips"
|
||||
@@ -96,20 +91,6 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate Snips component."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Snips",
|
||||
},
|
||||
)
|
||||
|
||||
# Make sure MQTT integration is enabled and the client is available
|
||||
if not await mqtt.async_wait_for_mqtt_client(hass):
|
||||
|
||||
@@ -130,11 +130,11 @@ async def async_generate_speaker_info(
|
||||
value = getattr(speaker, attrib)
|
||||
payload[attrib] = get_contents(value)
|
||||
|
||||
payload["enabled_entities"] = sorted(
|
||||
payload["enabled_entities"] = {
|
||||
entity_id
|
||||
for entity_id, s in hass.data[DATA_SONOS].entity_id_mappings.items()
|
||||
if s is speaker
|
||||
)
|
||||
}
|
||||
payload["media"] = await async_generate_media_info(hass, speaker)
|
||||
payload["activity_stats"] = speaker.activity_stats.report()
|
||||
payload["event_stats"] = speaker.event_stats.report()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sql",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.0"]
|
||||
"requirements": ["SQLAlchemy==2.0.40", "sqlparse==0.5.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0", "av==13.1.0", "numpy==2.2.6"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5", "av==13.1.0", "numpy==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
# as we will always load it and we do not want to have
|
||||
# to wait for the import executor when its busy later
|
||||
# in the startup process.
|
||||
from . import (
|
||||
binary_sensor as binary_sensor_pre_import, # noqa: F401
|
||||
sensor as sensor_pre_import, # noqa: F401
|
||||
)
|
||||
from . import sensor as sensor_pre_import # noqa: F401
|
||||
from .const import ( # noqa: F401 # noqa: F401
|
||||
DOMAIN,
|
||||
STATE_ABOVE_HORIZON,
|
||||
@@ -27,8 +24,6 @@ from .const import ( # noqa: F401 # noqa: F401
|
||||
)
|
||||
from .entity import Sun, SunConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -57,12 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
|
||||
await component.async_add_entities([sun])
|
||||
entry.runtime_data = sun
|
||||
entry.async_on_unload(sun.remove_listeners)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
entry, [Platform.SENSOR]
|
||||
):
|
||||
await entry.runtime_data.async_remove()
|
||||
return unload_ok
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
"""Binary Sensor platform for Sun integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED
|
||||
from .entity import Sun, SunConfigEntry
|
||||
|
||||
ENTITY_ID_BINARY_SENSOR_FORMAT = BINARY_SENSOR_DOMAIN + ".sun_{}"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class SunBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes a Sun binary sensor entity."""
|
||||
|
||||
value_fn: Callable[[Sun], bool | None]
|
||||
signal: str
|
||||
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[SunBinarySensorEntityDescription, ...] = (
|
||||
SunBinarySensorEntityDescription(
|
||||
key="solar_rising",
|
||||
translation_key="solar_rising",
|
||||
value_fn=lambda data: data.rising,
|
||||
entity_registry_enabled_default=False,
|
||||
signal=SIGNAL_EVENTS_CHANGED,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: SunConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sun binary sensor platform."""
|
||||
|
||||
sun = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SunBinarySensor(sun, description, entry.entry_id)
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SunBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a Sun binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: SunBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sun: Sun,
|
||||
entity_description: SunBinarySensorEntityDescription,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initiate Sun Binary Sensor."""
|
||||
self.entity_description = entity_description
|
||||
self.entity_id = ENTITY_ID_BINARY_SENSOR_FORMAT.format(entity_description.key)
|
||||
self._attr_unique_id = f"{entry_id}-{entity_description.key}"
|
||||
self.sun = sun
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name="Sun",
|
||||
identifiers={(DOMAIN, entry_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return value of binary sensor."""
|
||||
return self.entity_description.value_fn(self.sun)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register signal listener when added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self.entity_description.signal,
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
@@ -28,15 +28,6 @@
|
||||
"solar_rising": {
|
||||
"default": "mdi:sun-clock"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"solar_rising": {
|
||||
"default": "mdi:weather-sunny-off",
|
||||
"state": {
|
||||
"on": "mdi:weather-sunset-up",
|
||||
"off": "mdi:weather-sunset-down"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,15 +27,6 @@
|
||||
"solar_azimuth": { "name": "Solar azimuth" },
|
||||
"solar_elevation": { "name": "Solar elevation" },
|
||||
"solar_rising": { "name": "Solar rising" }
|
||||
},
|
||||
"binary_sensor": {
|
||||
"solar_rising": {
|
||||
"name": "Solar rising",
|
||||
"state": {
|
||||
"on": "Rising",
|
||||
"off": "Setting"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN as SWITCH_DOMAIN
|
||||
|
||||
DEFAULT_NAME = "Light Switch"
|
||||
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class LightSwitch(LightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Forward the turn_on command to the switch in this light switch."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: self._switch_entity_id},
|
||||
blocking=True,
|
||||
@@ -86,7 +86,7 @@ class LightSwitch(LightEntity):
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Forward the turn_off command to the switch in this light switch."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: self._switch_entity_id},
|
||||
blocking=True,
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN as SWITCH_AS_X_DOMAIN
|
||||
|
||||
|
||||
class BaseEntity(Entity):
|
||||
@@ -61,7 +61,7 @@ class BaseEntity(Entity):
|
||||
self._switch_entity_id = switch_entity_id
|
||||
|
||||
self._is_new_entity = (
|
||||
registry.async_get_entity_id(domain, DOMAIN, unique_id) is None
|
||||
registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -102,7 +102,7 @@ class BaseEntity(Entity):
|
||||
if registry.async_get(self.entity_id) is not None:
|
||||
registry.async_update_entity_options(
|
||||
self.entity_id,
|
||||
DOMAIN,
|
||||
SWITCH_AS_X_DOMAIN,
|
||||
self.async_generate_entity_options(),
|
||||
)
|
||||
|
||||
|
||||
@@ -7,13 +7,7 @@ from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
|
||||
from aiohttp import web
|
||||
from switchbot_api import (
|
||||
Device,
|
||||
Remote,
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -181,12 +175,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api = SwitchBotAPI(token=token, secret=secret)
|
||||
try:
|
||||
devices = await api.list_devices()
|
||||
except SwitchBotAuthenticationError as ex:
|
||||
except InvalidAuth as ex:
|
||||
_LOGGER.error(
|
||||
"Invalid authentication while connecting to SwitchBot API: %s", ex
|
||||
)
|
||||
return False
|
||||
except SwitchBotConnectionError as ex:
|
||||
except CannotConnect as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
_LOGGER.debug("Devices: %s", devices)
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator] = {}
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import (
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -40,9 +36,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await SwitchBotAPI(
|
||||
token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY]
|
||||
).list_devices()
|
||||
except SwitchBotConnectionError:
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except SwitchBotAuthenticationError:
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -4,7 +4,7 @@ from asyncio import timeout
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError
|
||||
from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]):
|
||||
status: Status = await self._api.get_status(self._device_id)
|
||||
_LOGGER.debug("Refreshing %s with %s", self._device_id, status)
|
||||
return status
|
||||
except SwitchBotConnectionError as err:
|
||||
except CannotConnect as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.4.0"]
|
||||
"requirements": ["switchbot-api==2.3.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiotedee"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotedee==0.2.23"]
|
||||
"requirements": ["aiotedee==0.2.20"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,924 +0,0 @@
|
||||
"""Telegram bot classes and utilities."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from telegram import (
|
||||
Bot,
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
Update,
|
||||
User,
|
||||
)
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import CallbackContext, filters
|
||||
from telegram.request import HTTPXRequest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_COMMAND,
|
||||
CONF_API_KEY,
|
||||
HTTP_BEARER_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
|
||||
|
||||
from .const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_AUTHENTICATION,
|
||||
ATTR_CAPTION,
|
||||
ATTR_CHAT_ID,
|
||||
ATTR_CHAT_INSTANCE,
|
||||
ATTR_DATA,
|
||||
ATTR_DATE,
|
||||
ATTR_DISABLE_NOTIF,
|
||||
ATTR_DISABLE_WEB_PREV,
|
||||
ATTR_FILE,
|
||||
ATTR_FROM_FIRST,
|
||||
ATTR_FROM_LAST,
|
||||
ATTR_KEYBOARD,
|
||||
ATTR_KEYBOARD_INLINE,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_MESSAGE_TAG,
|
||||
ATTR_MESSAGE_THREAD_ID,
|
||||
ATTR_MESSAGEID,
|
||||
ATTR_MSG,
|
||||
ATTR_MSGID,
|
||||
ATTR_ONE_TIME_KEYBOARD,
|
||||
ATTR_OPEN_PERIOD,
|
||||
ATTR_PARSER,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_REPLY_TO_MSGID,
|
||||
ATTR_REPLYMARKUP,
|
||||
ATTR_RESIZE_KEYBOARD,
|
||||
ATTR_STICKER_ID,
|
||||
ATTR_TEXT,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TITLE,
|
||||
ATTR_URL,
|
||||
ATTR_USER_ID,
|
||||
ATTR_USERNAME,
|
||||
ATTR_VERIFY_SSL,
|
||||
CONF_CHAT_ID,
|
||||
CONF_PROXY_PARAMS,
|
||||
CONF_PROXY_URL,
|
||||
DOMAIN,
|
||||
EVENT_TELEGRAM_CALLBACK,
|
||||
EVENT_TELEGRAM_COMMAND,
|
||||
EVENT_TELEGRAM_SENT,
|
||||
EVENT_TELEGRAM_TEXT,
|
||||
PARSER_HTML,
|
||||
PARSER_MD,
|
||||
PARSER_MD2,
|
||||
PARSER_PLAIN_TEXT,
|
||||
SERVICE_EDIT_CAPTION,
|
||||
SERVICE_EDIT_MESSAGE,
|
||||
SERVICE_SEND_ANIMATION,
|
||||
SERVICE_SEND_DOCUMENT,
|
||||
SERVICE_SEND_PHOTO,
|
||||
SERVICE_SEND_STICKER,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type TelegramBotConfigEntry = ConfigEntry[TelegramNotificationService]
|
||||
|
||||
|
||||
class BaseTelegramBot:
|
||||
"""The base class for the telegram bot."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TelegramBotConfigEntry) -> None:
|
||||
"""Initialize the bot base class."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the bot application."""
|
||||
|
||||
async def handle_update(self, update: Update, context: CallbackContext) -> bool:
|
||||
"""Handle updates from bot application set up by the respective platform."""
|
||||
_LOGGER.debug("Handling update %s", update)
|
||||
if not self.authorize_update(update):
|
||||
return False
|
||||
|
||||
# establish event type: text, command or callback_query
|
||||
if update.callback_query:
|
||||
# NOTE: Check for callback query first since effective message will be populated with the message
|
||||
# in .callback_query (python-telegram-bot docs are wrong)
|
||||
event_type, event_data = self._get_callback_query_event_data(
|
||||
update.callback_query
|
||||
)
|
||||
elif update.effective_message:
|
||||
event_type, event_data = self._get_message_event_data(
|
||||
update.effective_message
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("Unhandled update: %s", update)
|
||||
return True
|
||||
|
||||
event_context = Context()
|
||||
|
||||
_LOGGER.debug("Firing event %s: %s", event_type, event_data)
|
||||
self.hass.bus.async_fire(event_type, event_data, context=event_context)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_command_event_data(command_text: str | None) -> dict[str, str | list]:
|
||||
if not command_text or not command_text.startswith("/"):
|
||||
return {}
|
||||
command_parts = command_text.split()
|
||||
command = command_parts[0]
|
||||
args = command_parts[1:]
|
||||
return {ATTR_COMMAND: command, ATTR_ARGS: args}
|
||||
|
||||
def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]]:
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_MSGID: message.message_id,
|
||||
ATTR_CHAT_ID: message.chat.id,
|
||||
ATTR_DATE: message.date,
|
||||
ATTR_MESSAGE_THREAD_ID: message.message_thread_id,
|
||||
}
|
||||
if filters.COMMAND.filter(message):
|
||||
# This is a command message - set event type to command and split data into command and args
|
||||
event_type = EVENT_TELEGRAM_COMMAND
|
||||
event_data.update(self._get_command_event_data(message.text))
|
||||
else:
|
||||
event_type = EVENT_TELEGRAM_TEXT
|
||||
event_data[ATTR_TEXT] = message.text
|
||||
|
||||
if message.from_user:
|
||||
event_data.update(self._get_user_event_data(message.from_user))
|
||||
|
||||
return event_type, event_data
|
||||
|
||||
def _get_user_event_data(self, user: User) -> dict[str, Any]:
|
||||
return {
|
||||
ATTR_USER_ID: user.id,
|
||||
ATTR_FROM_FIRST: user.first_name,
|
||||
ATTR_FROM_LAST: user.last_name,
|
||||
}
|
||||
|
||||
def _get_callback_query_event_data(
|
||||
self, callback_query: CallbackQuery
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
event_type = EVENT_TELEGRAM_CALLBACK
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_MSGID: callback_query.id,
|
||||
ATTR_CHAT_INSTANCE: callback_query.chat_instance,
|
||||
ATTR_DATA: callback_query.data,
|
||||
ATTR_MSG: None,
|
||||
ATTR_CHAT_ID: None,
|
||||
}
|
||||
if callback_query.message:
|
||||
event_data[ATTR_MSG] = callback_query.message.to_dict()
|
||||
event_data[ATTR_CHAT_ID] = callback_query.message.chat.id
|
||||
|
||||
if callback_query.from_user:
|
||||
event_data.update(self._get_user_event_data(callback_query.from_user))
|
||||
|
||||
# Split data into command and args if possible
|
||||
event_data.update(self._get_command_event_data(callback_query.data))
|
||||
|
||||
return event_type, event_data
|
||||
|
||||
def authorize_update(self, update: Update) -> bool:
|
||||
"""Make sure either user or chat is in allowed_chat_ids."""
|
||||
from_user = update.effective_user.id if update.effective_user else None
|
||||
from_chat = update.effective_chat.id if update.effective_chat else None
|
||||
allowed_chat_ids: list[int] = [
|
||||
subentry.data[CONF_CHAT_ID] for subentry in self.config.subentries.values()
|
||||
]
|
||||
if from_user in allowed_chat_ids or from_chat in allowed_chat_ids:
|
||||
return True
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Unauthorized update - neither user id %s nor chat id %s is in allowed"
|
||||
" chats: %s"
|
||||
),
|
||||
from_user,
|
||||
from_chat,
|
||||
allowed_chat_ids,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class TelegramNotificationService:
|
||||
"""Implement the notification services for the Telegram Bot domain."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
app: BaseTelegramBot,
|
||||
bot: Bot,
|
||||
config: TelegramBotConfigEntry,
|
||||
parser: str,
|
||||
) -> None:
|
||||
"""Initialize the service."""
|
||||
self.app = app
|
||||
self.config = config
|
||||
self._parsers = {
|
||||
PARSER_HTML: ParseMode.HTML,
|
||||
PARSER_MD: ParseMode.MARKDOWN,
|
||||
PARSER_MD2: ParseMode.MARKDOWN_V2,
|
||||
PARSER_PLAIN_TEXT: None,
|
||||
}
|
||||
self._parse_mode = self._parsers.get(parser)
|
||||
self.bot = bot
|
||||
self.hass = hass
|
||||
|
||||
def _get_allowed_chat_ids(self) -> list[int]:
|
||||
allowed_chat_ids: list[int] = [
|
||||
subentry.data[CONF_CHAT_ID] for subentry in self.config.subentries.values()
|
||||
]
|
||||
|
||||
if not allowed_chat_ids:
|
||||
bot_name: str = self.config.title
|
||||
raise ServiceValidationError(
|
||||
"No allowed chat IDs found for bot",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_allowed_chat_ids",
|
||||
translation_placeholders={
|
||||
"bot_name": bot_name,
|
||||
},
|
||||
)
|
||||
|
||||
return allowed_chat_ids
|
||||
|
||||
def _get_last_message_id(self):
|
||||
return dict.fromkeys(self._get_allowed_chat_ids())
|
||||
|
||||
def _get_msg_ids(self, msg_data, chat_id):
|
||||
"""Get the message id to edit.
|
||||
|
||||
This can be one of (message_id, inline_message_id) from a msg dict,
|
||||
returning a tuple.
|
||||
**You can use 'last' as message_id** to edit
|
||||
the message last sent in the chat_id.
|
||||
"""
|
||||
message_id = inline_message_id = None
|
||||
if ATTR_MESSAGEID in msg_data:
|
||||
message_id = msg_data[ATTR_MESSAGEID]
|
||||
if (
|
||||
isinstance(message_id, str)
|
||||
and (message_id == "last")
|
||||
and (self._get_last_message_id()[chat_id] is not None)
|
||||
):
|
||||
message_id = self._get_last_message_id()[chat_id]
|
||||
else:
|
||||
inline_message_id = msg_data["inline_message_id"]
|
||||
return message_id, inline_message_id
|
||||
|
||||
def _get_target_chat_ids(self, target):
|
||||
"""Validate chat_id targets or return default target (first).
|
||||
|
||||
:param target: optional list of integers ([12234, -12345])
|
||||
:return list of chat_id targets (integers)
|
||||
"""
|
||||
allowed_chat_ids: list[int] = self._get_allowed_chat_ids()
|
||||
default_user: int = allowed_chat_ids[0]
|
||||
if target is not None:
|
||||
if isinstance(target, int):
|
||||
target = [target]
|
||||
chat_ids = [t for t in target if t in allowed_chat_ids]
|
||||
if chat_ids:
|
||||
return chat_ids
|
||||
_LOGGER.warning(
|
||||
"Disallowed targets: %s, using default: %s", target, default_user
|
||||
)
|
||||
return [default_user]
|
||||
|
||||
def _get_msg_kwargs(self, data):
|
||||
"""Get parameters in message data kwargs."""
|
||||
|
||||
def _make_row_inline_keyboard(row_keyboard):
|
||||
"""Make a list of InlineKeyboardButtons.
|
||||
|
||||
It can accept:
|
||||
- a list of tuples like:
|
||||
`[(text_b1, data_callback_b1),
|
||||
(text_b2, data_callback_b2), ...]
|
||||
- a string like: `/cmd1, /cmd2, /cmd3`
|
||||
- or a string like: `text_b1:/cmd1, text_b2:/cmd2`
|
||||
- also supports urls instead of callback commands
|
||||
"""
|
||||
buttons = []
|
||||
if isinstance(row_keyboard, str):
|
||||
for key in row_keyboard.split(","):
|
||||
if ":/" in key:
|
||||
# check if command or URL
|
||||
if key.startswith("https://"):
|
||||
label = key.split(",")[0]
|
||||
url = key[len(label) + 1 :]
|
||||
buttons.append(InlineKeyboardButton(label, url=url))
|
||||
else:
|
||||
# commands like: 'Label:/cmd' become ('Label', '/cmd')
|
||||
label = key.split(":/")[0]
|
||||
command = key[len(label) + 1 :]
|
||||
buttons.append(
|
||||
InlineKeyboardButton(label, callback_data=command)
|
||||
)
|
||||
else:
|
||||
# commands like: '/cmd' become ('CMD', '/cmd')
|
||||
label = key.strip()[1:].upper()
|
||||
buttons.append(InlineKeyboardButton(label, callback_data=key))
|
||||
elif isinstance(row_keyboard, list):
|
||||
for entry in row_keyboard:
|
||||
text_btn, data_btn = entry
|
||||
if data_btn.startswith("https://"):
|
||||
buttons.append(InlineKeyboardButton(text_btn, url=data_btn))
|
||||
else:
|
||||
buttons.append(
|
||||
InlineKeyboardButton(text_btn, callback_data=data_btn)
|
||||
)
|
||||
else:
|
||||
raise TypeError(str(row_keyboard))
|
||||
return buttons
|
||||
|
||||
# Defaults
|
||||
params = {
|
||||
ATTR_PARSER: self._parse_mode,
|
||||
ATTR_DISABLE_NOTIF: False,
|
||||
ATTR_DISABLE_WEB_PREV: None,
|
||||
ATTR_REPLY_TO_MSGID: None,
|
||||
ATTR_REPLYMARKUP: None,
|
||||
ATTR_TIMEOUT: None,
|
||||
ATTR_MESSAGE_TAG: None,
|
||||
ATTR_MESSAGE_THREAD_ID: None,
|
||||
}
|
||||
if data is not None:
|
||||
if ATTR_PARSER in data:
|
||||
params[ATTR_PARSER] = self._parsers.get(
|
||||
data[ATTR_PARSER], self._parse_mode
|
||||
)
|
||||
if ATTR_TIMEOUT in data:
|
||||
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
|
||||
if ATTR_DISABLE_NOTIF in data:
|
||||
params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF]
|
||||
if ATTR_DISABLE_WEB_PREV in data:
|
||||
params[ATTR_DISABLE_WEB_PREV] = data[ATTR_DISABLE_WEB_PREV]
|
||||
if ATTR_REPLY_TO_MSGID in data:
|
||||
params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID]
|
||||
if ATTR_MESSAGE_TAG in data:
|
||||
params[ATTR_MESSAGE_TAG] = data[ATTR_MESSAGE_TAG]
|
||||
if ATTR_MESSAGE_THREAD_ID in data:
|
||||
params[ATTR_MESSAGE_THREAD_ID] = data[ATTR_MESSAGE_THREAD_ID]
|
||||
# Keyboards:
|
||||
if ATTR_KEYBOARD in data:
|
||||
keys = data.get(ATTR_KEYBOARD)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
if keys:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardMarkup(
|
||||
[[key.strip() for key in row.split(",")] for row in keys],
|
||||
resize_keyboard=data.get(ATTR_RESIZE_KEYBOARD, False),
|
||||
one_time_keyboard=data.get(ATTR_ONE_TIME_KEYBOARD, False),
|
||||
)
|
||||
else:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True)
|
||||
|
||||
elif ATTR_KEYBOARD_INLINE in data:
|
||||
keys = data.get(ATTR_KEYBOARD_INLINE)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
|
||||
[_make_row_inline_keyboard(row) for row in keys]
|
||||
)
|
||||
return params
|
||||
|
||||
async def _send_msg(
|
||||
self, func_send, msg_error, message_tag, *args_msg, context=None, **kwargs_msg
|
||||
):
|
||||
"""Send one message."""
|
||||
try:
|
||||
out = await func_send(*args_msg, **kwargs_msg)
|
||||
if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID):
|
||||
chat_id = out.chat_id
|
||||
message_id = out[ATTR_MESSAGEID]
|
||||
self._get_last_message_id()[chat_id] = message_id
|
||||
_LOGGER.debug(
|
||||
"Last message ID: %s (from chat_id %s)",
|
||||
self._get_last_message_id(),
|
||||
chat_id,
|
||||
)
|
||||
|
||||
event_data = {
|
||||
ATTR_CHAT_ID: chat_id,
|
||||
ATTR_MESSAGEID: message_id,
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
|
||||
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
|
||||
ATTR_MESSAGE_THREAD_ID
|
||||
]
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TELEGRAM_SENT, event_data, context=context
|
||||
)
|
||||
elif not isinstance(out, bool):
|
||||
_LOGGER.warning(
|
||||
"Update last message: out_type:%s, out=%s", type(out), out
|
||||
)
|
||||
except TelegramError as exc:
|
||||
_LOGGER.error(
|
||||
"%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg
|
||||
)
|
||||
return None
|
||||
return out
|
||||
|
||||
async def send_message(self, message="", target=None, context=None, **kwargs):
|
||||
"""Send a message to one or multiple pre-allowed chat IDs."""
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_message,
|
||||
"Error sending message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id,
|
||||
text,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
if msg is not None:
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def delete_message(self, chat_id=None, context=None, **kwargs):
|
||||
"""Delete a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, _ = self._get_msg_ids(kwargs, chat_id)
|
||||
_LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id)
|
||||
deleted = await self._send_msg(
|
||||
self.bot.delete_message,
|
||||
"Error deleting message",
|
||||
None,
|
||||
chat_id,
|
||||
message_id,
|
||||
context=context,
|
||||
)
|
||||
# reduce message_id anyway:
|
||||
if self._get_last_message_id()[chat_id] is not None:
|
||||
# change last msg_id for deque(n_msgs)?
|
||||
self._get_last_message_id()[chat_id] -= 1
|
||||
return deleted
|
||||
|
||||
async def edit_message(self, type_edit, chat_id=None, context=None, **kwargs):
|
||||
"""Edit a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Edit message %s in chat ID %s with params: %s",
|
||||
message_id or inline_message_id,
|
||||
chat_id,
|
||||
params,
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_MESSAGE:
|
||||
message = kwargs.get(ATTR_MESSAGE)
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
_LOGGER.debug("Editing message with ID %s", message_id or inline_message_id)
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_text,
|
||||
"Error editing text message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
text,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_CAPTION:
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_caption,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
context=context,
|
||||
)
|
||||
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_reply_markup,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def answer_callback_query(
|
||||
self, message, callback_query_id, show_alert=False, context=None, **kwargs
|
||||
):
|
||||
"""Answer a callback originated with a press in an inline keyboard."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Answer callback query with callback ID %s: %s, alert: %s",
|
||||
callback_query_id,
|
||||
message,
|
||||
show_alert,
|
||||
)
|
||||
await self._send_msg(
|
||||
self.bot.answer_callback_query,
|
||||
"Error sending answer callback query",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
callback_query_id,
|
||||
text=message,
|
||||
show_alert=show_alert,
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def send_file(
|
||||
self, file_type=SERVICE_SEND_PHOTO, target=None, context=None, **kwargs
|
||||
):
|
||||
"""Send a photo, sticker, video, or document."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
file_content = await load_data(
|
||||
self.hass,
|
||||
url=kwargs.get(ATTR_URL),
|
||||
filepath=kwargs.get(ATTR_FILE),
|
||||
username=kwargs.get(ATTR_USERNAME),
|
||||
password=kwargs.get(ATTR_PASSWORD),
|
||||
authentication=kwargs.get(ATTR_AUTHENTICATION),
|
||||
verify_ssl=(
|
||||
get_default_context()
|
||||
if kwargs.get(ATTR_VERIFY_SSL, False)
|
||||
else get_default_no_verify_context()
|
||||
),
|
||||
)
|
||||
|
||||
msg_ids = {}
|
||||
if file_content:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Sending file to chat ID %s", chat_id)
|
||||
|
||||
if file_type == SERVICE_SEND_PHOTO:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_photo,
|
||||
"Error sending photo",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
photo=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_STICKER:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=file_content,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_VIDEO:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_video,
|
||||
"Error sending video",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
video=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_DOCUMENT:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_document,
|
||||
"Error sending document",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
document=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_VOICE:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_voice,
|
||||
"Error sending voice",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
voice=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_ANIMATION:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_animation,
|
||||
"Error sending animation",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
animation=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
msg_ids[chat_id] = msg.id
|
||||
file_content.seek(0)
|
||||
else:
|
||||
_LOGGER.error("Can't send file with kwargs: %s", kwargs)
|
||||
|
||||
return msg_ids
|
||||
|
||||
async def send_sticker(self, target=None, context=None, **kwargs) -> dict:
|
||||
"""Send a sticker from a telegram sticker pack."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
stickerid = kwargs.get(ATTR_STICKER_ID)
|
||||
|
||||
msg_ids = {}
|
||||
if stickerid:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=stickerid,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
return await self.send_file(SERVICE_SEND_STICKER, target, **kwargs)
|
||||
|
||||
async def send_location(
|
||||
self, latitude, longitude, target=None, context=None, **kwargs
|
||||
):
|
||||
"""Send a location."""
|
||||
latitude = float(latitude)
|
||||
longitude = float(longitude)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug(
|
||||
"Send location %s/%s to chat ID %s", latitude, longitude, chat_id
|
||||
)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_location,
|
||||
"Error sending location",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def send_poll(
|
||||
self,
|
||||
question,
|
||||
options,
|
||||
is_anonymous,
|
||||
allows_multiple_answers,
|
||||
target=None,
|
||||
context=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Send a poll."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
openperiod = kwargs.get(ATTR_OPEN_PERIOD)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_poll,
|
||||
"Error sending poll",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
question=question,
|
||||
options=options,
|
||||
is_anonymous=is_anonymous,
|
||||
allows_multiple_answers=allows_multiple_answers,
|
||||
open_period=openperiod,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def leave_chat(self, chat_id=None, context=None, **kwargs):
|
||||
"""Remove bot from chat."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
_LOGGER.debug("Leave from chat ID %s", chat_id)
|
||||
return await self._send_msg(
|
||||
self.bot.leave_chat, "Error leaving chat", None, chat_id, context=context
|
||||
)
|
||||
|
||||
|
||||
def initialize_bot(hass: HomeAssistant, p_config: MappingProxyType[str, Any]) -> Bot:
|
||||
"""Initialize telegram bot with proxy support."""
|
||||
api_key: str = p_config[CONF_API_KEY]
|
||||
proxy_url: str | None = p_config.get(CONF_PROXY_URL)
|
||||
proxy_params: dict | None = p_config.get(CONF_PROXY_PARAMS)
|
||||
|
||||
if proxy_url is not None:
|
||||
auth = None
|
||||
if proxy_params is None:
|
||||
# CONF_PROXY_PARAMS has been kept for backwards compatibility.
|
||||
proxy_params = {}
|
||||
elif "username" in proxy_params and "password" in proxy_params:
|
||||
# Auth can actually be stuffed into the URL, but the docs have previously
|
||||
# indicated to put them here.
|
||||
auth = proxy_params.pop("username"), proxy_params.pop("password")
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_auth_deprecation",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_persistent=False,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_placeholders={
|
||||
"proxy_params": CONF_PROXY_PARAMS,
|
||||
"proxy_url": CONF_PROXY_URL,
|
||||
"telegram_bot": "Telegram bot",
|
||||
},
|
||||
translation_key="proxy_params_auth_deprecation",
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/112778",
|
||||
)
|
||||
else:
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_deprecation",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_persistent=False,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_placeholders={
|
||||
"proxy_params": CONF_PROXY_PARAMS,
|
||||
"proxy_url": CONF_PROXY_URL,
|
||||
"httpx": "httpx",
|
||||
"telegram_bot": "Telegram bot",
|
||||
},
|
||||
translation_key="proxy_params_deprecation",
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/112778",
|
||||
)
|
||||
proxy = httpx.Proxy(proxy_url, auth=auth, **proxy_params)
|
||||
request = HTTPXRequest(connection_pool_size=8, proxy=proxy)
|
||||
else:
|
||||
request = HTTPXRequest(connection_pool_size=8)
|
||||
return Bot(token=api_key, request=request)
|
||||
|
||||
|
||||
async def load_data(
|
||||
hass: HomeAssistant,
|
||||
url=None,
|
||||
filepath=None,
|
||||
username=None,
|
||||
password=None,
|
||||
authentication=None,
|
||||
num_retries=5,
|
||||
verify_ssl=None,
|
||||
):
|
||||
"""Load data into ByteIO/File container from a source."""
|
||||
try:
|
||||
if url is not None:
|
||||
# Load data from URL
|
||||
params: dict[str, Any] = {}
|
||||
headers = {}
|
||||
if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
|
||||
headers = {"Authorization": f"Bearer {password}"}
|
||||
elif username is not None and password is not None:
|
||||
if authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
params["auth"] = httpx.DigestAuth(username, password)
|
||||
else:
|
||||
params["auth"] = httpx.BasicAuth(username, password)
|
||||
if verify_ssl is not None:
|
||||
params["verify"] = verify_ssl
|
||||
|
||||
retry_num = 0
|
||||
async with httpx.AsyncClient(
|
||||
timeout=15, headers=headers, **params
|
||||
) as client:
|
||||
while retry_num < num_retries:
|
||||
req = await client.get(url)
|
||||
if req.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
"Status code %s (retry #%s) loading %s",
|
||||
req.status_code,
|
||||
retry_num + 1,
|
||||
url,
|
||||
)
|
||||
else:
|
||||
data = io.BytesIO(req.content)
|
||||
if data.read():
|
||||
data.seek(0)
|
||||
data.name = url
|
||||
return data
|
||||
_LOGGER.warning(
|
||||
"Empty data (retry #%s) in %s)", retry_num + 1, url
|
||||
)
|
||||
retry_num += 1
|
||||
if retry_num < num_retries:
|
||||
await asyncio.sleep(
|
||||
1
|
||||
) # Add a sleep to allow other async operations to proceed
|
||||
_LOGGER.warning(
|
||||
"Can't load data in %s after %s retries", url, retry_num
|
||||
)
|
||||
elif filepath is not None:
|
||||
if hass.config.is_allowed_path(filepath):
|
||||
return await hass.async_add_executor_job(
|
||||
_read_file_as_bytesio, filepath
|
||||
)
|
||||
|
||||
_LOGGER.warning("'%s' are not secure to load data from!", filepath)
|
||||
else:
|
||||
_LOGGER.warning("Can't load data. No data found in params!")
|
||||
|
||||
except (OSError, TypeError) as error:
|
||||
_LOGGER.error("Can't load data into ByteIO: %s", error)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _read_file_as_bytesio(file_path: str) -> io.BytesIO:
|
||||
"""Read a file and return it as a BytesIO object."""
|
||||
with open(file_path, "rb") as file:
|
||||
data = io.BytesIO(file.read())
|
||||
data.name = file_path
|
||||
return data
|
||||
@@ -1,14 +1,6 @@
|
||||
"""Support for Telegram bot to send messages only."""
|
||||
|
||||
from telegram import Bot
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .bot import BaseTelegramBot, TelegramBotConfigEntry
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant, bot: Bot, config: TelegramBotConfigEntry
|
||||
) -> type[BaseTelegramBot] | None:
|
||||
async def async_setup_platform(hass, bot, config):
|
||||
"""Set up the Telegram broadcast platform."""
|
||||
return None
|
||||
return True
|
||||
|
||||
@@ -1,620 +0,0 @@
|
||||
"""Config flow for Telegram Bot."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from ipaddress import AddressValueError, IPv4Network
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from telegram import Bot, ChatFullInfo
|
||||
from telegram.error import BadRequest, InvalidToken, NetworkError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryData,
|
||||
ConfigSubentryFlow,
|
||||
OptionsFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, CONF_URL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from . import initialize_bot
|
||||
from .bot import TelegramBotConfigEntry
|
||||
from .const import (
|
||||
ATTR_PARSER,
|
||||
BOT_NAME,
|
||||
CONF_ALLOWED_CHAT_IDS,
|
||||
CONF_BOT_COUNT,
|
||||
CONF_CHAT_ID,
|
||||
CONF_PROXY_URL,
|
||||
CONF_TRUSTED_NETWORKS,
|
||||
DEFAULT_TRUSTED_NETWORKS,
|
||||
DOMAIN,
|
||||
ERROR_FIELD,
|
||||
ERROR_MESSAGE,
|
||||
ISSUE_DEPRECATED_YAML,
|
||||
ISSUE_DEPRECATED_YAML_HAS_MORE_PLATFORMS,
|
||||
ISSUE_DEPRECATED_YAML_IMPORT_ISSUE_ERROR,
|
||||
PARSER_HTML,
|
||||
PARSER_MD,
|
||||
PARSER_MD2,
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_POLLING,
|
||||
PLATFORM_WEBHOOKS,
|
||||
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_POLLING,
|
||||
PLATFORM_WEBHOOKS,
|
||||
],
|
||||
translation_key="platforms",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_API_KEY): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_PROXY_URL): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.URL)
|
||||
),
|
||||
}
|
||||
)
|
||||
STEP_RECONFIGURE_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_POLLING,
|
||||
PLATFORM_WEBHOOKS,
|
||||
],
|
||||
translation_key="platforms",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_PROXY_URL): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.URL)
|
||||
),
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
STEP_WEBHOOKS_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_URL): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.URL)
|
||||
),
|
||||
vol.Required(CONF_TRUSTED_NETWORKS): vol.Coerce(str),
|
||||
}
|
||||
)
|
||||
OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
ATTR_PARSER,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[PARSER_MD, PARSER_MD2, PARSER_HTML],
|
||||
translation_key="parsers",
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Options flow for webhooks."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
OPTIONS_SCHEMA,
|
||||
self.config_entry.options,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Telegram."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: TelegramBotConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Create the options flow."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: TelegramBotConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {SUBENTRY_TYPE_ALLOWED_CHAT_IDS: AllowedChatIdsSubEntryFlowHandler}
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Create instance of the config flow."""
|
||||
super().__init__()
|
||||
self._bot: Bot | None = None
|
||||
self._bot_name = "Unknown bot"
|
||||
|
||||
# for passing data between steps
|
||||
self._step_user_data: dict[str, Any] = {}
|
||||
|
||||
# triggered by async_setup() from __init__.py
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import of config entry from configuration.yaml."""
|
||||
|
||||
telegram_bot: str = f"{import_data[CONF_PLATFORM]} Telegram bot"
|
||||
bot_count: int = import_data[CONF_BOT_COUNT]
|
||||
|
||||
import_data[CONF_TRUSTED_NETWORKS] = ",".join(
|
||||
import_data[CONF_TRUSTED_NETWORKS]
|
||||
)
|
||||
try:
|
||||
config_flow_result: ConfigFlowResult = await self.async_step_user(
|
||||
import_data
|
||||
)
|
||||
except AbortFlow:
|
||||
# this happens if the config entry is already imported
|
||||
self._create_issue(ISSUE_DEPRECATED_YAML, telegram_bot, bot_count)
|
||||
raise
|
||||
else:
|
||||
errors: dict[str, str] | None = config_flow_result.get("errors")
|
||||
if errors:
|
||||
error: str = errors.get("base", "unknown")
|
||||
self._create_issue(
|
||||
error,
|
||||
telegram_bot,
|
||||
bot_count,
|
||||
config_flow_result["description_placeholders"],
|
||||
)
|
||||
return self.async_abort(reason="import_failed")
|
||||
|
||||
subentries: list[ConfigSubentryData] = []
|
||||
allowed_chat_ids: list[int] = import_data[CONF_ALLOWED_CHAT_IDS]
|
||||
for chat_id in allowed_chat_ids:
|
||||
chat_name: str = await _async_get_chat_name(self._bot, chat_id)
|
||||
subentry: ConfigSubentryData = ConfigSubentryData(
|
||||
data={CONF_CHAT_ID: chat_id},
|
||||
subentry_type=CONF_ALLOWED_CHAT_IDS,
|
||||
title=chat_name,
|
||||
unique_id=str(chat_id),
|
||||
)
|
||||
subentries.append(subentry)
|
||||
config_flow_result["subentries"] = subentries
|
||||
|
||||
self._create_issue(
|
||||
ISSUE_DEPRECATED_YAML,
|
||||
telegram_bot,
|
||||
bot_count,
|
||||
config_flow_result["description_placeholders"],
|
||||
)
|
||||
return config_flow_result
|
||||
|
||||
def _create_issue(
|
||||
self,
|
||||
issue: str,
|
||||
telegram_bot_type: str,
|
||||
bot_count: int,
|
||||
description_placeholders: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
translation_key: str = (
|
||||
ISSUE_DEPRECATED_YAML
|
||||
if bot_count == 1
|
||||
else ISSUE_DEPRECATED_YAML_HAS_MORE_PLATFORMS
|
||||
)
|
||||
if issue != ISSUE_DEPRECATED_YAML:
|
||||
translation_key = ISSUE_DEPRECATED_YAML_IMPORT_ISSUE_ERROR
|
||||
|
||||
telegram_bot = (
|
||||
description_placeholders.get(BOT_NAME, telegram_bot_type)
|
||||
if description_placeholders
|
||||
else telegram_bot_type
|
||||
)
|
||||
error_field = (
|
||||
description_placeholders.get(ERROR_FIELD, "Unknown error")
|
||||
if description_placeholders
|
||||
else "Unknown error"
|
||||
)
|
||||
error_message = (
|
||||
description_placeholders.get(ERROR_MESSAGE, "Unknown error")
|
||||
if description_placeholders
|
||||
else "Unknown error"
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
ISSUE_DEPRECATED_YAML,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Telegram Bot",
|
||||
"telegram_bot": telegram_bot,
|
||||
ERROR_FIELD: error_field,
|
||||
ERROR_MESSAGE: error_message,
|
||||
},
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/144617",
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow to create a new config entry for a Telegram bot."""
|
||||
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
)
|
||||
|
||||
# prevent duplicates
|
||||
await self.async_set_unique_id(user_input[CONF_API_KEY])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# validate connection to Telegram API
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
bot_name = await self._validate_bot(
|
||||
user_input, errors, description_placeholders
|
||||
)
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
if user_input[CONF_PLATFORM] != PLATFORM_WEBHOOKS:
|
||||
await self._shutdown_bot()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=bot_name,
|
||||
data={
|
||||
CONF_PLATFORM: user_input[CONF_PLATFORM],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_PROXY_URL: user_input.get(CONF_PROXY_URL),
|
||||
},
|
||||
options={
|
||||
# this value may come from yaml import
|
||||
ATTR_PARSER: user_input.get(ATTR_PARSER, PARSER_MD)
|
||||
},
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
self._bot_name = bot_name
|
||||
self._step_user_data.update(user_input)
|
||||
|
||||
if self.source == SOURCE_IMPORT:
|
||||
return await self.async_step_webhooks(
|
||||
{
|
||||
CONF_URL: user_input.get(CONF_URL),
|
||||
CONF_TRUSTED_NETWORKS: user_input[CONF_TRUSTED_NETWORKS],
|
||||
}
|
||||
)
|
||||
return await self.async_step_webhooks()
|
||||
|
||||
async def _shutdown_bot(self) -> None:
|
||||
"""Shutdown the bot if it exists."""
|
||||
if self._bot:
|
||||
await self._bot.shutdown()
|
||||
self._bot = None
|
||||
|
||||
async def _validate_bot(
|
||||
self,
|
||||
user_input: dict[str, Any],
|
||||
errors: dict[str, str],
|
||||
placeholders: dict[str, str],
|
||||
) -> str:
|
||||
try:
|
||||
bot = await self.hass.async_add_executor_job(
|
||||
initialize_bot, self.hass, MappingProxyType(user_input)
|
||||
)
|
||||
self._bot = bot
|
||||
|
||||
user = await bot.get_me()
|
||||
except InvalidToken as err:
|
||||
_LOGGER.warning("Invalid API token")
|
||||
errors["base"] = "invalid_api_key"
|
||||
placeholders[ERROR_FIELD] = "API key"
|
||||
placeholders[ERROR_MESSAGE] = str(err)
|
||||
return "Unknown bot"
|
||||
except (ValueError, NetworkError) as err:
|
||||
_LOGGER.warning("Invalid proxy")
|
||||
errors["base"] = "invalid_proxy_url"
|
||||
placeholders["proxy_url_error"] = str(err)
|
||||
placeholders[ERROR_FIELD] = "proxy url"
|
||||
placeholders[ERROR_MESSAGE] = str(err)
|
||||
return "Unknown bot"
|
||||
else:
|
||||
return user.full_name
|
||||
|
||||
async def async_step_webhooks(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle config flow for webhook Telegram bot."""
|
||||
|
||||
if not user_input:
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return self.async_show_form(
|
||||
step_id="webhooks",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_WEBHOOKS_DATA_SCHEMA,
|
||||
self._get_reconfigure_entry().data,
|
||||
),
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="webhooks",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_WEBHOOKS_DATA_SCHEMA,
|
||||
{
|
||||
CONF_TRUSTED_NETWORKS: ",".join(
|
||||
[str(network) for network in DEFAULT_TRUSTED_NETWORKS]
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {BOT_NAME: self._bot_name}
|
||||
self._validate_webhooks(user_input, errors, description_placeholders)
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="webhooks",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_WEBHOOKS_DATA_SCHEMA,
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
await self._shutdown_bot()
|
||||
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
user_input.update(self._step_user_data)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
title=self._bot_name,
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self._bot_name,
|
||||
data={
|
||||
CONF_PLATFORM: self._step_user_data[CONF_PLATFORM],
|
||||
CONF_API_KEY: self._step_user_data[CONF_API_KEY],
|
||||
CONF_PROXY_URL: self._step_user_data.get(CONF_PROXY_URL),
|
||||
CONF_URL: user_input.get(CONF_URL),
|
||||
CONF_TRUSTED_NETWORKS: user_input[CONF_TRUSTED_NETWORKS],
|
||||
},
|
||||
options={ATTR_PARSER: self._step_user_data.get(ATTR_PARSER, PARSER_MD)},
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
def _validate_webhooks(
|
||||
self,
|
||||
user_input: dict[str, Any],
|
||||
errors: dict[str, str],
|
||||
description_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
# validate URL
|
||||
if CONF_URL in user_input and not user_input[CONF_URL].startswith("https"):
|
||||
errors["base"] = "invalid_url"
|
||||
description_placeholders[ERROR_FIELD] = "URL"
|
||||
description_placeholders[ERROR_MESSAGE] = "URL must start with https"
|
||||
return
|
||||
if CONF_URL not in user_input:
|
||||
try:
|
||||
get_url(self.hass, require_ssl=True, allow_internal=False)
|
||||
except NoURLAvailableError:
|
||||
errors["base"] = "no_url_available"
|
||||
description_placeholders[ERROR_FIELD] = "URL"
|
||||
description_placeholders[ERROR_MESSAGE] = (
|
||||
"URL is required since you have not configured an external URL in Home Assistant"
|
||||
)
|
||||
return
|
||||
|
||||
# validate trusted networks
|
||||
csv_trusted_networks: list[str] = []
|
||||
formatted_trusted_networks: str = (
|
||||
user_input[CONF_TRUSTED_NETWORKS].lstrip("[").rstrip("]")
|
||||
)
|
||||
for trusted_network in cv.ensure_list_csv(formatted_trusted_networks):
|
||||
formatted_trusted_network: str = trusted_network.strip("'")
|
||||
try:
|
||||
IPv4Network(formatted_trusted_network)
|
||||
except (AddressValueError, ValueError) as err:
|
||||
errors["base"] = "invalid_trusted_networks"
|
||||
description_placeholders[ERROR_FIELD] = "trusted networks"
|
||||
description_placeholders[ERROR_MESSAGE] = str(err)
|
||||
return
|
||||
else:
|
||||
csv_trusted_networks.append(formatted_trusted_network)
|
||||
user_input[CONF_TRUSTED_NETWORKS] = csv_trusted_networks
|
||||
|
||||
return
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Reconfigure Telegram bot."""
|
||||
|
||||
api_key: str = self._get_reconfigure_entry().data[CONF_API_KEY]
|
||||
await self.async_set_unique_id(api_key)
|
||||
self._abort_if_unique_id_mismatch()
|
||||
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_RECONFIGURE_USER_DATA_SCHEMA,
|
||||
self._get_reconfigure_entry().data,
|
||||
),
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
user_input[CONF_API_KEY] = api_key
|
||||
bot_name = await self._validate_bot(
|
||||
user_input, errors, description_placeholders
|
||||
)
|
||||
self._bot_name = bot_name
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_RECONFIGURE_USER_DATA_SCHEMA,
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
if user_input[CONF_PLATFORM] != PLATFORM_WEBHOOKS:
|
||||
await self._shutdown_bot()
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), title=bot_name, data_updates=user_input
|
||||
)
|
||||
|
||||
self._step_user_data.update(user_input)
|
||||
return await self.async_step_webhooks()
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Reauth step."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Reauth confirm step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_REAUTH_DATA_SCHEMA, self._get_reauth_entry().data
|
||||
),
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
bot_name = await self._validate_bot(
|
||||
user_input, errors, description_placeholders
|
||||
)
|
||||
await self._shutdown_bot()
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_REAUTH_DATA_SCHEMA, self._get_reauth_entry().data
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), title=bot_name, data_updates=user_input
|
||||
)
|
||||
|
||||
|
||||
class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle a subentry flow for creating chat ID."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Create allowed chat ID."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
config_entry: TelegramBotConfigEntry = self._get_entry()
|
||||
bot = config_entry.runtime_data.bot
|
||||
|
||||
chat_id: int = user_input[CONF_CHAT_ID]
|
||||
chat_name = await _async_get_chat_name(bot, chat_id)
|
||||
if chat_name:
|
||||
return self.async_create_entry(
|
||||
title=chat_name,
|
||||
data={CONF_CHAT_ID: chat_id},
|
||||
unique_id=str(chat_id),
|
||||
)
|
||||
|
||||
errors["base"] = "chat_not_found"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def _async_get_chat_name(bot: Bot | None, chat_id: int) -> str:
|
||||
if not bot:
|
||||
return str(chat_id)
|
||||
|
||||
try:
|
||||
chat_info: ChatFullInfo = await bot.get_chat(chat_id)
|
||||
return chat_info.effective_name or str(chat_id)
|
||||
except BadRequest:
|
||||
return ""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user