Compare commits

..

2 Commits

Author SHA1 Message Date
Jan Čermák
96d49157f1 Sleep twice as suggested in PR 2026-03-17 16:33:57 +01:00
Jan Čermák
67dbf189cd Bump base image to 2026.02.0 with Python 3.14.3, use 3.14.3 in CI
This also bumps libcec used in the base image to 7.1.1, full changelog:
* https://github.com/home-assistant/docker/releases/tag/2026.02.0

Python changelog:
* https://docs.python.org/release/3.14.3/whatsnew/changelog.html
2026-03-17 16:33:56 +01:00
244 changed files with 2874 additions and 9649 deletions

View File

@@ -620,14 +620,12 @@ rules:
### Config Flow Testing
- **100% Coverage Required**: All config flow paths must be tested
- **Patch Boundaries**: Only patch library or client methods when testing config flows. Do not patch methods defined in `config_flow.py`; exercise the flow logic end-to-end.
- **Test Scenarios**:
- All flow initiation methods (user, discovery, import)
- Successful configuration paths
- Error recovery scenarios
- Prevention of duplicate entries
- Flow completion after errors
- Reauthentication/reconfigure flows
### Testing
- **Integration-specific tests** (recommended):

1
.gitattributes vendored
View File

@@ -16,7 +16,6 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true

View File

@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.01.0"
BASE_IMAGE_VERSION: "2026.02.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
@@ -35,7 +35,6 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -101,7 +100,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-24.04
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
@@ -196,20 +195,77 @@ jobs:
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Build base image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
image-tags: ${{ needs.init.outputs.version }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build variables
id: vars
shell: bash
env:
ARCH: ${{ matrix.arch }}
run: |
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
env:
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${BASE_IMAGE}"
- name: Verify cache image signature
id: cache
continue-on-error: true
env:
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${CACHE_IMAGE}"
- name: Build base image
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
version: ${{ needs.init.outputs.version }}
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.init.outputs.version }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -258,38 +314,35 @@ jobs:
with:
persist-credentials: false
- name: Compute extra tags
id: tags
shell: bash
- name: Set build additional args
env:
VERSION: ${{ needs.init.outputs.version }}
run: |
# Create general tags
if [[ "${VERSION}" =~ d ]]; then
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${VERSION}" =~ b ]]; then
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
- name: Build machine image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: machine/
cosign-base-identity: "https://github.com/home-assistant/core/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
file: machine/${{ matrix.machine }}
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
image-tags: |
${{ needs.init.outputs.version }}
${{ steps.tags.outputs.extra_tags }}
push: true
version: ${{ needs.init.outputs.version }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
publish_ha:
name: Publish version files

View File

@@ -1 +1 @@
3.14.2
3.14.3

2
CODEOWNERS generated
View File

@@ -1616,6 +1616,8 @@ build.json @home-assistant/supervisor
/tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
/homeassistant/components/steam_online/ @tkdrob

1
Dockerfile generated
View File

@@ -10,6 +10,7 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"

View File

@@ -46,10 +46,19 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
coordinator = AladdinConnectCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
try:
doors = await client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
entry.runtime_data = coordinator
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -91,7 +100,7 @@ def remove_stale_devices(
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data.data)
all_device_ids = set(config_entry.runtime_data)
for device_entry in device_entries:
device_id: str | None = None

View File

@@ -11,24 +11,22 @@ from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
"""Coordinator for Aladdin Connect integration."""
config_entry: AladdinConnectConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -39,16 +37,18 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> dict[str, GarageDoor]:
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
try:
doors = await self.client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise UpdateFailed(f"Error communicating with API: {err}") from err
await self.client.update_door(self.data.device_id, self.data.door_number)
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
return {door.unique_id: door for door in doors}
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data

View File

@@ -7,7 +7,7 @@ from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -24,22 +24,11 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
coordinators = entry.runtime_data
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@@ -49,10 +38,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator, door_id)
self._attr_unique_id = door_id
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
@@ -77,16 +66,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.door.status) is None:
if (status := self.coordinator.data.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.door.status == "closing"
return self.coordinator.data.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.door.status == "opening"
return self.coordinator.data.status == "opening"

View File

@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"doors": {
uid: {
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
"device_id": coordinator.data.device_id,
"door_number": coordinator.data.door_number,
"name": coordinator.data.name,
"status": coordinator.data.status,
"link_status": coordinator.data.link_status,
"battery_level": coordinator.data.battery_level,
}
for uid, door in config_entry.runtime_data.data.items()
for uid, coordinator in config_entry.runtime_data.items()
},
}

View File

@@ -1,7 +1,6 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -15,28 +14,17 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
self._door_id = door_id
door = self.door
device = coordinator.data
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, door.unique_id)},
identifiers={(DOMAIN, device.unique_id)},
manufacturer="Aladdin Connect",
name=door.name,
name=device.name,
)
self._device_id = door.device_id
self._number = door.door_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._door_id in self.coordinator.data
@property
def door(self) -> GarageDoor:
"""Return the garage door data."""
return self.coordinator.data[self._door_id]
self._device_id = device.device_id
self._number = device.door_number
@property
def client(self) -> AladdinConnectClient:

View File

@@ -57,7 +57,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
@@ -49,24 +49,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
coordinators = entry.runtime_data
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinConnectSensor(coordinator, door_id, description)
for door_id in new_devices
for description in SENSOR_TYPES
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
@@ -77,15 +66,14 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
def __init__(
self,
coordinator: AladdinConnectCoordinator,
door_id: str,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator, door_id)
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{door_id}-{entity_description.key}"
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.door)
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -123,22 +123,16 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"climate",
"cover",
"device_tracker",
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"lawn_mower",
"light",
"lock",
"media_player",
"motion",
"occupancy",
"person",
"siren",
"switch",
"vacuum",
"window",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
@@ -165,7 +159,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"remote",
"scene",
"schedule",
"select",
"siren",
"switch",
"text",

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==67"],
"requirements": ["axis==66"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -246,8 +246,6 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -334,10 +332,8 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size

View File

@@ -66,7 +66,6 @@ class ClementineDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY
)
_attr_volume_step = 0.04
def __init__(self, client, name):
"""Initialize the Clementine device."""
@@ -125,6 +124,16 @@ class ClementineDevice(MediaPlayerEntity):
return None, None
def volume_up(self) -> None:
"""Volume up the media player."""
newvolume = min(self._client.volume + 4, 100)
self._client.set_volume(newvolume)
def volume_down(self) -> None:
"""Volume down media player."""
newvolume = max(self._client.volume - 4, 0)
self._client.set_volume(newvolume)
def mute_volume(self, mute: bool) -> None:
"""Send mute command."""
self._client.set_volume(0)

View File

