Compare commits

...

70 Commits

Author SHA1 Message Date
J. Nick Koston a2a3f09dea use timeout=0 in scan_duration wiring test to avoid pytest-timeout 2026-05-23 00:00:11 -05:00
J. Nick Koston 900f5469bc wire timeout through as scan_duration in async_process_advertisements
so callers like snooz/qingping/xiaomi_ble config flows get an immediate active scan window for the duration of the wait, instead of relying on AUTO-mode scanners to be in active mode by coincidence. habluetooth clamps the effective window to 30s; the timeout still bounds the asyncio wait.
2026-05-22 23:37:02 -05:00
J. Nick Koston 1f541ed9d0 address copilot feedback
forward mode through async_process_advertisements so PASSIVE callers don't accidentally trigger an active scan; mock async_register_active_scan in the new tests instead of poking at the scheduler internals, and parametrize to cover the default scan_duration case.
2026-05-22 23:27:50 -05:00
J. Nick Koston 852c98b5d4 Merge remote-tracking branch 'upstream/dev' into feat/bluetooth-active-scan-window 2026-05-22 23:14:18 -05:00
J. Nick Koston 833e15d6f2 Bump habluetooth to 6.4.0 (#171918) 2026-05-23 00:10:50 -04:00
Matt ee56fd1eb0 Fix two HEOS bugs: host set construction and missing error decorator (#171913) 2026-05-22 18:42:43 -05:00
Felipe Santos e6528bae8a Add missing translation for connection failure on OpenRGB (#171892) 2026-05-22 21:59:39 +02:00
Joost Lekkerkerker a17eb65498 Refactor labs websocket API tests to use async_setup_component (#171891) 2026-05-22 21:53:52 +02:00
Joost Lekkerkerker 912a839d66 Don't call migrate entry in generic thermostat tests directly (#171887) 2026-05-22 21:44:10 +02:00
Martin Hjelmare 4306863729 Fix homekit test_reload flaky test (#171878) 2026-05-22 14:33:27 -05:00
J. Nick Koston 65b2348e99 Merge remote-tracking branch 'upstream/dev' into feat/bluetooth-active-scan-window 2026-05-22 13:54:41 -05:00
J. Nick Koston 1f97b39bad cleanup 2026-05-22 13:54:39 -05:00
Martin Hjelmare ba2f66e751 Remove not needed default force_update in flo (#171854) 2026-05-22 20:15:00 +02:00
Manu 94581d8ab6 Move service registration in System Bridge integration to async_setup (#171761)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-22 18:43:22 +02:00
Ingo Fischer 7d6ec7fc58 Bump matter-python-client to 0.7.1 (#171764)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-05-22 17:34:20 +01:00
Jan Bouwhuis f49de3548e Add MQTT message expiry interval option (#171143) 2026-05-22 18:27:22 +02:00
Manu 49ab42d3a2 Fix dead link in System Bridge service action (#171855) 2026-05-22 17:00:30 +02:00
Franck Nijhof 383f6142f0 Fix ZBT-2 hardware page crash when entry data is missing VID (#171828) 2026-05-22 16:58:01 +02:00
Kamil Breguła 2f120cf604 Fix rgb_color passed as RGBColor NamedTuple instead of plain tuple to light entity turn_on (#171795)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:56:34 +02:00
Franck Nijhof 37288849b3 Register Insteon modem device before platform setup (#171839) 2026-05-22 10:23:47 -04:00
zhangluofeng aa8659f507 Add xthings cloud lock (#171176)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-22 15:54:37 +02:00
DeerMaximum 40c0d79d1d Replaced duplicate constant with homeassistant.const in NINA (#171852) 2026-05-22 15:41:04 +02:00
Franck Nijhof bef8632d78 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 15:23:42 +02:00
Duco Sebel f00decfaa3 Use uptime device class for HomeWizard uptime sensor (#171830) 2026-05-22 15:23:09 +02:00
Manu 42e7add026 Add selector options translations to System Bridge integration (#171771) 2026-05-22 15:22:22 +02:00
Franck Nijhof 263aa3f16e Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 15:18:00 +02:00
mhuiskes 03b364dcf0 Refactor zeversolar tests: use fixtures, patch at use site, add unique_id (#171697) 2026-05-22 14:58:56 +02:00
Duco Sebel 3b1aaf39af Bumb python homewizard energy 10.1.0 (#171826) 2026-05-22 14:51:58 +02:00
Franck Nijhof b82ba43fa4 Add pylint checker for invalid MDI icon references (#171824)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 13:45:40 +02:00
starkillerOG d81ef5593c Bump reolink_aio to 0.20.0: Reolink battery camera support (#171836) 2026-05-22 12:59:33 +02:00
Manu 5c5e50f024 Fix platform unloading in System Bridge integration (#171822) 2026-05-22 12:56:03 +02:00
Lex Postma e796d9c467 Update strings.json to align with HomeWizard app (#171740)
Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com>
2026-05-22 12:54:58 +02:00
Karl Beecken 342f23526f Remove empty requirements_test_all.txt (#171530 follow-up) (#171834) 2026-05-22 12:38:58 +02:00
Erik Montnemery 814ec697cf Remove advanced mode from hue service actions (#171442) 2026-05-22 11:45:33 +02:00
Erik Montnemery 120f1446d4 Rename advanced section to additional options in telegram_bot service actions (#171460) 2026-05-22 11:44:05 +02:00
Franck Nijhof 170af75b7d Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 11:37:05 +02:00
Ariel Ebersberger 5432d29489 Use is/is not for same-enum identity comparisons (tests) (#171689) 2026-05-22 11:32:27 +02:00
Franck Nijhof 8098f4f6bc Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 11:25:28 +02:00
Simone Chemelli 6a70077687 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 11:17:17 +02:00
Max Michels 5dbb0464ba Replace duplicate constants with homeassistant.const imports (#171815) 2026-05-22 11:10:21 +02:00
dependabot[bot] 1df165ea02 Bump j178/prek-action from 2.0.3 to 2.0.4 (#171812)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 10:39:43 +02:00
Manu 62542eb911 Replace duplicate constants with homeassistant.const imports in xiaomi_miio (#171823) 2026-05-22 10:39:09 +02:00
Max Michels a842cac34c Replace duplicate constants with homeassistant.const imports (#171817) 2026-05-22 10:38:06 +02:00
Simone Chemelli 2460f688e3 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 10:34:00 +02:00
Simone Chemelli a868ea443c Fix hardcoded exception strings in uptimerobot (#171744) 2026-05-22 10:33:07 +02:00
Franck Nijhof 1d8565483b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 10:31:10 +02:00
dependabot[bot] 1ef3301253 Bump github/codeql-action from 4.35.4 to 4.35.5 (#171813)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 09:47:16 +02:00
Manu 525952f016 Add entity translations to System Bridge integration (#171807) 2026-05-22 09:00:54 +02:00
Shay Levy 3257275c5a Fix LG webOS TV hardcoded exception strings (#171777) 2026-05-22 08:28:19 +02:00
Max Michels cb54fd4921 Replace duplicate constants with homeassistant.const imports (#171809) 2026-05-22 07:57:08 +02:00
Max Michels b391fc61ea Replace duplicate constants with homeassistant.const imports (#171808) 2026-05-22 07:56:29 +02:00
J. Nick Koston 0b5a8a0426 Merge branch 'dev' into feat/bluetooth-active-scan-window 2026-05-21 23:42:14 -05:00
J. Nick Koston fcd4e4939c Bump habluetooth to 6.2.0 (#171800) 2026-05-21 23:08:17 -05:00
J. Nick Koston d892c76f49 Wire scan_interval and scan_duration into bluetooth.async_register_callback
When an integration registers an ACTIVE callback for a specific
address and declares how often it actually needs an active scan, the
manager now forwards (address, scan_interval, scan_duration) into
habluetooth's async_register_active_scan so AUTO-mode scanners flip
to ACTIVE on demand for that device instead of forcing continuous
active scanning. Without an address in the matcher, or for PASSIVE
mode, the new kwargs are no-ops on the active-scan path and the
callback registration itself is unchanged.
2026-05-21 23:02:00 -05:00
J. Nick Koston deb8b5da05 Parallelize pytest --collect-only in split_tests.py (#171772) 2026-05-21 22:58:01 -04:00
g4bri3lDev c7754a6ce9 Bump py-opendisplay to 7.2.3 (#171775) 2026-05-21 22:52:36 -04:00
J. Nick Koston 242724bd50 Bump aiodiscover to 3.2.3 (#171803) 2026-05-21 22:51:54 -04:00
Max Michels 42454563db Replace duplicate constants with homeassistant.const imports (#171790)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-21 22:51:34 -04:00
J. Nick Koston bf03d0c216 Bump dbus-fast to 5.0.3 (#171595) 2026-05-21 21:11:35 -05:00
Max Michels 568107e06b Replace duplicate constants with homeassistant.const imports (#171784) 2026-05-22 01:33:48 +03:00
Jens Timmerman 7da44428b6 Bump guntamatic to v1.9.0 (#171631) 2026-05-21 22:55:29 +01:00
Max Michels 0a27f31949 Replace duplicate constants with homeassistant.const imports (#171781) 2026-05-21 22:53:07 +01:00
Erwin Douna 905b868c82 Add recreate services to Portainer (#167225)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2026-05-21 22:52:07 +01:00
Max Michels 3187289913 Replace duplicate constants with homeassistant.const imports (#171776) 2026-05-22 00:18:54 +03:00
Max Michels 87cecd4a44 Replace duplicate constants with homeassistant.const imports (#171778) 2026-05-22 00:18:23 +03:00
Robert Svensson fed38b0e38 Replace duplicate ATTR_LOCKED constant with homeassistant.const import in deconz (#171779) 2026-05-22 00:17:22 +03:00
Raphael Hehl 6a36d1260b Bump uiprotect to 10.5.0 (#171768)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 15:42:31 -05:00
Raphael Hehl 49fc1b413d Bump pydantic to 2.13.4 (#171763)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-05-21 14:42:06 -05:00
Abílio Costa bffb0417cc Instruct agents to run prek after doing changes (#171757) 2026-05-21 20:16:26 +01:00
G Johansson 8b8c687fc3 Remove not needed exception handling in dnsip (#171758) 2026-05-21 20:58:32 +02:00
275 changed files with 13534 additions and 1658 deletions
+1
View File
@@ -15,6 +15,7 @@ Dockerfile.dev linguist-language=Dockerfile
# Generated files
CODEOWNERS linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
+1
View File
@@ -25,6 +25,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
+2 -2
View File
@@ -281,7 +281,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -302,7 +302,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
extra-args: --all-files zizmor
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:python"
+1
View File
@@ -15,6 +15,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
- .vscode/tasks.json contains useful commands used for development.
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
## Python Syntax Notes
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
+15 -6
View File
@@ -130,17 +130,26 @@ def async_register_callback(
callback: BluetoothCallback,
match_dict: BluetoothCallbackMatcher | None,
mode: BluetoothScanningMode,
*,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> Callable[[], None]:
"""Register to receive a callback on bluetooth change.
mode is currently not used as we only support active scanning.
Passive scanning will be available in the future. The flag
is required to be present to avoid a future breaking change
when we support passive scanning.
When ``mode`` is not PASSIVE and ``match_dict["address"]`` is set,
the address is registered with habluetooth's active-scan scheduler
so AUTO-mode scanners flip ACTIVE on demand for that device.
``scan_interval`` / ``scan_duration`` default to habluetooth's
DEFAULT_ACTIVE_SCAN_* (5 minutes / 10 seconds) when not provided;
integrations that need a different cadence can pass explicit
values. Without an address in the matcher the active-scan request
is skipped; the callback itself still fires normally.
Returns a callback that can be used to cancel the registration.
"""
return _get_manager(hass).async_register_callback(callback, match_dict)
return _get_manager(hass).async_register_callback(
callback, match_dict, mode, scan_interval, scan_duration
)
async def async_process_advertisements(
@@ -161,7 +170,7 @@ async def async_process_advertisements(
done.set_result(service_info)
unload = _get_manager(hass).async_register_callback(
_async_discovered_device, match_dict
_async_discovered_device, match_dict, mode, scan_duration=timeout
)
try:
+20 -1
View File
@@ -202,6 +202,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
self,
callback: BluetoothCallback,
matcher: BluetoothCallbackMatcher | None,
mode: BluetoothScanningMode = BluetoothScanningMode.ACTIVE,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> Callable[[], None]:
"""Register a callback."""
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
@@ -216,15 +219,31 @@ class HomeAssistantBluetoothManager(BluetoothManager):
connectable = callback_matcher[CONNECTABLE]
self._callback_index.add_callback_matcher(callback_matcher)
# If the matcher targets a specific address and the caller
# didn't explicitly ask for PASSIVE, wire it into habluetooth's
# active-scan scheduler so AUTO-mode scanners flip ACTIVE on
# demand for this device. ``scan_interval``/``scan_duration``
# default to habluetooth's DEFAULT_ACTIVE_SCAN_* when None.
cancel_active_scan: Callable[[], None] | None = None
if (
mode is not BluetoothScanningMode.PASSIVE
and (address := callback_matcher.get(ADDRESS)) is not None
):
cancel_active_scan = self.async_register_active_scan(
address, scan_interval, scan_duration
)
def _async_remove_callback() -> None:
self._callback_index.remove_callback_matcher(callback_matcher)
if cancel_active_scan is not None:
cancel_active_scan()
# If we have history for the subscriber, we can trigger the callback
# immediately with the last packet so the subscriber can see the
# device.
history = self._connectable_history if connectable else self._all_history
service_infos: Iterable[BluetoothServiceInfoBleak] = []
if address := callback_matcher.get(ADDRESS):
if (address := callback_matcher.get(ADDRESS)) is not None:
if service_info := history.get(address):
service_infos = [service_info]
else:
@@ -20,7 +20,7 @@
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.11",
"dbus-fast==5.0.0",
"habluetooth==6.1.0"
"dbus-fast==5.0.3",
"habluetooth==6.4.0"
]
}
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
translation_placeholders={"error": repr(err)},
) from err
except aiocomelit_exceptions.CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+2 -2
View File
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DeconzConfigEntry
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
from .const import ATTR_OFFSET, ATTR_VALVE
from .entity import DeconzDevice
from .hub import DeconzHub
-2
View File
@@ -43,8 +43,6 @@ PLATFORMS = [
]
ATTR_DARK = "dark"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCKED = "locked"
ATTR_OFFSET = "offset"
ATTR_ON = "on"
ATTR_VALVE = "valve"
+1 -1
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodiscover==3.2.0",
"aiodiscover==3.2.3",
"cached-ipaddress==1.0.1"
]
}
+1 -6
View File
@@ -6,7 +6,6 @@ import logging
import aiodns
from aiodns.error import DNSError
from pycares import AresError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
@@ -78,11 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
) from err
errors = [
result
for result in results
if isinstance(
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
)
result for result in results if isinstance(result, (TimeoutError, DNSError))
]
if errors and len(errors) == len(results):
await _close_resolvers()
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
-1
View File
@@ -10,7 +10,6 @@ from .coordinator import FloDeviceDataUpdateCoordinator
class FloEntity(Entity):
"""A base class for Flo entities."""
_attr_force_update = False
_attr_has_entity_name = True
_attr_should_poll = False
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["guntamatic==1.8.0"]
"requirements": ["guntamatic==1.9.0"]
}
+1 -1
View File
@@ -116,7 +116,7 @@ async def _validate_auth(
def _get_current_hosts(entry: HeosConfigEntry) -> set[str]:
"""Get a set of current hosts from the entry."""
hosts = set(entry.data[CONF_HOST])
hosts = {entry.data[CONF_HOST]}
if hasattr(entry, "runtime_data"):
hosts.update(
player.ip_address
@@ -473,6 +473,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
await self.coordinator.heos.set_group(new_members)
return
@catch_action_error("remove from queue")
async def async_remove_from_queue(self, queue_ids: list[int]) -> None:
"""Remove items from the queue."""
await self._player.remove_from_queue(queue_ids)
-2
View File
@@ -2,8 +2,6 @@
from homeassistant.const import Platform
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
ATTR_TIME_PERIOD = "time_period"
ATTR_ONOFF = "on_off"
CONF_CODE = "2fa"
+1 -1
View File
@@ -12,12 +12,12 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
+1 -2
View File
@@ -6,12 +6,11 @@ from typing import Any
from apyhiveapi import Hive
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.const import ATTR_MODE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
@@ -19,7 +19,9 @@ EXPECTED_ENTRY_VERSION = (
@callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info."""
entries = hass.config_entries.async_entries(DOMAIN)
entries = hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
)
return [
HardwareInfo(
board=None,
@@ -13,6 +13,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==10.0.1"],
"requirements": ["python-homewizard-energy==10.1.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}
+2 -10
View File
@@ -35,7 +35,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import DOMAIN
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
@@ -66,13 +65,6 @@ def to_percentage(value: float | None) -> float | None:
return value * 100 if value is not None else None
def uptime_to_datetime(value: int) -> datetime:
"""Convert seconds to datetime timestamp."""
return utcnow().replace(microsecond=0) - timedelta(seconds=value)
uptime_to_stable_datetime = ignore_variance(uptime_to_datetime, timedelta(minutes=5))
SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="smr_version",
@@ -643,7 +635,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="uptime",
translation_key="uptime",
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=(
@@ -651,7 +643,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
),
value_fn=(
lambda data: (
uptime_to_stable_datetime(data.system.uptime_s)
utcnow() - timedelta(seconds=data.system.uptime_s)
if data.system is not None and data.system.uptime_s is not None
else None
)
@@ -61,13 +61,14 @@
},
"select": {
"battery_group_mode": {
"name": "Battery group mode",
"name": "Battery group charging strategy",
"state": {
"predictive": "Smart charging",
"standby": "Standby",
"to_full": "Manual charge mode",
"zero": "Zero mode",
"zero_charge_only": "Zero mode (charge only)",
"zero_discharge_only": "Zero mode (discharge only)"
"to_full": "One-time full charge",
"zero": "Net zero",
"zero_charge_only": "Net zero (charge only)",
"zero_discharge_only": "Net zero (discharge only)"
}
}
},
+13 -12
View File
@@ -31,15 +31,16 @@ activate_scene:
dynamic:
selector:
boolean:
speed:
advanced: true
selector:
number:
min: 0
max: 100
brightness:
advanced: true
selector:
number:
min: 1
max: 255
scene_customization:
collapsed: true
fields:
speed:
selector:
number:
min: 0
max: 100
brightness:
selector:
number:
min: 1
max: 255
+6 -1
View File
@@ -184,7 +184,12 @@
"name": "Transition"
}
},
"name": "Activate Hue scene"
"name": "Activate Hue scene",
"sections": {
"scene_customization": {
"name": "Scene customization"
}
}
},
"hue_activate_scene": {
"description": "Activates a Hue scene stored in the Hue hub.",
@@ -87,6 +87,8 @@ def async_get_triggers(
# Get Hue device id from device identifier
hue_dev_id = get_hue_device_id(device_entry)
if hue_dev_id is None or hue_dev_id not in api.devices:
return []
# extract triggers from all button resources of this Hue device
triggers: list[dict[str, Any]] = []
model_id = api.devices[hue_dev_id].product_data.product_name
+2 -2
View File
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
create_insteon_device(hass, devices.modem, entry.entry_id)
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
for address in devices:
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
register_new_device_callback(hass)
async_setup_services(hass)
create_insteon_device(hass, devices.modem, entry.entry_id)
entry.async_create_background_task(
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
)
@@ -1,7 +1,7 @@
{
"entity": {
"binary_sensor": {
"erev_shabbat_hag": { "default": "mdi:candle-light" },
"erev_shabbat_hag": { "default": "mdi:candle" },
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
"motzei_shabbat_hag": { "default": "mdi:fire" }
},
+1 -1
View File
@@ -7,7 +7,7 @@
"service": "mdi:lock-open"
},
"disable": {
"service": "mdi:fash-off"
"service": "mdi:flash-off"
},
"enable": {
"service": "mdi:flash"
+4 -4
View File
@@ -28,25 +28,25 @@
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
}
},
+1 -1
View File
@@ -241,7 +241,7 @@ def preprocess_turn_on_alternatives(
if (color_name := params.pop(ATTR_COLOR_NAME, None)) is not None:
try:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
params[ATTR_RGB_COLOR] = tuple(color_util.color_name_to_rgb(color_name))
except ValueError:
_LOGGER.warning("Got unknown color %s, falling back to white", color_name)
params[ATTR_RGB_COLOR] = (255, 255, 255)
@@ -3,7 +3,7 @@
from datetime import timedelta
from typing import Any
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -131,7 +131,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Fetch the latest battery status from the bridge."""
status = await self._smartbridge.get_battery_status(self.device_id)
try:
status = await self._smartbridge.get_battery_status(self.device_id)
except BridgeResponseError:
self._attr_is_on = None
return
normalized_status = status.strip().casefold() if status else None
if normalized_status == BATTERY_STATUS_LOW:
self._attr_is_on = True
+1 -1
View File
@@ -60,7 +60,7 @@ def get_matter_device_info(
return None
return MatterDeviceInfo(
unique_id=node.device_info.uniqueID,
unique_id=node.device_info.uniqueID or "",
vendor_id=hex(node.device_info.vendorID),
product_id=hex(node.device_info.productID),
)
@@ -6,13 +6,14 @@ from typing import Any
from chip.clusters import Objects
from matter_server.common.helpers.util import dataclass_to_dict, parse_attribute_path
from homeassistant.components.diagnostics import REDACTED
from homeassistant.components.diagnostics import REDACTED, async_redact_data
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .helpers import MatterConfigEntry, get_matter, get_node_from_device_entry
ATTRIBUTES_TO_REDACT = {Objects.BasicInformation.Attributes.Location}
SERVER_INFO_TO_REDACT = {"wifi_ssid"}
def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]:
@@ -44,6 +45,7 @@ async def async_get_config_entry_diagnostics(
matter = get_matter(hass)
server_diagnostics = await matter.matter_client.get_diagnostics()
data = dataclass_to_dict(server_diagnostics)
data["info"] = async_redact_data(data["info"], SERVER_INFO_TO_REDACT)
nodes = [redact_matter_attributes(node_data) for node_data in data["nodes"]]
data["nodes"] = nodes
@@ -59,7 +61,9 @@ async def async_get_device_diagnostics(
node = get_node_from_device_entry(hass, device)
return {
"server_info": dataclass_to_dict(server_diagnostics.info),
"server_info": async_redact_data(
dataclass_to_dict(server_diagnostics.info), SERVER_INFO_TO_REDACT
),
"node": redact_matter_attributes(
remove_serialization_type(dataclass_to_dict(node.node_data) if node else {})
),
+1 -1
View File
@@ -102,7 +102,7 @@
"default": "mdi:home-lightning-bolt"
},
"eve_weather_trend": {
"default": "mdi:weather",
"default": "mdi:weather-cloudy",
"state": {
"cloudy": "mdi:weather-cloudy",
"rainy": "mdi:weather-rainy",
@@ -8,6 +8,6 @@
"documentation": "https://www.home-assistant.io/integrations/matter",
"integration_type": "hub",
"iot_class": "local_push",
"requirements": ["matter-python-client==0.6.0"],
"requirements": ["matter-python-client==0.7.1"],
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
}
+2 -1
View File
@@ -4,12 +4,13 @@ from dataclasses import dataclass
from typing import cast
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, llm
from .application_credentials import authorization_server_context
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .const import CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .coordinator import ModelContextProtocolCoordinator, TokenManager
from .types import ModelContextProtocolConfigEntry
+2 -8
View File
@@ -13,7 +13,7 @@ from yarl import URL
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_TOKEN, CONF_URL
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -24,13 +24,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import async_get_config_entry_implementation
from .application_credentials import authorization_server_context
from .const import (
CONF_ACCESS_TOKEN,
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
from .const import CONF_AUTHORIZATION_URL, CONF_SCOPE, CONF_TOKEN_URL, DOMAIN
from .coordinator import TokenManager, mcp_client
_LOGGER = logging.getLogger(__name__)
-2
View File
@@ -2,8 +2,6 @@
DOMAIN = "mcp"
# pylint: disable-next=home-assistant-duplicate-const
CONF_ACCESS_TOKEN = "access_token"
CONF_AUTHORIZATION_URL = "authorization_url"
CONF_TOKEN_URL = "token_url"
CONF_SCOPE = "scope"
+1 -5
View File
@@ -41,7 +41,7 @@ from mcp.shared.message import SessionMessage
from homeassistant.components import conversation
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.const import CONF_LLM_HASS_API, CONTENT_TYPE_JSON
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import llm
@@ -56,10 +56,6 @@ _LOGGER = logging.getLogger(__name__)
STREAMABLE_API = "/api/mcp"
TIMEOUT = 60 # Seconds
# Content types
# pylint: disable-next=home-assistant-duplicate-const
CONTENT_TYPE_JSON = "application/json"
# Legacy SSE endpoint
SSE_API = f"/{DOMAIN}/sse"
MESSAGES_API = f"/{DOMAIN}/messages/{{session_id}}"
@@ -108,6 +108,7 @@ ABBREVIATIONS = {
"mode_stat_t": "mode_state_topic",
"mode_stat_tpl": "mode_state_template",
"modes": "modes",
"msg_exp_int": "message_expiry_interval",
"name": "name",
"o": "origin",
"off_dly": "off_delay",
@@ -120,6 +120,8 @@ from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.selector import (
BooleanSelector,
DurationSelector,
DurationSelectorConfig,
FileSelector,
FileSelectorConfig,
NumberSelector,
@@ -227,6 +229,7 @@ from .const import (
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_MAX,
CONF_MAX_KELVIN,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_MIN,
CONF_MIN_KELVIN,
CONF_MODE_COMMAND_TEMPLATE,
@@ -3721,6 +3724,11 @@ MQTT_DEVICE_PLATFORM_FIELDS = {
default=DEFAULT_QOS,
section="mqtt_settings",
),
CONF_MESSAGE_EXPIRY_INTERVAL: PlatformField(
selector=DurationSelector(DurationSelectorConfig(enable_day=True)),
required=False,
section="mqtt_settings",
),
}
+1
View File
@@ -49,6 +49,7 @@ CONF_IMAGE_TOPIC = "image_topic"
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template"
CONF_KEEPALIVE = "keepalive"
CONF_MESSAGE_EXPIRY_INTERVAL = "message_expiry_interval"
CONF_ORIGIN = "origin"
CONF_QOS = ATTR_QOS
CONF_RETAIN = ATTR_RETAIN
+12 -2
View File
@@ -17,7 +17,7 @@ from .models import DATA_MQTT, PublishPayloadType
STORED_MESSAGES = 10
@dataclass
@dataclass(frozen=True, slots=True)
class TimestampedPublishMessage:
"""MQTT Message."""
@@ -26,6 +26,8 @@ class TimestampedPublishMessage:
qos: int
retain: bool
timestamp: float
encoding: str | None
kwargs: dict[str, Any]
def log_message(
@@ -35,6 +37,8 @@ def log_message(
payload: PublishPayloadType,
qos: int,
retain: bool,
encoding: str | None,
**kwargs: Any,
) -> None:
"""Log an outgoing MQTT message."""
entity_info = hass.data[DATA_MQTT].debug_info_entities.setdefault(
@@ -45,7 +49,13 @@ def log_message(
"messages": deque(maxlen=STORED_MESSAGES),
}
msg = TimestampedPublishMessage(
topic, payload, qos, retain, timestamp=time.monotonic()
topic,
payload,
qos,
retain,
timestamp=time.monotonic(),
encoding=encoding,
kwargs=kwargs,
)
entity_info["transmitted"][topic]["messages"].append(msg)
+12 -26
View File
@@ -84,6 +84,7 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
@@ -94,7 +95,6 @@ from .const import (
CONF_SW_VERSION,
CONF_TOPIC,
CONF_VIA_DEVICE,
DEFAULT_ENCODING,
DOMAIN,
MQTT_CONNECTION_STATE,
)
@@ -153,6 +153,8 @@ MQTT_ATTRIBUTES_BLOCKED = {
"unit_of_measurement",
}
PUBLISH_KWARGS = (CONF_MESSAGE_EXPIRY_INTERVAL,)
@callback
def async_handle_schema_error(
@@ -1539,36 +1541,20 @@ class MqttEntity(
await MqttDiscoveryUpdateMixin.async_will_remove_from_hass(self)
debug_info.remove_entity_data(self.hass, self.entity_id)
async def async_publish(
self,
topic: str,
payload: PublishPayloadType,
qos: int = 0,
retain: bool = False,
encoding: str | None = DEFAULT_ENCODING,
) -> None:
"""Publish message to an MQTT topic."""
log_message(self.hass, self.entity_id, topic, payload, qos, retain)
await async_publish(
self.hass,
topic,
payload,
qos,
retain,
encoding,
)
async def async_publish_with_config(
self, topic: str, payload: PublishPayloadType
) -> None:
"""Publish payload to a topic using config."""
await self.async_publish(
topic,
payload,
self._config[CONF_QOS],
self._config[CONF_RETAIN],
self._config[CONF_ENCODING],
kwargs: dict[str, Any] = {
key: value for key, value in self._config.items() if key in PUBLISH_KWARGS
}
qos: int = self._config[CONF_QOS]
retain: bool = self._config[CONF_RETAIN]
encoding: str = self._config[CONF_ENCODING]
log_message(
self.hass, self.entity_id, topic, payload, qos, retain, encoding, **kwargs
)
await async_publish(self.hass, topic, payload, qos, retain, encoding, **kwargs)
@staticmethod
@abstractmethod
+10
View File
@@ -509,10 +509,20 @@ class MqttComponentConfig:
discovery_payload: MQTTDiscoveryPayload
class MessageExpiryInterval(TypedDict, total=False):
"""Hold the Message Expiry Interval."""
days: float
hours: float
minutes: float
seconds: float
class DeviceMqttOptions(TypedDict, total=False):
"""Hold the shared MQTT specific options for an MQTT device."""
qos: int
message_expiry_interval: MessageExpiryInterval
class MqttDeviceData(TypedDict, total=False):
+12
View File
@@ -40,6 +40,7 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
@@ -66,6 +67,7 @@ SHARED_OPTIONS = [
CONF_AVAILABILITY_TOPIC,
CONF_COMMAND_TOPIC,
CONF_ENCODING,
CONF_MESSAGE_EXPIRY_INTERVAL,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_STATE_TOPIC,
@@ -161,6 +163,14 @@ MQTT_ORIGIN_INFO_SCHEMA = vol.All(
),
)
def valid_message_expiry_interval(value: Any) -> int:
"""Return Message Expiry Interval in seconds."""
if isinstance(value, int):
return cv.positive_int(value) # type: ignore[no-any-return]
return int(cv.positive_time_period_dict(value).total_seconds())
MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
@@ -172,6 +182,7 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
@@ -203,6 +214,7 @@ DEVICE_DISCOVERY_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Required(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_MESSAGE_EXPIRY_INTERVAL): valid_message_expiry_interval,
vol.Optional(CONF_QOS): valid_qos_schema,
vol.Optional(CONF_ENCODING): cv.string,
}
@@ -197,9 +197,11 @@
},
"mqtt_settings": {
"data": {
"message_expiry_interval": "Message Expiry Interval",
"qos": "QoS"
},
"data_description": {
"message_expiry_interval": "Retention time interval for published message.",
"qos": "The Quality of Service value the device's entities should use."
},
"name": "MQTT settings"
@@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -14,7 +15,6 @@ from .const import (
ATTR_DESCRIPTION,
ATTR_EXPIRES,
ATTR_HEADLINE,
ATTR_ID,
ATTR_RECOMMENDED_ACTIONS,
ATTR_SENDER,
ATTR_SENT,
-2
View File
@@ -29,8 +29,6 @@ ATTR_SEVERITY: str = "severity"
ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions"
ATTR_AFFECTED_AREAS: str = "affected_areas"
ATTR_WEB: str = "web"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ID: str = "id"
ATTR_SENT: str = "sent"
ATTR_START: str = "start"
ATTR_EXPIRES: str = "expires"
@@ -595,8 +595,8 @@ class OpenAIBaseLLMEntity(Entity):
)
)
if "reasoning" not in model_args:
# Reasoning models handle this correctly with just a prompt
if not model_args["model"].startswith("o"):
# o-series models handle this correctly with just a prompt
remove_citations = True
tools.append(web_search)
@@ -15,5 +15,5 @@
"iot_class": "local_push",
"loggers": ["opendisplay"],
"quality_scale": "silver",
"requirements": ["py-opendisplay==5.9.0"]
"requirements": ["py-opendisplay==7.2.3"]
}
@@ -218,7 +218,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
pil_image,
refresh_mode=refresh_mode,
dither_mode=dither_mode,
tone_compression=tone_compression,
tone=tone_compression,
fit=fit_mode,
rotate=rotation,
)
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
_LOGGER.debug(
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
)
udn = discovery_info.upnp[ATTR_UPNP_UDN]
if isinstance(udn, list):
if not udn:
return self.async_abort(reason="incomplete_discovery")
udn = udn[0]
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
await self.async_set_unique_id(udn)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
_LOGGER.debug(
@@ -73,6 +73,9 @@
}
},
"exceptions": {
"cannot_connect": {
"message": "Failed to connect to OpenRGB SDK server {server_address}: {error}"
},
"communication_error": {
"message": "Failed to communicate with OpenRGB SDK server {server_address}: {error}"
},
@@ -118,6 +118,9 @@
"services": {
"prune_images": {
"service": "mdi:delete-sweep"
},
"recreate_container": {
"service": "mdi:restart"
}
}
}
@@ -20,6 +20,9 @@ from .coordinator import PortainerConfigEntry
ATTR_DATE_UNTIL = "until"
ATTR_DANGLING = "dangling"
ATTR_TIMEOUT = "timeout"
ATTR_PULL_IMAGE = "pull_image"
ATTR_CONTAINER_DEVICE_ID = "container_device_id"
SERVICE_PRUNE_IMAGES = "prune_images"
SERVICE_PRUNE_IMAGES_SCHEMA = vol.Schema(
@@ -32,6 +35,17 @@ SERVICE_PRUNE_IMAGES_SCHEMA = vol.Schema(
},
)
SERVICE_RECREATE_CONTAINER = "recreate_container"
SERVICE_RECREATE_CONTAINER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONTAINER_DEVICE_ID): cv.string,
vol.Optional(ATTR_TIMEOUT): vol.All(
cv.time_period, vol.Range(min=timedelta(minutes=1))
),
vol.Optional(ATTR_PULL_IMAGE): cv.boolean,
}
)
async def _extract_config_entry(service_call: ServiceCall) -> PortainerConfigEntry:
"""Extract config entry from the service call."""
@@ -75,6 +89,45 @@ async def _get_endpoint_id(
return endpoint_data.endpoint.id
async def _get_container_and_endpoint_ids(
call: ServiceCall,
) -> tuple[PortainerConfigEntry, int, str]:
"""Get config entry, endpoint ID and container ID from the container device ID."""
device_reg = dr.async_get(call.hass)
device = device_reg.async_get(call.data[ATTR_CONTAINER_DEVICE_ID])
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
config_entry: PortainerConfigEntry | None = None
for loaded_entry in call.hass.config_entries.async_loaded_entries(DOMAIN):
if loaded_entry.entry_id in device.config_entries:
config_entry = loaded_entry
break
if config_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
coordinator = config_entry.runtime_data
for data in coordinator.data.values():
for container_name, container_data in data.containers.items():
if (
DOMAIN,
f"{config_entry.entry_id}_{data.endpoint.id}_{container_name}",
) in device.identifiers:
return config_entry, data.endpoint.id, container_data.container.id
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
async def prune_images(call: ServiceCall) -> None:
"""Prune unused images in Portainer, with more controls."""
config_entry = await _extract_config_entry(call)
@@ -104,6 +157,40 @@ async def prune_images(call: ServiceCall) -> None:
) from err
async def recreate_container(call: ServiceCall) -> None:
"""Recreate a container in Portainer, with more controls."""
config_entry, endpoint_id, container_id = await _get_container_and_endpoint_ids(
call
)
coordinator = config_entry.runtime_data
timeout: timedelta | None = call.data.get(ATTR_TIMEOUT)
try:
await coordinator.portainer.container_recreate(
endpoint_id=endpoint_id,
container_id=container_id,
**({"timeout": timeout} if timeout is not None else {}),
pull_image=call.data.get(ATTR_PULL_IMAGE, False),
)
except PortainerAuthenticationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_auth_no_details",
) from err
except PortainerConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect_no_details",
) from err
except PortainerTimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_connect_no_details",
) from err
await coordinator.async_request_refresh()
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services."""
@@ -113,3 +200,10 @@ async def async_setup_services(hass: HomeAssistant) -> None:
prune_images,
SERVICE_PRUNE_IMAGES_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_RECREATE_CONTAINER,
recreate_container,
SERVICE_RECREATE_CONTAINER_SCHEMA,
)
@@ -16,3 +16,20 @@ prune_images:
required: false
selector:
boolean: {}
recreate_container:
fields:
container_device_id:
required: true
selector:
device:
integration: portainer
model: Container
timeout:
required: false
selector:
duration:
pull_image:
required: false
selector:
boolean:
@@ -235,6 +235,24 @@
}
},
"name": "Prune unused images"
},
"recreate_container": {
"description": "Recreates a container on a Portainer endpoint. This is more disruptive than a restart as the container will be stopped, removed, and then re-created with the same configuration. Use with caution.",
"fields": {
"container_device_id": {
"description": "The container to recreate.",
"name": "Container"
},
"pull_image": {
"description": "Whether to pull the image before recreating the container. This can be used to update the container to the latest version of the image.",
"name": "Pull image"
},
"timeout": {
"description": "The time to wait for the container to stop before killing it. If not provided, a default of 5 minutes will be used.",
"name": "Timeout"
}
},
"name": "Recreate container"
}
},
"system_health": {
+16 -16
View File
@@ -29,29 +29,29 @@
}
},
"sensor": {
"translation_key_0": {
"default": "mdi:abc"
"flow_sensor_clicks_cubic_meter": {
"default": "mdi:water-pump"
},
"translation_key_1": {
"default": "mdi:abc"
"flow_sensor_consumed_liters": {
"default": "mdi:water-pump"
},
"translation_key_2": {
"default": "mdi:abc"
"flow_sensor_leak_clicks": {
"default": "mdi:pipe-leak"
},
"translation_key_3": {
"default": "mdi:abc"
"flow_sensor_leak_volume": {
"default": "mdi:pipe-leak"
},
"translation_key_4": {
"default": "mdi:abc"
"flow_sensor_start_index": {
"default": "mdi:water-pump"
},
"translation_key_5": {
"default": "mdi:abc"
"flow_sensor_watering_clicks": {
"default": "mdi:water-pump"
},
"translation_key_6": {
"default": "mdi:abc"
"last_leak_detected": {
"default": "mdi:pipe-leak"
},
"translation_key_7": {
"default": "mdi:abc"
"rain_sensor_rain_start": {
"default": "mdi:weather-pouring"
}
},
"switch": {
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.19.1"]
"requirements": ["reolink-aio==0.20.0"]
}
@@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_USERNAME
from homeassistant.const import CONF_REGION, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -38,7 +38,6 @@ from . import RoborockConfigEntry
from .const import (
CONF_BASE_URL,
CONF_ENTRY_CODE,
CONF_REGION,
CONF_SHOW_BACKGROUND,
CONF_SHOW_ROOMS,
CONF_SHOW_WALLS,
@@ -13,8 +13,6 @@ CONF_USER_DATA = "user_data"
CONF_SHOW_BACKGROUND = "show_background"
CONF_SHOW_WALLS = "show_walls"
CONF_SHOW_ROOMS = "show_rooms"
# pylint: disable-next=home-assistant-duplicate-const
CONF_REGION = "region"
REGION_OPTIONS = ["auto", "us", "eu", "ru", "cn"]
# Option Flow steps
+1 -2
View File
@@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_MODEL,
CONF_MAC,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
@@ -30,8 +31,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
BLE_TEMP_HANDLE = 0x24
BLE_TEMP_UUID = "0000ff92-0000-1000-8000-00805f9b34fb"
+2 -2
View File
@@ -1,5 +1,7 @@
"""Define constants for the SleepIQ component."""
from homeassistant.const import PRESSURE
DATA_SLEEPIQ = "data_sleepiq"
DOMAIN = "sleepiq"
@@ -11,8 +13,6 @@ FIRMNESS = "firmness"
ICON_EMPTY = "mdi:bed-empty"
ICON_OCCUPIED = "mdi:bed"
IS_IN_BED = "is_in_bed"
# pylint: disable-next=home-assistant-duplicate-const
PRESSURE = "pressure"
SLEEP_NUMBER = "sleep_number"
FOOT_WARMING_TIMER = "foot_warming_timer"
FOOT_WARMER = "foot_warmer"
+1 -2
View File
@@ -11,14 +11,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfTime
from homeassistant.const import PRESSURE, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
HEART_RATE,
HRV,
PRESSURE,
RESPIRATORY_RATE,
SLEEP_DURATION,
SLEEP_NUMBER,
+1 -2
View File
@@ -7,6 +7,7 @@ import smarttub
import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -19,8 +20,6 @@ from .entity import SmartTubOnboardSensorBase
# the desired duration, in hours, of the cycle
ATTR_DURATION = "duration"
ATTR_CYCLE_LAST_UPDATED = "cycle_last_updated"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
# the hour of the day at which to start the cycle (0-23)
ATTR_START_HOUR = "start_hour"
-4
View File
@@ -38,12 +38,8 @@ PLATFORMS = [
Platform.SENSOR,
]
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_LOCK = "lock"
SERVICE_REMOTE_START = "remote_start"
SERVICE_REMOTE_STOP = "remote_stop"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_UNLOCK = "unlock"
SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door"
ATTR_DOOR = "door"
@@ -4,9 +4,10 @@ import logging
from subarulink.exceptions import SubaruException
from homeassistant.const import SERVICE_UNLOCK
from homeassistant.exceptions import HomeAssistantError
from .const import SERVICE_REMOTE_START, SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN
from .const import SERVICE_REMOTE_START, VEHICLE_NAME, VEHICLE_VIN
_LOGGER = logging.getLogger(__name__)
@@ -7,14 +7,13 @@ from surepy.enums import Location
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
import voluptuous as vol
from homeassistant.const import Platform
from homeassistant.const import ATTR_LOCATION, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_FLAP_ID,
ATTR_LOCATION,
ATTR_LOCK_STATE,
ATTR_PET_NAME,
DOMAIN,
@@ -18,7 +18,5 @@ SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW
SERVICE_SET_LOCK_STATE = "set_lock_state"
SERVICE_SET_PET_LOCATION = "set_pet_location"
ATTR_FLAP_ID = "flap_id"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCATION = "location"
ATTR_LOCK_STATE = "lock_state"
ATTR_PET_NAME = "pet_name"
@@ -8,7 +8,7 @@ from surepy.enums import EntityType, Location, LockState
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.const import ATTR_LOCATION, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -16,7 +16,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import (
ATTR_FLAP_ID,
ATTR_LOCATION,
ATTR_LOCK_STATE,
ATTR_PET_NAME,
DOMAIN,
@@ -1,9 +1,7 @@
"""The System Bridge integration."""
import asyncio
from dataclasses import asdict
import logging
from typing import Any
from systembridgeconnector.exceptions import (
AuthenticationException,
@@ -11,71 +9,34 @@ from systembridgeconnector.exceptions import (
ConnectionErrorException,
DataMissingException,
)
from systembridgeconnector.models.keyboard_key import KeyboardKey
from systembridgeconnector.models.keyboard_text import KeyboardText
from systembridgeconnector.models.modules.processes import Process
from systembridgeconnector.models.open_path import OpenPath
from systembridgeconnector.models.open_url import OpenUrl
from systembridgeconnector.version import Version
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_COMMAND,
CONF_ENTITY_ID,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PATH,
CONF_PORT,
CONF_TOKEN,
CONF_URL,
Platform,
)
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from .config_flow import SystemBridgeConfigFlow
from .const import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
def _get_coordinator(
hass: HomeAssistant, entry_id: str
) -> SystemBridgeDataUpdateCoordinator:
"""Return the coordinator for a config entry id."""
entry: SystemBridgeConfigEntry | None = hass.config_entries.async_get_entry(
entry_id
)
if entry is None or entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": entry_id},
)
return entry.runtime_data
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.MEDIA_PLAYER,
@@ -84,26 +45,12 @@ PLATFORMS = [
Platform.UPDATE,
]
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_TEXT = "text"
SERVICE_GET_PROCESS_BY_ID = "get_process_by_id"
SERVICE_GET_PROCESSES_BY_NAME = "get_processes_by_name"
SERVICE_OPEN_PATH = "open_path"
SERVICE_POWER_COMMAND = "power_command"
SERVICE_OPEN_URL = "open_url"
SERVICE_SEND_KEYPRESS = "send_keypress"
SERVICE_SEND_TEXT = "send_text"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the System Bridge services."""
POWER_COMMAND_MAP = {
"hibernate": "power_hibernate",
"lock": "power_lock",
"logout": "power_logout",
"restart": "power_restart",
"shutdown": "power_shutdown",
"sleep": "power_sleep",
}
async_setup_services(hass)
return True
async def async_setup_entry(
@@ -231,219 +178,6 @@ async def async_setup_entry(
)
)
if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
return True
def valid_device(device: str) -> str:
"""Check device is valid."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device)
if device_entry is not None:
try:
return next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device},
) from exception
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device},
)
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
"""Handle the get process by id service call."""
_LOGGER.debug("Get process by id: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
try:
return asdict(
next(
process
for process in processes
if process.id == service_call.data[CONF_ID]
)
)
except StopIteration as exception:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="process_not_found",
translation_placeholders={"id": service_call.data[CONF_ID]},
) from exception
async def handle_get_processes_by_name(
service_call: ServiceCall,
) -> ServiceResponse:
"""Handle the get process by name service call."""
_LOGGER.debug("Get process by name: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
# Find processes from list
items: list[dict[str, Any]] = [
asdict(process)
for process in coordinator.data.processes
if process.name is not None
and service_call.data[CONF_NAME].lower() in process.name.lower()
]
return {
"count": len(items),
"processes": list(items),
}
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open path service call."""
_LOGGER.debug("Open path: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
return asdict(response)
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
"""Handle the power command service call."""
_LOGGER.debug("Power command: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
)()
return asdict(response)
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open url service call."""
_LOGGER.debug("Open URL: %s", service_call.data)
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
return asdict(response)
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
return asdict(response)
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
return asdict(response)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_GET_PROCESS_BY_ID,
handle_get_process_by_id,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_ID): cv.positive_int,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_GET_PROCESSES_BY_NAME,
handle_get_processes_by_name,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_NAME): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_PATH,
handle_open_path,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_PATH): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_POWER_COMMAND,
handle_power_command,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_URL,
handle_open_url,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_URL): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_SEND_KEYPRESS,
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_KEY): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
},
)
# pylint: disable-next=home-assistant-service-registered-in-setup-entry
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT,
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_TEXT): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
# Reload entry when its updated.
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@@ -454,9 +188,7 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = entry.runtime_data
@@ -17,15 +17,51 @@
"boot_time": {
"default": "mdi:av-timer"
},
"cpu_power_core": {
"default": "mdi:chip"
},
"cpu_power_package": {
"default": "mdi:chip"
},
"cpu_speed": {
"default": "mdi:speedometer"
},
"display_refresh_rate": {
"default": "mdi:monitor"
},
"display_resolution_x": {
"default": "mdi:monitor"
},
"display_resolution_y": {
"default": "mdi:monitor"
},
"displays_connected": {
"default": "mdi:monitor"
},
"gpu_core_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_fan_speed": {
"default": "mdi:fan"
},
"gpu_memory_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_memory_free": {
"default": "mdi:memory"
},
"gpu_memory_used": {
"default": "mdi:memory"
},
"gpu_memory_used_percentage": {
"default": "mdi:memory"
},
"gpu_power_usage": {
"default": "mdi:lightning-bolt"
},
"gpu_usage_percentage": {
"default": "mdi:percent"
},
"kernel": {
"default": "mdi:devices"
},
@@ -38,6 +74,9 @@
"memory_used": {
"default": "mdi:memory"
},
"memory_used_percentage": {
"default": "mdi:memory"
},
"os": {
"default": "mdi:devices"
},
@@ -47,6 +86,12 @@
"processes": {
"default": "mdi:counter"
},
"processes_load_cpu": {
"default": "mdi:percent"
},
"space_used": {
"default": "mdi:harddisk"
},
"version": {
"default": "mdi:counter"
},
@@ -27,7 +27,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
@@ -284,10 +284,10 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
),
SystemBridgeSensorEntityDescription(
key="memory_used_percentage",
translation_key="memory_used_percentage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data: data.memory.virtual.percent,
),
SystemBridgeSensorEntityDescription(
@@ -380,11 +380,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"filesystem_{partition.mount_point.replace(':', '')}",
name=f"{partition.mount_point} space used",
translation_key="space_used",
translation_placeholders={"partition": partition.mount_point},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:harddisk",
value=(
lambda data, dk=index_device, pk=index_partition: (
partition_usage(data, dk, pk)
@@ -427,10 +427,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_x",
name=f"Display {display.id} resolution x",
translation_key="display_resolution_x",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_horizontal(
data, k
),
@@ -441,10 +441,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_y",
name=f"Display {display.id} resolution y",
translation_key="display_resolution_y",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_vertical(
data, k
),
@@ -455,12 +455,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_refresh_rate",
name=f"Display {display.id} refresh rate",
translation_key="display_refresh_rate",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:monitor",
value=lambda data, k=index: display_refresh_rate(data, k),
),
entry.data[CONF_PORT],
@@ -474,13 +474,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_core_clock_speed",
name=f"{gpu.name} clock speed",
translation_key="gpu_core_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_core_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -489,13 +489,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_clock_speed",
name=f"{gpu.name} memory clock speed",
translation_key="gpu_memory_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_memory_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -504,12 +504,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_free",
name=f"{gpu.name} memory free",
translation_key="gpu_memory_free",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_free(data, k),
),
entry.data[CONF_PORT],
@@ -518,11 +518,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used_percentage",
name=f"{gpu.name} memory used %",
translation_key="gpu_memory_used_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -531,13 +531,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used",
name=f"{gpu.name} memory used",
translation_key="gpu_memory_used",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used(data, k),
),
entry.data[CONF_PORT],
@@ -546,11 +546,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_fan_speed",
name=f"{gpu.name} fan speed",
translation_key="gpu_fan_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
icon="mdi:fan",
value=lambda data, k=index: gpu_fan_speed(data, k),
),
entry.data[CONF_PORT],
@@ -559,7 +559,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_power_usage",
name=f"{gpu.name} power usage",
translation_key="gpu_power_usage",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
@@ -571,7 +572,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_temperature",
name=f"{gpu.name} temperature",
translation_key="gpu_temperature",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -585,11 +587,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_usage_percentage",
name=f"{gpu.name} usage %",
translation_key="gpu_usage_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:percent",
value=lambda data, k=index: gpu_usage_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -605,11 +607,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"processes_load_cpu_{cpu.id}",
name=f"Load CPU {cpu.id}",
translation_key="processes_load_cpu",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_usage_per_cpu(data, k),
),
@@ -619,11 +621,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"cpu_power_core_{cpu.id}",
name=f"CPU Core {cpu.id} Power",
translation_key="cpu_power_core",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:chip",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_power_per_cpu(data, k),
),
@@ -653,8 +655,6 @@ class SystemBridgeSensor(SystemBridgeEntity, SensorEntity):
description.key,
)
self.entity_description = description
if description.name is not UNDEFINED:
self._attr_has_entity_name = False
@property
def native_value(self) -> StateType:
@@ -0,0 +1,269 @@
"""Service registration for System Bridge integration."""
from dataclasses import asdict
import logging
from typing import Any
from systembridgeconnector.models.keyboard_key import KeyboardKey
from systembridgeconnector.models.keyboard_text import KeyboardText
from systembridgeconnector.models.modules.processes import Process
from systembridgeconnector.models.open_path import OpenPath
from systembridgeconnector.models.open_url import OpenUrl
import voluptuous as vol
from homeassistant.const import CONF_COMMAND, CONF_ID, CONF_NAME, CONF_PATH, CONF_URL
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
service,
)
from .const import DOMAIN
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_TEXT = "text"
POWER_COMMAND_MAP = {
"hibernate": "power_hibernate",
"lock": "power_lock",
"logout": "power_logout",
"restart": "power_restart",
"shutdown": "power_shutdown",
"sleep": "power_sleep",
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for System Bridge integration."""
hass.services.async_register(
DOMAIN,
"get_process_by_id",
handle_get_process_by_id,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_ID): cv.positive_int,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"get_processes_by_name",
handle_get_processes_by_name,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_NAME): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"open_path",
handle_open_path,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_PATH): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"power_command",
handle_power_command,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"open_url",
handle_open_url,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_URL): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
"send_keypress",
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_KEY): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
},
)
hass.services.async_register(
DOMAIN,
"send_text",
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): cv.string,
vol.Required(CONF_TEXT): cv.string,
},
),
supports_response=SupportsResponse.ONLY,
)
def _get_coordinator(
hass: HomeAssistant, device_id: str
) -> SystemBridgeDataUpdateCoordinator:
"""Return the coordinator for a device id."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device_id)
if device_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device_id},
)
try:
entry_id = next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"device": device_id},
) from e
entry: SystemBridgeConfigEntry = service.async_get_config_entry(
hass, DOMAIN, entry_id
)
return entry.runtime_data
async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
"""Handle the get process by id service call."""
_LOGGER.debug("Get process by id: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
processes: list[Process] = coordinator.data.processes
# Find process.id from list, raise ServiceValidationError if not found
try:
return asdict(
next(
process
for process in processes
if process.id == service_call.data[CONF_ID]
)
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="process_not_found",
translation_placeholders={"id": service_call.data[CONF_ID]},
) from e
async def handle_get_processes_by_name(
service_call: ServiceCall,
) -> ServiceResponse:
"""Handle the get process by name service call."""
_LOGGER.debug("Get process by name: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
# Find processes from list
items: list[dict[str, Any]] = [
asdict(process)
for process in coordinator.data.processes
if process.name is not None
and service_call.data[CONF_NAME].lower() in process.name.lower()
]
return {
"count": len(items),
"processes": list(items),
}
async def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open path service call."""
_LOGGER.debug("Open path: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_path(
OpenPath(path=service_call.data[CONF_PATH])
)
return asdict(response)
async def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
"""Handle the power command service call."""
_LOGGER.debug("Power command: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await getattr(
coordinator.websocket_client,
POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
)()
return asdict(response)
async def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
"""Handle the open url service call."""
_LOGGER.debug("Open URL: %s", service_call.data)
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.open_url(
OpenUrl(url=service_call.data[CONF_URL])
)
return asdict(response)
async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_keypress service call."""
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=service_call.data[CONF_KEY])
)
return asdict(response)
async def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
"""Handle the send_text service call."""
coordinator = _get_coordinator(service_call.hass, service_call.data[CONF_BRIDGE])
response = await coordinator.websocket_client.keyboard_text(
KeyboardText(text=service_call.data[CONF_TEXT])
)
return asdict(response)
@@ -89,3 +89,4 @@ power_command:
- "restart"
- "shutdown"
- "sleep"
translation_key: "power_command"
@@ -54,6 +54,9 @@
"boot_time": {
"name": "Boot time"
},
"cpu_power_core": {
"name": "CPU core {cpu_id} power"
},
"cpu_power_package": {
"name": "CPU package power"
},
@@ -66,9 +69,45 @@
"cpu_voltage": {
"name": "CPU voltage"
},
"display_refresh_rate": {
"name": "Display {display_id} refresh rate"
},
"display_resolution_x": {
"name": "Display {display_id} resolution x"
},
"display_resolution_y": {
"name": "Display {display_id} resolution y"
},
"displays_connected": {
"name": "Displays connected"
},
"gpu_core_clock_speed": {
"name": "{gpu_name} clock speed"
},
"gpu_fan_speed": {
"name": "{gpu_name} fan speed"
},
"gpu_memory_clock_speed": {
"name": "{gpu_name} memory clock speed"
},
"gpu_memory_free": {
"name": "{gpu_name} memory free"
},
"gpu_memory_used": {
"name": "{gpu_name} memory used"
},
"gpu_memory_used_percentage": {
"name": "{gpu_name} memory used %"
},
"gpu_power_usage": {
"name": "{gpu_name} power usage"
},
"gpu_temperature": {
"name": "{gpu_name} temperature"
},
"gpu_usage_percentage": {
"name": "{gpu_name} usage %"
},
"kernel": {
"name": "Kernel"
},
@@ -81,6 +120,9 @@
"memory_used": {
"name": "Memory used"
},
"memory_used_percentage": {
"name": "Memory used %"
},
"os": {
"name": "Operating system"
},
@@ -90,6 +132,12 @@
"processes": {
"name": "Processes"
},
"processes_load_cpu": {
"name": "Load CPU {cpu_id}"
},
"space_used": {
"name": "{partition} space used"
},
"version": {
"name": "Version"
},
@@ -130,6 +178,18 @@
"title": "System Bridge upgrade required"
}
},
"selector": {
"power_command": {
"options": {
"hibernate": "Hibernate",
"lock": "Lock",
"logout": "Logout",
"restart": "[%key:common::action::restart%]",
"shutdown": "Shutdown",
"sleep": "Sleep"
}
}
},
"services": {
"get_process_by_id": {
"description": "Gets a process by the ID.",
@@ -32,6 +32,7 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
_attr_has_entity_name = True
_attr_title = "System Bridge"
_attr_name = None
def __init__(
self,
@@ -44,7 +45,6 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
api_port,
"update",
)
self._attr_name = coordinator.data.system.hostname
@property
def installed_version(self) -> str | None:
@@ -58,7 +58,7 @@ send_message:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -101,7 +101,7 @@ send_chat_action:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -195,7 +195,7 @@ send_photo:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -287,7 +287,7 @@ send_media_group:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -372,7 +372,7 @@ send_sticker:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -466,7 +466,7 @@ send_animation:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -560,7 +560,7 @@ send_video:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -645,7 +645,7 @@ send_voice:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -739,7 +739,7 @@ send_document:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -804,7 +804,7 @@ send_location:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -861,7 +861,7 @@ send_poll:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -913,7 +913,7 @@ edit_message:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -991,7 +991,7 @@ edit_message_media:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1028,7 +1028,7 @@ edit_caption:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1061,7 +1061,7 @@ edit_replymarkup:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1108,7 +1108,7 @@ delete_message:
example: "{{ trigger.event.data.message.message_id }}"
selector:
text:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1129,7 +1129,7 @@ leave_chat:
filter:
domain: notify
integration: telegram_bot
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1164,7 +1164,7 @@ set_message_reaction:
required: false
selector:
boolean:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1233,7 +1233,7 @@ send_message_draft:
- "markdownv2"
- "plain_text"
translation_key: "parse_mode"
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -367,8 +367,8 @@
},
"name": "Delete message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -425,8 +425,8 @@
},
"name": "Edit caption",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -472,8 +472,8 @@
},
"name": "Edit message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -535,8 +535,8 @@
},
"name": "Edit message media",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -569,8 +569,8 @@
},
"name": "Edit reply markup",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -592,8 +592,8 @@
},
"name": "Leave chat",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -671,8 +671,8 @@
},
"name": "Send animation",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -705,8 +705,8 @@
},
"name": "Send chat action",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -784,8 +784,8 @@
},
"name": "Send document",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -842,8 +842,8 @@
},
"name": "Send location",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -889,8 +889,8 @@
},
"name": "Send media group",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -952,8 +952,8 @@
},
"name": "Send message",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "Additional options"
}
}
},
@@ -991,8 +991,8 @@
},
"name": "Send message draft",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1070,8 +1070,8 @@
},
"name": "Send photo",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "URL options"
@@ -1128,8 +1128,8 @@
},
"name": "Send poll",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1203,8 +1203,8 @@
},
"name": "Send sticker",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1285,8 +1285,8 @@
},
"name": "Send video",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1363,8 +1363,8 @@
},
"name": "Send voice",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1401,8 +1401,8 @@
},
"name": "Set message reaction",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
}
@@ -497,7 +497,7 @@
"default": "mdi:battery-clock"
},
"forward_collision_warning": {
"default": "mdi:car-crash",
"default": "mdi:car-emergency",
"state": {
"average": "mdi:alert-circle",
"early": "mdi:alert-octagon",
@@ -634,7 +634,7 @@
"default": "mdi:key"
},
"pedal_position": {
"default": "mdi:pedestal"
"default": "mdi:gauge"
},
"powershare_hours_left": {
"default": "mdi:clock-time-eight-outline"
@@ -794,7 +794,7 @@
"service": "mdi:calendar-plus"
},
"add_precondition_schedule": {
"service": "mdi:hvac-outline"
"service": "mdi:hvac"
},
"navigation_gps_request": {
"service": "mdi:crosshairs-gps"
@@ -803,7 +803,7 @@
"service": "mdi:calendar-minus"
},
"remove_precondition_schedule": {
"service": "mdi:hvac-off-outline"
"service": "mdi:hvac-off"
},
"set_scheduled_charging": {
"service": "mdi:timeline-clock-outline"
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["uiprotect"],
"quality_scale": "platinum",
"requirements": ["uiprotect==10.4.1"]
"requirements": ["uiprotect==10.5.0"]
}
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
from .const import DOMAIN, PLATFORMS
from .coordinator import UptimeRobotConfigEntry, UptimeRobotDataUpdateCoordinator
@@ -15,9 +15,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeRobotConfigEntry)
"""Set up UptimeRobot from a config entry."""
key: str = entry.data[CONF_API_KEY]
if key.startswith(("ur", "m")):
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(
"Wrong API key type detected, use the 'main' API key"
translation_domain=DOMAIN,
translation_key="api_key_wrong_type",
)
uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass))
@@ -48,11 +48,16 @@ class UptimeRobotDataUpdateCoordinator(
try:
response = await self.api.async_get_monitors()
except UptimeRobotAuthenticationException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(exception) from exception
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_exception",
) from exception
except UptimeRobotException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(exception) from exception
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_generic_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
if TYPE_CHECKING:
assert isinstance(response.data, list)
@@ -57,7 +57,16 @@
}
},
"exceptions": {
"api_exception": {
"api_authentication_exception": {
"message": "API authentication failed, please check your API key"
},
"api_generic_exception": {
"message": "API error: {error}"
},
"api_key_wrong_type": {
"message": "Wrong API key type detected, use the 'main' API key"
},
"api_switch_exception": {
"message": "Could not turn on/off monitoring: {error}"
}
}
@@ -33,7 +33,7 @@ def uptimerobot_api_call[_T: UptimeRobotEntity, **_P](
except UptimeRobotException as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_exception",
translation_key="api_switch_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
@@ -68,7 +68,7 @@
"state": {
"lightning": "mdi:weather-lightning-rainy",
"rain": "mdi:weather-rainy",
"rain_snow": "mdi:weather-snoy-rainy",
"rain_snow": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy"
}
},
+4 -2
View File
@@ -46,8 +46,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebOsTvConfigEntry) -> b
try:
await client.connect()
except WebOsTvPairError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_failed",
) from err
# If pairing request accepted there will be no error
# Update the stored key without triggering reauth
@@ -6,6 +6,7 @@ from homeassistant.components.device_automation import (
DEVICE_TRIGGER_BASE_SCHEMA,
InvalidDeviceAutomationConfig,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -13,10 +14,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN, trigger
from .helpers import (
async_get_client_by_device_entry,
async_get_device_entry_by_device_id,
)
from .helpers import async_get_device_entry_by_device_id
from .triggers.turn_on import (
PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE,
async_get_turn_on_trigger,
@@ -40,10 +38,31 @@ async def async_validate_trigger_config(
device_id = config[CONF_DEVICE_ID]
try:
device = async_get_device_entry_by_device_id(hass, device_id)
async_get_client_by_device_entry(hass, device)
except ValueError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise InvalidDeviceAutomationConfig(err) from err
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device_id},
) from err
for config_entry_id in device.config_entries:
if (
entry := hass.config_entries.async_get_entry(config_entry_id)
) and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
break
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_config_entry_not_loaded",
translation_placeholders={"device_id": device.id},
)
else:
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device.id},
)
return config
+1 -26
View File
@@ -4,7 +4,7 @@ import logging
from aiowebostv import WebOsClient, WebOsTvState
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -56,31 +56,6 @@ def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> s
return entity_entry.device_id
@callback
def async_get_client_by_device_entry(
hass: HomeAssistant, device: DeviceEntry
) -> WebOsClient:
"""Get WebOsClient from Device Registry by device entry.
Raises ValueError if client is not found.
"""
for config_entry_id in device.config_entries:
entry: WebOsTvConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
if entry and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
return entry.runtime_data
raise ValueError(
f"Device {device.id} is not from a loaded {DOMAIN} config entry"
)
raise ValueError(
f"Device {device.id} is not from an existing {DOMAIN} config entry"
)
def get_sources(tv_state: WebOsTvState) -> list[str]:
"""Construct sources list."""
sources = []
@@ -46,9 +46,18 @@
}
},
"exceptions": {
"auth_failed": {
"message": "Pairing failed, make sure to accept the pairing request on your TV."
},
"communication_error": {
"message": "Communication error while calling {func} for device {name}: {error}"
},
"device_config_entry_not_loaded": {
"message": "The LG webOS TV integration for device {device_id} is not loaded."
},
"device_not_valid": {
"message": "Device {device_id} is not a valid LG webOS TV device."
},
"device_off": {
"message": "Error calling {func} for device {name}: Device is off and cannot be controlled."
},

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