@@ -32,7 +32,6 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .condition import make_cover_is_closed_condition, make_cover_is_open_condition
from .const import (
ATTR_CURRENT_POSITION,
ATTR_CURRENT_TILT_POSITION,
@@ -81,8 +80,6 @@ __all__ = [
"CoverEntityFeature",
"CoverState",
"make_cover_closed_trigger",
"make_cover_is_closed_condition",
"make_cover_is_open_condition",
"make_cover_opened_trigger",
]

View File

@@ -9,12 +9,9 @@ from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change
from . import DOMAIN
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
@@ -26,10 +23,10 @@ async def async_setup_entry(
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("valve_1", "Front Garden", ValveState.OPEN),
DemoValve("valve_2", "Orchard", ValveState.CLOSED),
DemoValve("valve_3", "Back Garden", ValveState.CLOSED, position=70),
DemoValve("valve_4", "Trees", ValveState.CLOSED, position=30),
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
DemoValve("Back Garden", ValveState.CLOSED, position=70),
DemoValve("Trees", ValveState.CLOSED, position=30),
]
)
@@ -37,24 +34,17 @@ async def async_setup_entry(
class DemoValve(ValveEntity):
"""Representation of a Demo valve."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
name: str,
state: str,
moveable: bool = True,
position: int | None = None,
) -> None:
"""Initialize the valve."""
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
self._attr_name = name
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE

View File

@@ -1,29 +0,0 @@
"""Provides conditions for doors."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_DOOR: dict[str, str] = {
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.DOOR,
COVER_DOMAIN: CoverDeviceClass.DOOR,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_DOOR),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_DOOR),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for doors."""
return CONDITIONS

View File

@@ -1,28 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: door
- domain: cover
device_class: door
is_open:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: door
- domain: cover
device_class: door

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:door-closed"
},
"is_open": {
"condition": "mdi:door-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:door-closed"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted doors.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted doors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more doors are closed.",
"fields": {
"behavior": {
"description": "[%key:component::door::common::condition_behavior_description%]",
"name": "[%key:component::door::common::condition_behavior_name%]"
}
},
"name": "Door is closed"
},
"is_open": {
"description": "Tests if one or more doors are open.",
"fields": {
"behavior": {
"description": "[%key:component::door::common::condition_behavior_description%]",
"name": "[%key:component::door::common::condition_behavior_name%]"
}
},
"name": "Door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -12,12 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfInformation,
UnitOfTemperature,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -61,14 +56,6 @@ SENSORS: tuple[FullySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FullySensorEntityDescription(
key="batteryTemperature",
translation_key="battery_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FullySensorEntityDescription(
key="currentPage",
translation_key="current_page",

View File

@@ -118,9 +118,6 @@
}
},
"sensor": {
"battery_temperature": {
"name": "Battery temperature"
},
"current_page": {
"name": "Current page"
},

View File

@@ -1,31 +0,0 @@
"""Provides conditions for garage doors."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_GARAGE_DOOR: dict[str, str] = {
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.GARAGE_DOOR,
COVER_DOMAIN: CoverDeviceClass.GARAGE,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_GARAGE_DOOR
),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GARAGE_DOOR),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for garage doors."""
return CONDITIONS

View File

@@ -1,28 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: garage_door
- domain: cover
device_class: garage
is_open:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: garage_door
- domain: cover
device_class: garage

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:garage"
},
"is_open": {
"condition": "mdi:garage-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:garage"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted garage doors.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted garage doors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more garage doors are closed.",
"fields": {
"behavior": {
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
}
},
"name": "Garage door is closed"
},
"is_open": {
"description": "Tests if one or more garage doors are open.",
"fields": {
"behavior": {
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
}
},
"name": "Garage door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -1,24 +0,0 @@
"""Provides conditions for gates."""
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverDeviceClass,
make_cover_is_closed_condition,
make_cover_is_open_condition,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition
DEVICE_CLASSES_GATE: dict[str, str] = {
COVER_DOMAIN: CoverDeviceClass.GATE,
}
CONDITIONS: dict[str, type[Condition]] = {
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_GATE),
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GATE),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for gates."""
return CONDITIONS

View File

@@ -1,24 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: gate
is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: gate

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_closed": {
"condition": "mdi:gate"
},
"is_open": {
"condition": "mdi:gate-open"
}
},
"triggers": {
"closed": {
"trigger": "mdi:gate"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted gates.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted gates to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_closed": {
"description": "Tests if one or more gates are closed.",
"fields": {
"behavior": {
"description": "[%key:component::gate::common::condition_behavior_description%]",
"name": "[%key:component::gate::common::condition_behavior_name%]"
}
},
"name": "Gate is closed"
},
"is_open": {
"description": "Tests if one or more gates are open.",
"fields": {
"behavior": {
"description": "[%key:component::gate::common::condition_behavior_description%]",
"name": "[%key:component::gate::common::condition_behavior_name%]"
}
},
"name": "Gate is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -251,7 +251,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
def get_data(
self, entity_description: GrowattSensorEntityDescription
) -> str | int | float | datetime.datetime | datetime.date | None:
) -> str | int | float | None:
"""Get the data."""
variable = entity_description.api_key
api_value = self.data.get(variable)

View File

@@ -1,17 +1,4 @@
{
"entity": {
"sensor": {
"storage_load_consumption_solar_storage": {
"default": "mdi:lightning-bolt"
},
"total_money_today": {
"default": "mdi:cash"
},
"total_money_total": {
"default": "mdi:cash"
}
}
},
"services": {
"read_ac_charge_times": {
"service": "mdi:battery-clock-outline"

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GrowattConfigEntry, GrowattCoordinator
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
_LOGGER = logging.getLogger(__name__)
@@ -26,10 +27,9 @@ PARALLEL_UPDATES = (
@dataclass(frozen=True, kw_only=True)
class GrowattNumberEntityDescription(NumberEntityDescription):
class GrowattNumberEntityDescription(NumberEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt number entity."""
api_key: str
write_key: str | None = None # Parameter ID for writing (if different from api_key)
@@ -130,7 +130,6 @@ class GrowattNumber(CoordinatorEntity[GrowattCoordinator], NumberEntity):
identifiers={(DOMAIN, coordinator.device_id)},
manufacturer="Growatt",
name=coordinator.device_id,
serial_number=coordinator.device_id,
)
@property

View File

@@ -32,7 +32,9 @@ rules:
test-coverage: done
# Gold
devices: done
devices:
status: todo
comment: Add serial_number field to DeviceInfo in sensor, number, and switch platforms using device_id/serial_id.
diagnostics: todo
discovery-update-info: todo
discovery: todo
@@ -44,12 +46,16 @@ rules:
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-category:
status: todo
comment: Add EntityCategory.DIAGNOSTIC to temperature and other diagnostic sensors. Merge GrowattRequiredKeysMixin into GrowattSensorEntityDescription using kw_only=True.
entity-device-class:
status: todo
comment: Replace custom precision field with suggested_display_precision to preserve full data granularity.
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt

View File

@@ -2,14 +2,12 @@
from __future__ import annotations
from datetime import date, datetime
import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from ..const import DOMAIN
@@ -101,18 +99,24 @@ class GrowattSensor(CoordinatorEntity[GrowattCoordinator], SensorEntity):
self.entity_description = description
self._attr_unique_id = unique_id
self._attr_icon = "mdi:solar-power"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_id)},
manufacturer="Growatt",
name=name,
serial_number=serial_id,
)
@property
def native_value(self) -> StateType | date | datetime:
def native_value(self) -> str | int | float | None:
"""Return the state of the sensor."""
return self.coordinator.get_data(self.entity_description)
result = self.coordinator.get_data(self.entity_description)
if (
isinstance(result, (int, float))
and self.entity_description.precision is not None
):
result = round(result, self.entity_description.precision)
return result
@property
def native_unit_of_measurement(self) -> str | None:

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -23,7 +22,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_energy_total",
@@ -31,7 +30,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="powerTotal",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
suggested_display_precision=1,
precision=1,
state_class=SensorStateClass.TOTAL,
),
GrowattSensorEntityDescription(
@@ -41,7 +40,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_1",
@@ -50,7 +49,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_1",
@@ -59,7 +58,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_voltage_input_2",
@@ -68,7 +67,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_2",
@@ -77,7 +76,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_2",
@@ -86,7 +85,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_voltage_input_3",
@@ -95,7 +94,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_amperage_input_3",
@@ -104,7 +103,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_wattage_input_3",
@@ -113,7 +112,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_internal_wattage",
@@ -122,7 +121,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_reactive_voltage",
@@ -131,9 +130,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_inverter_reactive_amperage",
@@ -142,9 +139,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_frequency",
@@ -153,9 +148,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_current_wattage",
@@ -164,7 +157,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_current_reactive_wattage",
@@ -173,9 +166,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_ipm_temperature",
@@ -184,9 +175,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="inverter_temperature",
@@ -195,8 +184,6 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
)

View File

@@ -7,11 +7,18 @@ from dataclasses import dataclass
from homeassistant.components.sensor import SensorEntityDescription
@dataclass(frozen=True, kw_only=True)
class GrowattSensorEntityDescription(SensorEntityDescription):
"""Describes Growatt sensor entity."""
@dataclass(frozen=True)
class GrowattRequiredKeysMixin:
"""Mixin for required keys."""
api_key: str
@dataclass(frozen=True)
class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt sensor entity."""
precision: int | None = None
currency: bool = False
previous_value_drop_threshold: float | None = None
never_resets: bool = False

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
@@ -91,8 +90,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
GrowattSensorEntityDescription(
key="sph_temperature_1",
@@ -101,8 +98,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
GrowattSensorEntityDescription(
key="sph_temperature_2",
@@ -111,8 +106,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
GrowattSensorEntityDescription(
key="sph_temperature_3",
@@ -121,8 +114,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
GrowattSensorEntityDescription(
key="sph_temperature_4",
@@ -131,8 +122,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
GrowattSensorEntityDescription(
key="sph_temperature_5",
@@ -141,8 +130,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
# Values from 'sph_energy' API call
GrowattSensorEntityDescription(

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -190,7 +189,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_pv_charging_voltage",
@@ -199,7 +198,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_pv_charging_voltage_2",
@@ -208,7 +207,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_ac_input_frequency_out",
@@ -217,9 +216,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_output_voltage",
@@ -228,7 +225,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_ac_output_frequency",
@@ -237,9 +234,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_current_PV",
@@ -248,7 +243,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_current_1",
@@ -257,7 +252,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_current_2",
@@ -266,7 +261,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_grid_amperage_input",
@@ -275,7 +270,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_grid_out_current",
@@ -284,7 +279,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_battery_voltage",
@@ -293,7 +288,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
GrowattSensorEntityDescription(
key="storage_load_percentage",
@@ -302,6 +297,6 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
precision=2,
),
)

View File

@@ -8,7 +8,6 @@ from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -27,7 +26,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total",
@@ -36,7 +35,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -46,7 +45,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -56,7 +55,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_1",
@@ -64,7 +63,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv1",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_1",
@@ -72,7 +71,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv1",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_1",
@@ -81,7 +80,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_2",
@@ -90,7 +89,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -100,7 +99,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_2",
@@ -108,7 +107,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv2",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_2",
@@ -116,7 +115,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv2",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_2",
@@ -125,7 +124,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_3",
@@ -134,7 +133,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -144,7 +143,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_3",
@@ -152,7 +151,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv3",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_3",
@@ -160,7 +159,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv3",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_3",
@@ -169,7 +168,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_energy_total_input_4",
@@ -178,7 +177,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
never_resets=True,
),
GrowattSensorEntityDescription(
@@ -188,7 +187,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_voltage_input_4",
@@ -196,7 +195,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vpv4",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_amperage_input_4",
@@ -204,7 +203,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ipv4",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_wattage_input_4",
@@ -213,7 +212,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_solar_generation_today",
@@ -222,7 +221,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_solar_generation_total",
@@ -240,7 +239,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_reactive_voltage",
@@ -248,9 +247,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="vacrs",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_frequency",
@@ -258,9 +255,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="fac",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_current_wattage",
@@ -269,7 +264,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_1",
@@ -277,9 +272,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp1",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_2",
@@ -287,9 +280,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp2",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_3",
@@ -297,9 +288,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp3",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_4",
@@ -307,9 +296,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp4",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_temperature_5",
@@ -317,9 +304,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="temp5",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_all_batteries_discharge_today",
@@ -471,7 +456,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_pac_to_user_total",
@@ -480,7 +465,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_pac_to_grid_total",
@@ -489,7 +474,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_system_production_today",
@@ -498,7 +483,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_system_production_total",
@@ -508,7 +493,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_self_consumption_today",
@@ -517,7 +502,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_self_consumption_total",
@@ -527,7 +512,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_import_from_grid_today",
@@ -536,7 +521,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_import_from_grid_total",
@@ -546,7 +531,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_batteries_charged_from_grid_today",
@@ -555,7 +540,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_batteries_charged_from_grid_total",
@@ -565,7 +550,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
never_resets=True,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_p_system",
@@ -574,7 +559,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
GrowattSensorEntityDescription(
key="tlx_p_self",
@@ -583,6 +568,6 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
precision=1,
),
)

View File

@@ -18,6 +18,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GrowattConfigEntry, GrowattCoordinator
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
_LOGGER = logging.getLogger(__name__)
@@ -27,10 +28,9 @@ PARALLEL_UPDATES = (
@dataclass(frozen=True, kw_only=True)
class GrowattSwitchEntityDescription(SwitchEntityDescription):
class GrowattSwitchEntityDescription(SwitchEntityDescription, GrowattRequiredKeysMixin):
"""Describes Growatt switch entity."""
api_key: str
write_key: str | None = None # Parameter ID for writing (if different from api_key)
@@ -87,7 +87,6 @@ class GrowattSwitch(CoordinatorEntity[GrowattCoordinator], SwitchEntity):
identifiers={(DOMAIN, coordinator.device_id)},
manufacturer="Growatt",
name=coordinator.device_id,
serial_number=coordinator.device_id,
)
@property

View File

@@ -119,6 +119,7 @@ from .coordinator import (
get_core_stats,
get_host_info,
get_info,
get_issues_info,
get_network_info,
get_os_info,
get_store,
@@ -157,6 +158,7 @@ __all__ = [
"get_core_stats",
"get_host_info",
"get_info",
"get_issues_info",
"get_network_info",
"get_os_info",
"get_store",

View File

@@ -132,7 +132,6 @@ ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
ISSUE_KEY_ADDON_DEPRECATED_ARCH = "issue_addon_deprecated_arch_addon"
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
@@ -173,7 +172,6 @@ EXTRA_PLACEHOLDERS = {
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
},
ISSUE_KEY_ADDON_DEPRECATED: HELP_URLS,
ISSUE_KEY_ADDON_DEPRECATED_ARCH: HELP_URLS,
}

View File

@@ -47,7 +47,6 @@ from .const import (
EVENT_SUPPORTED_CHANGED,
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
@@ -91,7 +90,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
}
_LOGGER = logging.getLogger(__name__)
@@ -255,10 +253,9 @@ class SupervisorIssues:
def add_issue(self, issue: Issue) -> None:
"""Add or update an issue in the list. Create or update a repair if necessary."""
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
placeholders: dict[str, str] = {}
if not issue.suggestions and issue.key in EXTRA_PLACEHOLDERS:
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[issue.key].copy()
else:
placeholders = {}
placeholders |= EXTRA_PLACEHOLDERS[issue.key]
if issue.reference:
placeholders[PLACEHOLDER_KEY_REFERENCE] = issue.reference

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["aiohasupervisor==0.4.2"],
"requirements": ["aiohasupervisor==0.4.1"],
"single_config_entry": true
}

View File

@@ -21,7 +21,6 @@ from .const import (
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DEPRECATED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
@@ -65,16 +64,11 @@ class SupervisorIssueRepairFlow(RepairsFlow):
@property
def description_placeholders(self) -> dict[str, str] | None:
"""Get description placeholders for steps."""
if not self.issue:
return None
if self.issue.key in EXTRA_PLACEHOLDERS:
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[self.issue.key].copy()
else:
placeholders = {}
if self.issue.reference:
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
placeholders = {}
if self.issue:
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
if self.issue.reference:
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
return placeholders or None
@@ -238,7 +232,6 @@ async def async_create_fix_flow(
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
}:
return AddonIssueRepairFlow(hass, issue_id)

View File

@@ -85,19 +85,6 @@
},
"title": "Installed app is deprecated"
},
"issue_addon_deprecated_arch_addon": {
"fix_flow": {
"abort": {
"apply_suggestion_fail": "Could not uninstall the app. Check the Supervisor logs for more details."
},
"step": {
"addon_execute_remove": {
"description": "App {addon} only supports architectures and/or machines which are no longer supported by Home Assistant. It will stop working in a future release.\n\nSelecting **Submit** will uninstall this deprecated app. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
}
}
},
"title": "Installed app is built for unsupported architectures and/or machines"
},
"issue_addon_detached_addon_missing": {
"description": "Repository for app {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the Home Assistant Supervisor may not be able to build/download the resources required.\n\nPlease check the [app's documentation]({addon_url}) for installation instructions and add the repository to the store.",
"title": "Missing repository for an installed app"

View File

@@ -72,6 +72,13 @@ class HuumDevice(HuumBaseEntity, ClimateEntity):
return HVACMode.HEAT
return HVACMode.OFF
@property
def icon(self) -> str:
"""Return nice icon for heater."""
if self.hvac_mode == HVACMode.HEAT:
return "mdi:radiator"
return "mdi:radiator-off"
@property
def current_temperature(self) -> int | None:
"""Return the current temperature."""

View File

@@ -45,6 +45,8 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
)
await huum.status()
except Forbidden, NotAuthenticated:
# Most likely Forbidden as that is what is returned from `.status()` with bad creds
_LOGGER.error("Could not log in to Huum with given credentials")
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unknown error")

View File

@@ -54,6 +54,7 @@ class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
try:
return await self.huum.status()
except (Forbidden, NotAuthenticated) as err:
_LOGGER.error("Could not log in to Huum with given credentials")
raise UpdateFailed(
"Could not log in to Huum with given credentials"
) from err

View File

@@ -7,7 +7,11 @@ rules:
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
config-flow:
status: done
comment: |
PLANNED: Remove _LOGGER.error call from config_flow.py — the error
message is redundant with the errors dict entry.
dependency-transparency: done
docs-actions:
status: exempt
@@ -36,7 +40,11 @@ rules:
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
log-when-unavailable:
status: done
comment: |
PLANNED: Remove _LOGGER.error from coordinator.py — the message is already
passed to UpdateFailed, so logging it separately is redundant.
parallel-updates: done
reauthentication-flow: todo
test-coverage:
@@ -66,7 +74,11 @@ rules:
comment: All entities are core functionality.
entity-translations: done
exception-translations: todo
icon-translations: done
icon-translations:
status: done
comment: |
PLANNED: Remove the icon property from climate.py — entities should not set
custom icons. Use HA defaults or icon translations instead.
reconfiguration-flow: todo
repair-issues:
status: exempt

View File

@@ -22,13 +22,7 @@
"description": "Authenticate against IntelliFire cloud"
},
"pick_cloud_device": {
"data": {
"serial": "Fireplace serial number"
},
"data_description": {
"serial": "Serial number of the fireplace to configure"
},
"description": "Select fireplace by serial number.",
"description": "Select fireplace by serial number:",
"title": "Configure fireplace"
}
}
@@ -165,10 +159,6 @@
"control_mode": "Send commands to",
"read_mode": "Read data from"
},
"data_description": {
"control_mode": "Whether to send fireplace commands via the `Local` or `Cloud` API",
"read_mode": "Whether to read fireplace state via the `Local` or `Cloud` API"
},
"description": "Some users find that their fireplace hardware prioritizes `Cloud` communication and may experience timeouts with `Local` control. If you encounter connectivity issues, try switching to `Cloud` for the affected endpoint.",
"title": "Endpoint selection"
}

View File

@@ -151,9 +151,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
return value
def get_options_map(
self, command: str, *, snake_case: bool = False
) -> dict[str, str]:
def get_options_map(self, command: str) -> dict[str, str]:
"""Get the available options for a command."""
capabilities = self.capabilities.get(command, {})
@@ -164,10 +162,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
values = list(capabilities.get("parameter", {}).get("read", {}).values())
options = {v: v.translate(TRANSLATIONS) for v in values}
if snake_case:
return {k: v.replace("-", "_") for k, v in options.items()}
return options
return {v: v.translate(TRANSLATIONS) for v in values}
def supports(self, command: type[Command]) -> bool:
"""Check if the device supports a command."""

View File

@@ -18,9 +18,6 @@
"dynamic_control": {
"default": "mdi:lightbulb-on-outline"
},
"hdr_processing": {
"default": "mdi:image-filter-hdr-outline"
},
"input": {
"default": "mdi:hdmi-port"
},
@@ -29,9 +26,6 @@
},
"light_power": {
"default": "mdi:lightbulb-on-outline"
},
"picture_mode": {
"default": "mdi:movie-roll"
}
},
"sensor": {

View File

@@ -20,7 +20,6 @@ class JvcProjectorSelectDescription(SelectEntityDescription):
"""Describes JVC Projector select entities."""
command: type[Command]
snake_case_states: bool = False
SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
@@ -50,18 +49,6 @@ SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
command=cmd.Anamorphic,
entity_registry_enabled_default=False,
),
JvcProjectorSelectDescription(
key="hdr_processing",
command=cmd.HdrProcessing,
entity_registry_enabled_default=False,
snake_case_states=True,
),
JvcProjectorSelectDescription(
key="picture_mode",
command=cmd.PictureMode,
entity_registry_enabled_default=False,
snake_case_states=True,
),
)
@@ -97,8 +84,7 @@ class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity):
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
self._options_map: dict[str, str] = coordinator.get_options_map(
self.command.name,
snake_case=description.snake_case_states,
self.command.name
)
@property

View File

@@ -7,19 +7,16 @@ from dataclasses import dataclass
from jvcprojector import Command, command as cmd
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
from .entity import JvcProjectorEntity
from .util import deprecate_entity
@dataclass(frozen=True, kw_only=True)
@@ -87,29 +84,12 @@ async def async_setup_entry(
) -> None:
"""Set up the JVC Projector platform from a config entry."""
coordinator = entry.runtime_data
entity_registry = er.async_get(hass)
entities: list[JvcProjectorSensorEntity] = []
for description in SENSORS:
if not coordinator.supports(description.command):
continue
if description.key in (
"hdr_processing",
"picture_mode",
) and not deprecate_entity(
hass,
entity_registry,
SENSOR_DOMAIN,
f"{coordinator.unique_id}_{description.key}",
f"deprecated_sensor_{entry.entry_id}_{description.key}",
"deprecated_sensor",
f"{coordinator.unique_id}_{description.key}",
f"select.jvc_projector_{description.key}",
):
continue
entities.append(JvcProjectorSensorEntity(coordinator, description))
async_add_entities(entities)
async_add_entities(
JvcProjectorSensorEntity(coordinator, description)
for description in SENSORS
if coordinator.supports(description.command)
)
class JvcProjectorSensorEntity(JvcProjectorEntity, SensorEntity):

View File

@@ -71,15 +71,6 @@
"off": "[%key:common::state::off%]"
}
},
"hdr_processing": {
"name": "HDR Processing",
"state": {
"frame_by_frame": "Frame-by-Frame",
"hdr10p": "HDR10+",
"scene_by_scene": "Scene-by-Scene",
"static": "Static"
}
},
"input": {
"name": "Input",
"state": {
@@ -110,23 +101,6 @@
"mid": "[%key:common::state::medium%]",
"normal": "[%key:common::state::normal%]"
}
},
"picture_mode": {
"name": "Picture Mode",
"state": {
"frame_adapt_hdr": "Frame Adapt HDR",
"frame_adapt_hdr2": "Frame Adapt HDR2",
"frame_adapt_hdr3": "Frame Adapt HDR3",
"hdr1": "HDR1",
"hdr10": "HDR10",
"hdr10_ll": "HDR10 LL",
"hdr2": "HDR2",
"last_setting": "Last setting",
"pana_pq": "Pana PQ",
"user_4": "User 4",
"user_5": "User 5",
"user_6": "User 6"
}
}
},
"sensor": {
@@ -182,7 +156,7 @@
"hdr10": "HDR10",
"hdr10-ll": "HDR10 LL",
"hdr2": "HDR2",
"last-setting": "Last setting",
"last-setting": "Last Setting",
"pana-pq": "Pana PQ",
"user-4": "User 4",
"user-5": "User 5",
@@ -208,15 +182,5 @@
"name": "Low latency mode"
}
}
},
"issues": {
"deprecated_sensor": {
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nUpdate your dashboards, templates, automations and scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
"title": "Deprecated sensor detected"
},
"deprecated_sensor_scripts": {
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nUpdate the above automations or scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
"title": "[%key:component::jvc_projector::issues::deprecated_sensor::title%]"
}
}
}

View File

@@ -1,104 +0,0 @@
"""Utility helpers for the jvc_projector integration."""
from __future__ import annotations
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .const import DOMAIN
def deprecate_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
platform_domain: str,
entity_unique_id: str,
issue_id: str,
issue_string: str,
replacement_entity_unique_id: str,
replacement_entity_id: str,
version: str = "2026.9.0",
) -> bool:
"""Create an issue for deprecated entities."""
if entity_id := entity_registry.async_get_entity_id(
platform_domain, DOMAIN, entity_unique_id
):
entity_entry = entity_registry.async_get(entity_id)
if not entity_entry:
async_delete_issue(hass, DOMAIN, issue_id)
return False
items = get_automations_and_scripts_using_entity(hass, entity_id)
if entity_entry.disabled and not items:
entity_registry.async_remove(entity_id)
async_delete_issue(hass, DOMAIN, issue_id)
return False
translation_key = issue_string
placeholders = {
"entity_id": entity_id,
"entity_name": entity_entry.name or entity_entry.original_name or "Unknown",
"replacement_entity_id": (
entity_registry.async_get_entity_id(
Platform.SELECT, DOMAIN, replacement_entity_unique_id
)
or replacement_entity_id
),
}
if items:
translation_key = f"{translation_key}_scripts"
placeholders["items"] = "\n".join(items)
async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version=version,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders=placeholders,
)
return True
async_delete_issue(hass, DOMAIN, issue_id)
return False
def get_automations_and_scripts_using_entity(
hass: HomeAssistant,
entity_id: str,
) -> list[str]:
"""Get automations and scripts using an entity."""
# These helpers return referencing automation/script entity IDs.
automations = automations_with_entity(hass, entity_id)
scripts = scripts_with_entity(hass, entity_id)
if not automations and not scripts:
return []
entity_registry = er.async_get(hass)
items: list[str] = []
for integration, entities in (
("automation", automations),
("script", scripts),
):
for used_entity_id in entities:
# Prefer entity-registry metadata so we can render edit links.
if item := entity_registry.async_get(used_entity_id):
items.append(
f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})"
)
else:
# Keep unresolved references as plain text so they still count as usage.
items.append(f"- `{used_entity_id}`")
return items

View File

@@ -1,25 +0,0 @@
"""Provides conditions for motion."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_state_condition
_MOTION_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.MOTION)
}
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for motion."""
return CONDITIONS

View File

@@ -1,24 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_detected:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: motion
is_not_detected:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: motion

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_detected": {
"condition": "mdi:motion-sensor"
},
"is_not_detected": {
"condition": "mdi:motion-sensor-off"
}
},
"triggers": {
"cleared": {
"trigger": "mdi:motion-sensor-off"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted motion sensors.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted motion sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_detected": {
"description": "Tests if one or more motion sensors are detecting motion.",
"fields": {
"behavior": {
"description": "[%key:component::motion::common::condition_behavior_description%]",
"name": "[%key:component::motion::common::condition_behavior_name%]"
}
},
"name": "Motion is detected"
},
"is_not_detected": {
"description": "Tests if one or more motion sensors are not detecting motion.",
"fields": {
"behavior": {
"description": "[%key:component::motion::common::condition_behavior_description%]",
"name": "[%key:component::motion::common::condition_behavior_name%]"
}
},
"name": "Motion is not detected"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -18,8 +18,6 @@ ABBREVIATIONS = {
"bri_stat_t": "brightness_state_topic",
"bri_tpl": "brightness_template",
"bri_val_tpl": "brightness_value_template",
"cln_segmnts_cmd_t": "clean_segments_command_topic",
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
"clr_temp_cmd_tpl": "color_temp_command_template",
"clrm_stat_t": "color_mode_state_topic",
"clrm_val_tpl": "color_mode_value_template",
@@ -187,7 +185,6 @@ ABBREVIATIONS = {
"rgbww_cmd_t": "rgbww_command_topic",
"rgbww_stat_t": "rgbww_state_topic",
"rgbww_val_tpl": "rgbww_value_template",
"segmnts": "segments",
"send_cmd_t": "send_command_topic",
"send_if_off": "send_if_off",
"set_fan_spd_t": "set_fan_speed_topic",

View File

@@ -1484,7 +1484,6 @@ class MqttEntity(
self._config = config
self._setup_from_config(self._config)
self._setup_common_attributes_from_config(self._config)
self._process_entity_update()
# Prepare MQTT subscriptions
self.attributes_prepare_discovery_update(config)
@@ -1587,10 +1586,6 @@ class MqttEntity(
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
@callback
def _process_entity_update(self) -> None:
"""Process an entity discovery update."""
@abstractmethod
@callback
def _prepare_subscribe_topics(self) -> None:

View File

@@ -10,13 +10,12 @@ import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -28,7 +27,7 @@ from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import MqttCommandTemplate, ReceiveMessage
from .models import ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic
@@ -53,9 +52,6 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
STATE_CLEANING: VacuumActivity.CLEANING,
}
CONF_SEGMENTS = "segments"
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
@@ -141,39 +137,8 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
def validate_clean_area_config(config: ConfigType) -> ConfigType:
"""Check for a valid configuration and check segments."""
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
):
raise vol.Invalid(
f"Options `{CONF_SEGMENTS}` and "
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
)
segments: list[str]
if segments := config[CONF_SEGMENTS]:
if not config.get(CONF_UNIQUE_ID):
raise vol.Invalid(
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
)
unique_segments: set[str] = set()
for segment in segments:
segment_id, _, _ = segment.partition(".")
if not segment_id or segment_id in unique_segments:
raise vol.Invalid(
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
f"unique segment ID '{segment_id}'. Got {segments}"
)
unique_segments.add(segment_id)
return config
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
@@ -199,10 +164,7 @@ _BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
DISCOVERY_SCHEMA = vol.All(
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
)
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
async def async_setup_entry(
@@ -229,11 +191,9 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
_segments: list[Segment]
_command_topic: str | None
_set_fan_speed_topic: str | None
_send_command_topic: str | None
_clean_segments_command_topic: str
_payloads: dict[str, str | None]
def __init__(
@@ -269,23 +229,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
self._attr_supported_features = _strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
)
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
segments: list[str] = config[CONF_SEGMENTS]
self._segments = [
Segment(id=segment_id, name=name or segment_id)
for segment_id, _, name in [
segment.partition(".") for segment in segments
]
]
self._clean_segments_command_topic = config[
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
]
self._clean_segments_command_template = MqttCommandTemplate(
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
entity=self,
).async_render
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
self._command_topic = config.get(CONF_COMMAND_TOPIC)
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
@@ -303,20 +246,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
)
}
@callback
def _process_entity_update(self) -> None:
"""Check vacuum segments with registry entry."""
if (
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
and (last_seen := self.last_seen_segments) is not None
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
):
self.async_create_segments_issue()
async def mqtt_async_added_to_hass(self) -> None:
"""Check vacuum segments with registry entry."""
self._process_entity_update()
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
@@ -348,19 +277,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""(Re)Subscribe to topics."""
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Perform an area clean."""
await self.async_publish_with_config(
self._clean_segments_command_topic,
self._clean_segments_command_template(
json_dumps(segment_ids), {"value": segment_ids}
),
)
async def async_get_segments(self) -> list[Segment]:
"""Return the available segments."""
return self._segments
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
"""Publish a command."""
if self._command_topic is None:

View File

@@ -1,25 +0,0 @@
"""Provides conditions for occupancy."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_state_condition
_OCCUPANCY_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.OCCUPANCY)
}
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_OFF),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for occupancy."""
return CONDITIONS

View File

@@ -1,24 +0,0 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_detected:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: occupancy
is_not_detected:
fields: *condition_common_fields
target:
entity:
- domain: binary_sensor
device_class: occupancy

View File

@@ -1,12 +1,4 @@
{
"conditions": {
"is_detected": {
"condition": "mdi:home-account"
},
"is_not_detected": {
"condition": "mdi:home-outline"
}
},
"triggers": {
"cleared": {
"trigger": "mdi:home-outline"

View File

@@ -1,39 +1,9 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted occupancy sensors.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_detected": {
"description": "Tests if one or more occupancy sensors are detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::occupancy::common::condition_behavior_description%]",
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
}
},
"name": "Occupancy is detected"
},
"is_not_detected": {
"description": "Tests if one or more occupancy sensors are not detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::occupancy::common::condition_behavior_description%]",
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
}
},
"name": "Occupancy is not detected"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "platinum",
"quality_scale": "silver",
"requirements": ["opower==0.17.0"]
}

View File

@@ -58,7 +58,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
@@ -71,7 +71,7 @@ rules:
status: exempt
comment: The integration has no user-configurable options that are not authentication-related.
repair-issues: done
stale-devices: done
stale-devices: todo
# Platinum
async-dependency: done

View File

@@ -15,8 +15,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import EntityCategory, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -208,102 +207,48 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Opower sensor."""
coordinator = entry.runtime_data
created_sensors: set[tuple[str, str]] = set()
@callback
def _update_entities() -> None:
"""Update entities."""
new_entities: list[OpowerSensor] = []
current_account_device_ids: set[str] = set()
current_account_ids: set[str] = set()
for opower_data in coordinator.data.values():
account = opower_data.account
forecast = opower_data.forecast
device_id = (
f"{coordinator.api.utility.subdomain()}_{account.utility_account_id}"
)
current_account_device_ids.add(device_id)
current_account_ids.add(account.utility_account_id)
device = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=f"{account.meter_type.name} account {account.utility_account_id}",
manufacturer="Opower",
model=coordinator.api.utility.name(),
entry_type=DeviceEntryType.SERVICE,
)
sensors: tuple[OpowerEntityDescription, ...] = COMMON_SENSORS
if (
account.meter_type == MeterType.ELEC
and forecast is not None
and forecast.unit_of_measure == UnitOfMeasure.KWH
):
sensors += ELEC_SENSORS
elif (
account.meter_type == MeterType.GAS
and forecast is not None
and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF]
):
sensors += GAS_SENSORS
for sensor in sensors:
sensor_key = (account.utility_account_id, sensor.key)
if sensor_key in created_sensors:
continue
created_sensors.add(sensor_key)
new_entities.append(
OpowerSensor(
coordinator,
sensor,
account.utility_account_id,
device,
device_id,
)
)
if new_entities:
async_add_entities(new_entities)
# Remove any registered devices not in the current coordinator data
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
for device_entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
entities: list[OpowerSensor] = []
opower_data_list = coordinator.data.values()
for opower_data in opower_data_list:
account = opower_data.account
forecast = opower_data.forecast
device_id = (
f"{coordinator.api.utility.subdomain()}_{account.utility_account_id}"
)
device = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=f"{account.meter_type.name} account {account.utility_account_id}",
manufacturer="Opower",
model=coordinator.api.utility.name(),
entry_type=DeviceEntryType.SERVICE,
)
sensors: tuple[OpowerEntityDescription, ...] = COMMON_SENSORS
if (
account.meter_type == MeterType.ELEC
and forecast is not None
and forecast.unit_of_measure == UnitOfMeasure.KWH
):
device_domain_ids = {
identifier[1]
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
}
if not device_domain_ids:
# This device has no Opower identifiers; it may be a merged/shared
# device owned by another integration. Do not alter it here.
continue
if not device_domain_ids.isdisjoint(current_account_device_ids):
continue # device is still active
# Device is stale — remove its entities then detach it
for entity_entry in er.async_entries_for_device(
entity_registry, device_entry.id, include_disabled_entities=True
):
if entity_entry.config_entry_id != entry.entry_id:
continue
entity_registry.async_remove(entity_entry.entity_id)
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=entry.entry_id
sensors += ELEC_SENSORS
elif (
account.meter_type == MeterType.GAS
and forecast is not None
and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF]
):
sensors += GAS_SENSORS
entities.extend(
OpowerSensor(
coordinator,
sensor,
account.utility_account_id,
device,
device_id,
)
for sensor in sensors
)
# Prune sensor tracking for accounts that are no longer present
if created_sensors:
stale_sensor_keys = {
sensor_key
for sensor_key in created_sensors
if sensor_key[0] not in current_account_ids
}
if stale_sensor_keys:
created_sensors.difference_update(stale_sensor_keys)
_update_entities()
entry.async_on_unload(coordinator.async_add_listener(_update_entities))
async_add_entities(entities)
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
@@ -327,11 +272,6 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
self._attr_device_info = device
self.utility_account_id = utility_account_id
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.utility_account_id in self.coordinator.data
@property
def native_value(self) -> StateType | date | datetime:
"""Return the state."""

View File

@@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool:

View File

@@ -8,20 +8,6 @@
"default": "mdi:arrow-expand-left"
}
},
"number": {
"display_brightness": {
"default": "mdi:brightness-6",
"state": {
"0": "mdi:brightness-2",
"1": "mdi:brightness-4",
"2": "mdi:brightness-4",
"3": "mdi:brightness-5",
"4": "mdi:brightness-5",
"5": "mdi:brightness-7",
"6": "mdi:brightness-7"
}
}
},
"sensor": {
"inside_temperature": {
"default": "mdi:home-thermometer"

View File

@@ -1,80 +0,0 @@
"""Number platform for Prana integration."""
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
PARALLEL_UPDATES = 1
class PranaNumberType(StrEnum):
"""Enumerates Prana number types exposed by the device API."""
DISPLAY_BRIGHTNESS = "display_brightness"
@dataclass(frozen=True, kw_only=True)
class PranaNumberEntityDescription(NumberEntityDescription):
"""Description of a Prana number entity."""
key: PranaNumberType
value_fn: Callable[[PranaCoordinator], float | None]
set_value_fn: Callable[[Any, float], Any]
ENTITIES: tuple[PranaNumberEntityDescription, ...] = (
PranaNumberEntityDescription(
key=PranaNumberType.DISPLAY_BRIGHTNESS,
translation_key="display_brightness",
native_min_value=0,
native_max_value=6,
native_step=1,
mode=NumberMode.SLIDER,
entity_category=EntityCategory.CONFIG,
value_fn=lambda coord: coord.data.brightness,
set_value_fn=lambda api, val: api.set_brightness(
0 if val == 0 else 2 ** (int(val) - 1)
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: PranaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Prana number entities from a config entry."""
async_add_entities(
PranaNumber(entry.runtime_data, entity_description)
for entity_description in ENTITIES
)
class PranaNumber(PranaBaseEntity, NumberEntity):
"""Representation of a Prana number entity."""
entity_description: PranaNumberEntityDescription
@property
def native_value(self) -> float | None:
"""Return the entity value."""
return self.entity_description.value_fn(self.coordinator)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.entity_description.set_value_fn(self.coordinator.api_client, value)
await self.coordinator.async_refresh()

View File

@@ -49,11 +49,6 @@
}
}
},
"number": {
"display_brightness": {
"name": "Display brightness"
}
},
"sensor": {
"inside_temperature": {
"name": "Inside temperature"

View File

@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
ProxmoxVMButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
ProxmoxContainerButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,

View File

@@ -4,11 +4,10 @@ from __future__ import annotations
import mimetypes
from aiodns.error import DNSError
import pycountry
from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
from radios import FilterBy, Order, RadioBrowser, Station
from homeassistant.components.media_player import BrowseError, MediaClass, MediaType
from homeassistant.components.media_player import MediaClass, MediaType
from homeassistant.components.media_source import (
BrowseMediaSource,
MediaSource,
@@ -16,7 +15,6 @@ from homeassistant.components.media_source import (
PlayMedia,
Unresolvable,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.location import vincenty
@@ -57,20 +55,9 @@ class RadioMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve selected Radio station to a streaming URL."""
if self.entry.state != ConfigEntryState.LOADED:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
try:
station = await radios.station(uuid=item.identifier)
except (DNSError, RadioBrowserError) as e:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
station = await radios.station(uuid=item.identifier)
if not station:
raise Unresolvable("Radio station is no longer available")
@@ -87,37 +74,25 @@ class RadioMediaSource(MediaSource):
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if self.entry.state != ConfigEntryState.LOADED:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
try:
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
except (DNSError, RadioBrowserError) as e:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
@callback
@staticmethod

View File

@@ -5,13 +5,5 @@
"description": "Do you want to add Radio Browser to Home Assistant?"
}
}
},
"exceptions": {
"config_entry_not_ready": {
"message": "Radio Browser integration is not ready"
},
"radio_browser_error": {
"message": "Error occurred while communicating with Radio Browser"
}
}
}

View File

@@ -18,7 +18,6 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
]

View File

@@ -1,144 +0,0 @@
"""Support for Renault number entities."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, cast
from renault_api.kamereon.models import KamereonVehicleBatterySocData
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RenaultConfigEntry
from .const import DOMAIN
from .entity import RenaultDataEntity, RenaultDataEntityDescription
# Coordinator is used to centralize the data updates
# but renault servers are unreliable and it's safer to queue action calls
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class RenaultNumberEntityDescription(
NumberEntityDescription, RenaultDataEntityDescription
):
"""Class describing Renault number entities."""
data_key: str
update_fn: Callable[[RenaultNumberEntity, float], Coroutine[Any, Any, None]]
async def _set_charge_limit_min(entity: RenaultNumberEntity, value: float) -> None:
"""Set the minimum SOC.
The target SOC is required to set the minimum SOC, so we need to fetch it first.
"""
if (data := entity.coordinator.data) is None or (
target_soc := data.socTarget
) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="battery_soc_unavailable",
)
await _set_charge_limits(entity, min_soc=round(value), target_soc=target_soc)
async def _set_charge_limit_target(entity: RenaultNumberEntity, value: float) -> None:
"""Set the target SOC.
The minimum SOC is required to set the target SOC, so we need to fetch it first.
"""
if (data := entity.coordinator.data) is None or (min_soc := data.socMin) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="battery_soc_unavailable",
)
await _set_charge_limits(entity, min_soc=min_soc, target_soc=round(value))
async def _set_charge_limits(
entity: RenaultNumberEntity, min_soc: int, target_soc: int
) -> None:
"""Set the minimum and target SOC.
Optimistically update local coordinator data so the new
limits are reflected immediately without a remote refresh,
as Renault servers may still cache old values.
"""
await entity.vehicle.set_battery_soc(min_soc=min_soc, target_soc=target_soc)
entity.coordinator.data.socMin = min_soc
entity.coordinator.data.socTarget = target_soc
entity.coordinator.async_set_updated_data(entity.coordinator.data)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: RenaultConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renault entities from config entry."""
entities: list[RenaultNumberEntity] = [
RenaultNumberEntity(vehicle, description)
for vehicle in config_entry.runtime_data.vehicles.values()
for description in NUMBER_TYPES
if description.coordinator in vehicle.coordinators
]
async_add_entities(entities)
class RenaultNumberEntity(
RenaultDataEntity[KamereonVehicleBatterySocData], NumberEntity
):
"""Mixin for number specific attributes."""
entity_description: RenaultNumberEntityDescription
@property
def native_value(self) -> float | None:
"""Return the entity value to represent the entity state."""
return cast(float | None, self._get_data_attr(self.entity_description.data_key))
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self.entity_description.update_fn(self, value)
NUMBER_TYPES: tuple[RenaultNumberEntityDescription, ...] = (
RenaultNumberEntityDescription(
key="charge_limit_min",
coordinator="battery_soc",
data_key="socMin",
update_fn=_set_charge_limit_min,
device_class=NumberDeviceClass.BATTERY,
native_min_value=15,
native_max_value=45,
native_step=5,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
translation_key="charge_limit_min",
),
RenaultNumberEntityDescription(
key="charge_limit_target",
coordinator="battery_soc",
data_key="socTarget",
update_fn=_set_charge_limit_target,
device_class=NumberDeviceClass.BATTERY,
native_min_value=55,
native_max_value=100,
native_step=5,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
translation_key="charge_limit_target",
),
)

View File

@@ -174,13 +174,6 @@ class RenaultVehicleProxy:
"""Stop vehicle charge."""
return await self._vehicle.set_charge_stop()
@with_error_wrapping
async def set_battery_soc(
self, min_soc: int, target_soc: int
) -> models.KamereonVehicleBatterySocActionData:
"""Set vehicle battery SoC levels."""
return await self._vehicle.set_battery_soc(min=min_soc, target=target_soc)
@with_error_wrapping
async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData:
"""Stop vehicle ac."""
@@ -277,10 +270,4 @@ COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = (
key="pressure",
update_method=lambda x: x.get_tyre_pressure,
),
RenaultCoordinatorDescription(
endpoint="soc-levels",
key="battery_soc",
requires_electricity=True,
update_method=lambda x: x.get_battery_soc,
),
)

View File

@@ -94,14 +94,6 @@
"name": "[%key:common::config_flow::data::location%]"
}
},
"number": {
"charge_limit_min": {
"name": "Minimum charge level"
},
"charge_limit_target": {
"name": "Target charge level"
}
},
"select": {
"charge_mode": {
"name": "Charge mode",
@@ -207,9 +199,6 @@
}
},
"exceptions": {
"battery_soc_unavailable": {
"message": "Battery state of charge data is currently unavailable"
},
"invalid_device_id": {
"message": "No device with ID {device_id} was found"
},

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==4.25.0",
"python-roborock==4.20.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime
from datetime import date, datetime
import ephem
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from homeassistant.util.dt import utcnow
from .const import DOMAIN, TYPE_ASTRONOMICAL
@@ -50,7 +50,7 @@ async def async_setup_entry(
def get_season(
current_datetime: datetime, hemisphere: str, season_tracking_type: str
current_date: date, hemisphere: str, season_tracking_type: str
) -> str | None:
"""Calculate the current season."""
@@ -58,36 +58,22 @@ def get_season(
return None
if season_tracking_type == TYPE_ASTRONOMICAL:
spring_start = (
ephem.next_equinox(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
summer_start = (
ephem.next_solstice(str(current_datetime.year))
.datetime()
.replace(tzinfo=dt_util.UTC)
)
autumn_start = (
ephem.next_equinox(spring_start).datetime().replace(tzinfo=dt_util.UTC)
)
winter_start = (
ephem.next_solstice(summer_start).datetime().replace(tzinfo=dt_util.UTC)
)
spring_start = ephem.next_equinox(str(current_date.year)).datetime()
summer_start = ephem.next_solstice(str(current_date.year)).datetime()
autumn_start = ephem.next_equinox(spring_start).datetime()
winter_start = ephem.next_solstice(summer_start).datetime()
else:
spring_start = current_datetime.replace(
month=3, day=1, hour=0, minute=0, second=0, microsecond=0
)
spring_start = datetime(2017, 3, 1).replace(year=current_date.year)
summer_start = spring_start.replace(month=6)
autumn_start = spring_start.replace(month=9)
winter_start = spring_start.replace(month=12)
season = STATE_WINTER
if spring_start <= current_datetime < summer_start:
if spring_start <= current_date < summer_start:
season = STATE_SPRING
elif summer_start <= current_datetime < autumn_start:
elif summer_start <= current_date < autumn_start:
season = STATE_SUMMER
elif autumn_start <= current_datetime < winter_start:
elif autumn_start <= current_date < winter_start:
season = STATE_AUTUMN
# If user is located in the southern hemisphere swap the season
@@ -118,4 +104,6 @@ class SeasonSensorEntity(SensorEntity):
def update(self) -> None:
"""Update season."""
self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type)
self._attr_native_value = get_season(
utcnow().replace(tzinfo=None), self.hemisphere, self.type
)

View File

@@ -20,10 +20,5 @@
"select_previous": {
"service": "mdi:format-list-bulleted"
}
},
"triggers": {
"selection_changed": {
"trigger": "mdi:format-list-bulleted"
}
}
}

View File

@@ -76,11 +76,5 @@
"name": "Previous"
}
},
"title": "Select",
"triggers": {
"selection_changed": {
"description": "Triggers after the selected option of one or more dropdowns changes.",
"name": "Selection changed"
}
}
"title": "Select"
}

View File

@@ -1,40 +0,0 @@
"""Provides triggers for selects."""
from homeassistant.components.input_select import DOMAIN as INPUT_SELECT_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
)
from .const import DOMAIN
class SelectionChangedTrigger(EntityTriggerBase):
"""Trigger for select entity when its selection changes."""
_domain_specs = {DOMAIN: DomainSpec(), INPUT_SELECT_DOMAIN: DomainSpec()}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return from_state.state != to_state.state
def is_valid_state(self, state: State) -> bool:
"""Check if the new state is not invalid."""
return state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
TRIGGERS: dict[str, type[Trigger]] = {
"selection_changed": SelectionChangedTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for selects."""
return TRIGGERS

View File

@@ -1,5 +0,0 @@
selection_changed:
target:
entity:
- domain: select
- domain: input_select

View File

@@ -208,15 +208,6 @@ CAPABILITY_TO_SENSORS: dict[
supported_states_attributes=Attribute.SUPPORTED_COOKTOP_OPERATING_STATE,
)
},
Capability.SAMSUNG_CE_MICROFIBER_FILTER_STATUS: {
Attribute.STATUS: SmartThingsBinarySensorEntityDescription(
key=Attribute.STATUS,
translation_key="microfiber_filter_blockage",
is_on_key="blockage",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
)
},
}

View File

@@ -38,5 +38,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.7.2"]
"requirements": ["pysmartthings==3.7.0"]
}

View File

@@ -73,9 +73,6 @@
"keep_fresh_mode_active": {
"name": "Keep fresh mode active"
},
"microfiber_filter_blockage": {
"name": "Filter blockage"
},
"oven_cavity_status": {
"name": "Second cavity status"
},

View File

@@ -288,8 +288,6 @@ class SongpalEntity(MediaPlayerEntity):
self._volume_min = volume.minVolume
self._volume = volume.volume
self._volume_control = volume
if self._volume_max:
self._attr_volume_step = 1 / self._volume_max
self._attr_is_volume_muted = self._volume_control.is_muted
status = await self._dev.get_power()
@@ -383,6 +381,14 @@ class SongpalEntity(MediaPlayerEntity):
_LOGGER.debug("Setting volume to %s", volume)
return await self._volume_control.set_volume(volume)
async def async_volume_up(self) -> None:
"""Set volume up."""
return await self._volume_control.set_volume(self._volume + 1)
async def async_volume_down(self) -> None:
"""Set volume down."""
return await self._volume_control.set_volume(self._volume - 1)
async def async_turn_on(self) -> None:
"""Turn the device on."""
try:

View File

@@ -1,7 +1,7 @@
{
"domain": "starlink",
"name": "Starlink",
"codeowners": [],
"codeowners": ["@boswelja"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/starlink",
"integration_type": "device",

View File

@@ -8,7 +8,6 @@ from http import HTTPStatus
import logging
from xml.parsers.expat import ExpatError
from aiohttp import ClientSession
import voluptuous as vol
import xmltodict
@@ -151,7 +150,7 @@ async def async_setup_platform(
apikey = config[CONF_API_KEY]
bandwidthcap = config[CONF_TOTAL_BANDWIDTH]
ts_data = StartcaData(websession, apikey, bandwidthcap)
ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap)
ret = await ts_data.async_update()
if ret is False:
_LOGGER.error("Invalid Start.ca API key: %s", apikey)
@@ -177,9 +176,7 @@ async def async_setup_platform(
class StartcaSensor(SensorEntity):
"""Representation of Start.ca Bandwidth sensor."""
def __init__(
self, startcadata: StartcaData, name: str, description: SensorEntityDescription
) -> None:
def __init__(self, startcadata, name, description: SensorEntityDescription) -> None:
"""Initialize the sensor."""
self.entity_description = description
self.startcadata = startcadata
@@ -197,10 +194,9 @@ class StartcaSensor(SensorEntity):
class StartcaData:
"""Get data from Start.ca API."""
def __init__(
self, websession: ClientSession, api_key: str, bandwidth_cap: int
) -> None:
def __init__(self, loop, websession, api_key, bandwidth_cap):
"""Initialize the data object."""
self.loop = loop
self.websession = websession
self.api_key = api_key
self.bandwidth_cap = bandwidth_cap
@@ -219,7 +215,7 @@ class StartcaData:
return float(value) * 10**-9
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> bool:
async def async_update(self):
"""Get the Start.ca bandwidth data from the web service."""
_LOGGER.debug("Updating Start.ca usage data")
url = f"https://www.start.ca/support/usage/api?key={self.api_key}"

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
import copy
import dataclasses
import datetime
import logging
@@ -29,6 +28,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
from .const import (
ATTR_DESCRIPTION,
@@ -240,7 +240,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""An entity that represents a To-do list."""
_attr_todo_items: list[TodoItem] | None = None
_update_listeners: list[Callable[[list[TodoItem]], None]] | None = None
_update_listeners: list[Callable[[list[JsonValueType] | None], None]] | None = None
@property
def state(self) -> int | None:
@@ -281,9 +281,13 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@callback
def async_subscribe_updates(
self, listener: Callable[[list[TodoItem]], None]
self,
listener: Callable[[list[JsonValueType] | None], None],
) -> CALLBACK_TYPE:
"""Subscribe to To-do list item updates."""
"""Subscribe to To-do list item updates.
Called by websocket API.
"""
if self._update_listeners is None:
self._update_listeners = []
self._update_listeners.append(listener)
@@ -302,7 +306,9 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if not self._update_listeners:
return
todo_items = [copy.copy(item) for item in self.todo_items or []]
todo_items: list[JsonValueType] = [
dataclasses.asdict(item) for item in self.todo_items or ()
]
for listener in self._update_listeners:
listener(todo_items)
@@ -335,13 +341,13 @@ async def websocket_handle_subscribe_todo_items(
return
@callback
def todo_item_listener(todo_items: list[TodoItem]) -> None:
def todo_item_listener(todo_items: list[JsonValueType] | None) -> None:
"""Push updated To-do list items to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"],
{
"items": [dataclasses.asdict(item) for item in todo_items],
"items": todo_items,
},
)
)
@@ -351,7 +357,7 @@ async def websocket_handle_subscribe_todo_items(
)
connection.send_result(msg["id"])
# Push an initial list update
# Push an initial forecast update
entity.async_update_listeners()

View File

@@ -35,13 +35,7 @@ ALARM: dict[DeviceCategory, tuple[AlarmControlPanelEntityDescription, ...]] = {
key=DPCode.MASTER_MODE,
name="Alarm",
),
),
DeviceCategory.WG2: (
AlarmControlPanelEntityDescription(
key=DPCode.MASTER_MODE,
name="Alarm",
),
),
)
}
_TUYA_TO_HA_STATE_MAPPINGS = {

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