mirror of
https://github.com/home-assistant/core.git
synced 2026-03-18 17:02:25 +01:00
Compare commits
53 Commits
add_temper
...
remove-get
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695f3a3f99 | ||
|
|
a63516ff71 | ||
|
|
55b082edb6 | ||
|
|
b0c3ede4fd | ||
|
|
84bd1cd336 | ||
|
|
25bbfcc595 | ||
|
|
bf05925c8b | ||
|
|
488d9ad75c | ||
|
|
2dfad3d755 | ||
|
|
7e759bf730 | ||
|
|
9678049e72 | ||
|
|
8602ba2679 | ||
|
|
78c3503b7d | ||
|
|
fbb3b81991 | ||
|
|
26eaf510ee | ||
|
|
5c83d16995 | ||
|
|
388b258d6c | ||
|
|
2c9a5c10da | ||
|
|
5a68bafd69 | ||
|
|
33fce89a2b | ||
|
|
1932f61da3 | ||
|
|
5a231b27b9 | ||
|
|
5617e8c7bc | ||
|
|
2b5b0e9d0f | ||
|
|
732f553b48 | ||
|
|
0a53b227ed | ||
|
|
44b73ab7bd | ||
|
|
538061d512 | ||
|
|
e307ceccb5 | ||
|
|
ea7558c0ad | ||
|
|
c4399b5547 | ||
|
|
d989a83d7b | ||
|
|
d04f3530df | ||
|
|
647d957ffe | ||
|
|
a3f3c87b39 | ||
|
|
447b17a2a4 | ||
|
|
eb2b92687c | ||
|
|
6424e3658e | ||
|
|
d1d8754853 | ||
|
|
c4ff7fa676 | ||
|
|
f1fe1d3956 | ||
|
|
fd0d60b787 | ||
|
|
9ddefaaacd | ||
|
|
5c8df048b1 | ||
|
|
d86d85ec56 | ||
|
|
660f12b683 | ||
|
|
b8238c86e6 | ||
|
|
754828188e | ||
|
|
6992a3c72b | ||
|
|
738d4f662a | ||
|
|
7f33ac72ab | ||
|
|
0891d814fa | ||
|
|
ddab50edcc |
@@ -620,12 +620,14 @@ 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
1
.gitattributes
vendored
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
125
.github/workflows/builder.yml
vendored
125
.github/workflows/builder.yml
vendored
@@ -35,6 +35,7 @@ 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
|
||||
@@ -100,7 +101,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
include:
|
||||
- arch: amd64
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
@@ -195,77 +196,20 @@ jobs:
|
||||
run: |
|
||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
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
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ steps.vars.outputs.platform }}
|
||||
push: true
|
||||
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
|
||||
arch: ${{ matrix.arch }}
|
||||
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_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 }}
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
|
||||
build_machine:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
@@ -314,35 +258,38 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set build additional args
|
||||
- name: Compute extra tags
|
||||
id: tags
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
# Create general tags
|
||||
if [[ "${VERSION}" =~ d ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${VERSION}" =~ b ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
- name: Build machine image
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
with:
|
||||
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 }}"
|
||||
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 }}
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
|
||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -1616,8 +1616,6 @@ 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
|
||||
@@ -1699,8 +1697,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
/tests/components/teltonika/ @karlbeecken
|
||||
/homeassistant/components/temperature/ @home-assistant/core
|
||||
/tests/components/temperature/ @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
|
||||
1
Dockerfile
generated
1
Dockerfile
generated
@@ -10,7 +10,6 @@ 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/"
|
||||
|
||||
|
||||
@@ -247,7 +247,6 @@ DEFAULT_INTEGRATIONS = {
|
||||
"humidity",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
|
||||
@@ -123,16 +123,22 @@ _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 = {
|
||||
@@ -159,9 +165,9 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"remote",
|
||||
"scene",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==66"],
|
||||
"requirements": ["axis==67"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -246,6 +246,8 @@ 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
|
||||
@@ -332,8 +334,10 @@ 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 decrypting backup: %s", err)
|
||||
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||
error = err
|
||||
else:
|
||||
# Pad the output stream to the requested minimum size
|
||||
|
||||
@@ -66,6 +66,7 @@ class ClementineDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
)
|
||||
_attr_volume_step = 0.04
|
||||
|
||||
def __init__(self, client, name):
|
||||
"""Initialize the Clementine device."""
|
||||
@@ -124,16 +125,6 @@ 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)
|
||||
|
||||
@@ -32,6 +32,7 @@ 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,
|
||||
@@ -80,6 +81,8 @@ __all__ = [
|
||||
"CoverEntityFeature",
|
||||
"CoverState",
|
||||
"make_cover_closed_trigger",
|
||||
"make_cover_is_closed_condition",
|
||||
"make_cover_is_open_condition",
|
||||
"make_cover_opened_trigger",
|
||||
]
|
||||
|
||||
|
||||
29
homeassistant/components/door/condition.py
Normal file
29
homeassistant/components/door/condition.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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
|
||||
28
homeassistant/components/door/conditions.yaml
Normal file
28
homeassistant/components/door/conditions.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
.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
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:door-closed"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:door-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:door-closed"
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -12,7 +12,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -56,6 +61,14 @@ 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",
|
||||
|
||||
@@ -118,6 +118,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"battery_temperature": {
|
||||
"name": "Battery temperature"
|
||||
},
|
||||
"current_page": {
|
||||
"name": "Current page"
|
||||
},
|
||||
|
||||
31
homeassistant/components/garage_door/condition.py
Normal file
31
homeassistant/components/garage_door/condition.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""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
|
||||
28
homeassistant/components/garage_door/conditions.yaml
Normal file
28
homeassistant/components/garage_door/conditions.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
.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
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:garage"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:garage-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:garage"
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
24
homeassistant/components/gate/condition.py
Normal file
24
homeassistant/components/gate/condition.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""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
|
||||
24
homeassistant/components/gate/conditions.yaml
Normal file
24
homeassistant/components/gate/conditions.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
.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
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:gate"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:gate-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:gate"
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -251,7 +251,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
def get_data(
|
||||
self, entity_description: GrowattSensorEntityDescription
|
||||
) -> str | int | float | None:
|
||||
) -> str | int | float | datetime.datetime | datetime.date | None:
|
||||
"""Get the data."""
|
||||
variable = entity_description.api_key
|
||||
api_value = self.data.get(variable)
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
{
|
||||
"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"
|
||||
|
||||
@@ -17,7 +17,6 @@ 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,9 +26,10 @@ PARALLEL_UPDATES = (
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattNumberEntityDescription(NumberEntityDescription, GrowattRequiredKeysMixin):
|
||||
class GrowattNumberEntityDescription(NumberEntityDescription):
|
||||
"""Describes Growatt number entity."""
|
||||
|
||||
api_key: str
|
||||
write_key: str | None = None # Parameter ID for writing (if different from api_key)
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ class GrowattNumber(CoordinatorEntity[GrowattCoordinator], NumberEntity):
|
||||
identifiers={(DOMAIN, coordinator.device_id)},
|
||||
manufacturer="Growatt",
|
||||
name=coordinator.device_id,
|
||||
serial_number=coordinator.device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -32,9 +32,7 @@ rules:
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: todo
|
||||
comment: Add serial_number field to DeviceInfo in sensor, number, and switch platforms using device_id/serial_id.
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
@@ -46,16 +44,12 @@ rules:
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
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-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
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
|
||||
@@ -99,24 +101,18 @@ 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) -> str | int | float | None:
|
||||
def native_value(self) -> StateType | date | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
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
|
||||
return self.coordinator.get_data(self.entity_description)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -22,7 +23,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_energy_total",
|
||||
@@ -30,7 +31,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="powerTotal",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -40,7 +41,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_1",
|
||||
@@ -49,7 +50,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_1",
|
||||
@@ -58,7 +59,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_voltage_input_2",
|
||||
@@ -67,7 +68,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_2",
|
||||
@@ -76,7 +77,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_2",
|
||||
@@ -85,7 +86,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_voltage_input_3",
|
||||
@@ -94,7 +95,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_3",
|
||||
@@ -103,7 +104,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_3",
|
||||
@@ -112,7 +113,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_internal_wattage",
|
||||
@@ -121,7 +122,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_reactive_voltage",
|
||||
@@ -130,7 +131,9 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_inverter_reactive_amperage",
|
||||
@@ -139,7 +142,9 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_frequency",
|
||||
@@ -148,7 +153,9 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_current_wattage",
|
||||
@@ -157,7 +164,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_current_reactive_wattage",
|
||||
@@ -166,7 +173,9 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_ipm_temperature",
|
||||
@@ -175,7 +184,9 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_temperature",
|
||||
@@ -184,6 +195,8 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,18 +7,11 @@ from dataclasses import dataclass
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GrowattRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
api_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin):
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Growatt sensor entity."""
|
||||
|
||||
precision: int | None = None
|
||||
api_key: str
|
||||
currency: bool = False
|
||||
previous_value_drop_threshold: float | None = None
|
||||
never_resets: bool = False
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
@@ -90,6 +91,8 @@ 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",
|
||||
@@ -98,6 +101,8 @@ 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",
|
||||
@@ -106,6 +111,8 @@ 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",
|
||||
@@ -114,6 +121,8 @@ 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",
|
||||
@@ -122,6 +131,8 @@ 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",
|
||||
@@ -130,6 +141,8 @@ 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(
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -189,7 +190,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_pv_charging_voltage",
|
||||
@@ -198,7 +199,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_pv_charging_voltage_2",
|
||||
@@ -207,7 +208,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_ac_input_frequency_out",
|
||||
@@ -216,7 +217,9 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_output_voltage",
|
||||
@@ -225,7 +228,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_ac_output_frequency",
|
||||
@@ -234,7 +237,9 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_PV",
|
||||
@@ -243,7 +248,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_1",
|
||||
@@ -252,7 +257,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_2",
|
||||
@@ -261,7 +266,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_grid_amperage_input",
|
||||
@@ -270,7 +275,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_grid_out_current",
|
||||
@@ -279,7 +284,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_battery_voltage",
|
||||
@@ -288,7 +293,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_load_percentage",
|
||||
@@ -297,6 +302,6 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=2,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -26,7 +27,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total",
|
||||
@@ -35,7 +36,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -45,7 +46,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -55,7 +56,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_1",
|
||||
@@ -63,7 +64,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv1",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_1",
|
||||
@@ -71,7 +72,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv1",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_1",
|
||||
@@ -80,7 +81,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_2",
|
||||
@@ -89,7 +90,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -99,7 +100,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_2",
|
||||
@@ -107,7 +108,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv2",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_2",
|
||||
@@ -115,7 +116,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv2",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_2",
|
||||
@@ -124,7 +125,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_3",
|
||||
@@ -133,7 +134,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -143,7 +144,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_3",
|
||||
@@ -151,7 +152,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv3",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_3",
|
||||
@@ -159,7 +160,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv3",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_3",
|
||||
@@ -168,7 +169,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_4",
|
||||
@@ -177,7 +178,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -187,7 +188,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_4",
|
||||
@@ -195,7 +196,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv4",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_4",
|
||||
@@ -203,7 +204,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv4",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_4",
|
||||
@@ -212,7 +213,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_solar_generation_today",
|
||||
@@ -221,7 +222,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_solar_generation_total",
|
||||
@@ -239,7 +240,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_reactive_voltage",
|
||||
@@ -247,7 +248,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vacrs",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_frequency",
|
||||
@@ -255,7 +258,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="fac",
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_current_wattage",
|
||||
@@ -264,7 +269,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_1",
|
||||
@@ -272,7 +277,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp1",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_2",
|
||||
@@ -280,7 +287,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp2",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_3",
|
||||
@@ -288,7 +297,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp3",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_4",
|
||||
@@ -296,7 +307,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp4",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_5",
|
||||
@@ -304,7 +317,9 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp5",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_all_batteries_discharge_today",
|
||||
@@ -456,7 +471,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_pac_to_user_total",
|
||||
@@ -465,7 +480,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_pac_to_grid_total",
|
||||
@@ -474,7 +489,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_system_production_today",
|
||||
@@ -483,7 +498,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_system_production_total",
|
||||
@@ -493,7 +508,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_self_consumption_today",
|
||||
@@ -502,7 +517,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_self_consumption_total",
|
||||
@@ -512,7 +527,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_import_from_grid_today",
|
||||
@@ -521,7 +536,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_import_from_grid_total",
|
||||
@@ -531,7 +546,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_batteries_charged_from_grid_today",
|
||||
@@ -540,7 +555,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_batteries_charged_from_grid_total",
|
||||
@@ -550,7 +565,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_p_system",
|
||||
@@ -559,7 +574,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_p_self",
|
||||
@@ -568,6 +583,6 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
precision=1,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ 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__)
|
||||
|
||||
@@ -28,9 +27,10 @@ PARALLEL_UPDATES = (
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattSwitchEntityDescription(SwitchEntityDescription, GrowattRequiredKeysMixin):
|
||||
class GrowattSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes Growatt switch entity."""
|
||||
|
||||
api_key: str
|
||||
write_key: str | None = None # Parameter ID for writing (if different from api_key)
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class GrowattSwitch(CoordinatorEntity[GrowattCoordinator], SwitchEntity):
|
||||
identifiers={(DOMAIN, coordinator.device_id)},
|
||||
manufacturer="Growatt",
|
||||
name=coordinator.device_id,
|
||||
serial_number=coordinator.device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -119,7 +119,6 @@ from .coordinator import (
|
||||
get_core_stats,
|
||||
get_host_info,
|
||||
get_info,
|
||||
get_issues_info,
|
||||
get_network_info,
|
||||
get_os_info,
|
||||
get_store,
|
||||
@@ -158,7 +157,6 @@ __all__ = [
|
||||
"get_core_stats",
|
||||
"get_host_info",
|
||||
"get_info",
|
||||
"get_issues_info",
|
||||
"get_network_info",
|
||||
"get_os_info",
|
||||
"get_store",
|
||||
|
||||
@@ -132,6 +132,7 @@ 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"
|
||||
|
||||
@@ -172,6 +173,7 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ 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,
|
||||
@@ -90,6 +91,7 @@ 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__)
|
||||
@@ -253,9 +255,10 @@ 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 |= EXTRA_PLACEHOLDERS[issue.key]
|
||||
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[issue.key].copy()
|
||||
else:
|
||||
placeholders = {}
|
||||
|
||||
if issue.reference:
|
||||
placeholders[PLACEHOLDER_KEY_REFERENCE] = issue.reference
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.4.1"],
|
||||
"requirements": ["aiohasupervisor==0.4.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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,
|
||||
@@ -64,11 +65,16 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
placeholders = {}
|
||||
if self.issue:
|
||||
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
|
||||
if self.issue.reference:
|
||||
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
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}
|
||||
|
||||
return placeholders or None
|
||||
|
||||
@@ -232,6 +238,7 @@ 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)
|
||||
|
||||
|
||||
@@ -85,6 +85,19 @@
|
||||
},
|
||||
"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"
|
||||
|
||||
@@ -72,13 +72,6 @@ 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."""
|
||||
|
||||
@@ -45,8 +45,6 @@ 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")
|
||||
|
||||
@@ -54,7 +54,6 @@ 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
|
||||
|
||||
@@ -7,11 +7,7 @@ rules:
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: 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.
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
@@ -40,11 +36,7 @@ rules:
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: 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.
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage:
|
||||
@@ -74,11 +66,7 @@ rules:
|
||||
comment: All entities are core functionality.
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
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.
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
"description": "Authenticate against IntelliFire cloud"
|
||||
},
|
||||
"pick_cloud_device": {
|
||||
"description": "Select fireplace by serial number:",
|
||||
"data": {
|
||||
"serial": "Fireplace serial number"
|
||||
},
|
||||
"data_description": {
|
||||
"serial": "Serial number of the fireplace to configure"
|
||||
},
|
||||
"description": "Select fireplace by serial number.",
|
||||
"title": "Configure fireplace"
|
||||
}
|
||||
}
|
||||
@@ -159,6 +165,10 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -151,7 +151,9 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
return value
|
||||
|
||||
def get_options_map(self, command: str) -> dict[str, str]:
|
||||
def get_options_map(
|
||||
self, command: str, *, snake_case: bool = False
|
||||
) -> dict[str, str]:
|
||||
"""Get the available options for a command."""
|
||||
capabilities = self.capabilities.get(command, {})
|
||||
|
||||
@@ -162,7 +164,10 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
values = list(capabilities.get("parameter", {}).get("read", {}).values())
|
||||
|
||||
return {v: v.translate(TRANSLATIONS) for v in 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
|
||||
|
||||
def supports(self, command: type[Command]) -> bool:
|
||||
"""Check if the device supports a command."""
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"dynamic_control": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"hdr_processing": {
|
||||
"default": "mdi:image-filter-hdr-outline"
|
||||
},
|
||||
"input": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
@@ -26,6 +29,9 @@
|
||||
},
|
||||
"light_power": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"picture_mode": {
|
||||
"default": "mdi:movie-roll"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -20,6 +20,7 @@ class JvcProjectorSelectDescription(SelectEntityDescription):
|
||||
"""Describes JVC Projector select entities."""
|
||||
|
||||
command: type[Command]
|
||||
snake_case_states: bool = False
|
||||
|
||||
|
||||
SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
@@ -49,6 +50,18 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +97,8 @@ 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
|
||||
self.command.name,
|
||||
snake_case=description.snake_case_states,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,16 +7,19 @@ 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)
|
||||
@@ -84,12 +87,29 @@ 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)
|
||||
|
||||
async_add_entities(
|
||||
JvcProjectorSensorEntity(coordinator, description)
|
||||
for description in SENSORS
|
||||
if coordinator.supports(description.command)
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
class JvcProjectorSensorEntity(JvcProjectorEntity, SensorEntity):
|
||||
|
||||
@@ -71,6 +71,15 @@
|
||||
"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": {
|
||||
@@ -101,6 +110,23 @@
|
||||
"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": {
|
||||
@@ -156,7 +182,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",
|
||||
@@ -182,5 +208,15 @@
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
homeassistant/components/jvc_projector/util.py
Normal file
104
homeassistant/components/jvc_projector/util.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""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
|
||||
25
homeassistant/components/motion/condition.py
Normal file
25
homeassistant/components/motion/condition.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""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
|
||||
24
homeassistant/components/motion/conditions.yaml
Normal file
24
homeassistant/components/motion/conditions.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
.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
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"condition": "mdi:motion-sensor"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"condition": "mdi:motion-sensor-off"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"cleared": {
|
||||
"trigger": "mdi:motion-sensor-off"
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
25
homeassistant/components/occupancy/condition.py
Normal file
25
homeassistant/components/occupancy/condition.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""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
|
||||
24
homeassistant/components/occupancy/conditions.yaml
Normal file
24
homeassistant/components/occupancy/conditions.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
.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
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"condition": "mdi:home-account"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"condition": "mdi:home-outline"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"cleared": {
|
||||
"trigger": "mdi:home-outline"
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["opower==0.17.0"]
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ rules:
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
dynamic-devices: done
|
||||
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: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -15,7 +15,8 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -207,48 +208,102 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Opower sensor."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
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
|
||||
):
|
||||
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
|
||||
)
|
||||
created_sensors: set[tuple[str, str]] = set()
|
||||
|
||||
async_add_entities(entities)
|
||||
@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
|
||||
):
|
||||
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
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
@@ -272,6 +327,11 @@ 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."""
|
||||
|
||||
@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.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.restart.post()
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
|
||||
@@ -18,6 +18,7 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
144
homeassistant/components/renault/number.py
Normal file
144
homeassistant/components/renault/number.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""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",
|
||||
),
|
||||
)
|
||||
@@ -174,6 +174,13 @@ 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."""
|
||||
@@ -270,4 +277,10 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -94,6 +94,14 @@
|
||||
"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",
|
||||
@@ -199,6 +207,9 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -20,5 +20,10 @@
|
||||
"select_previous": {
|
||||
"service": "mdi:format-list-bulleted"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"selection_changed": {
|
||||
"trigger": "mdi:format-list-bulleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,11 @@
|
||||
"name": "Previous"
|
||||
}
|
||||
},
|
||||
"title": "Select"
|
||||
"title": "Select",
|
||||
"triggers": {
|
||||
"selection_changed": {
|
||||
"description": "Triggers after the selected option of one or more dropdowns changes.",
|
||||
"name": "Selection changed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
homeassistant/components/select/trigger.py
Normal file
40
homeassistant/components/select/trigger.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""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
|
||||
5
homeassistant/components/select/triggers.yaml
Normal file
5
homeassistant/components/select/triggers.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
selection_changed:
|
||||
target:
|
||||
entity:
|
||||
- domain: select
|
||||
- domain: input_select
|
||||
@@ -208,6 +208,15 @@ 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,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.7.0"]
|
||||
"requirements": ["pysmartthings==3.7.2"]
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
"keep_fresh_mode_active": {
|
||||
"name": "Keep fresh mode active"
|
||||
},
|
||||
"microfiber_filter_blockage": {
|
||||
"name": "Filter blockage"
|
||||
},
|
||||
"oven_cavity_status": {
|
||||
"name": "Second cavity status"
|
||||
},
|
||||
|
||||
@@ -288,6 +288,8 @@ 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()
|
||||
@@ -381,14 +383,6 @@ 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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "starlink",
|
||||
"name": "Starlink",
|
||||
"codeowners": ["@boswelja"],
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/starlink",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -8,6 +8,7 @@ from http import HTTPStatus
|
||||
import logging
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
from aiohttp import ClientSession
|
||||
import voluptuous as vol
|
||||
import xmltodict
|
||||
|
||||
@@ -150,7 +151,7 @@ async def async_setup_platform(
|
||||
apikey = config[CONF_API_KEY]
|
||||
bandwidthcap = config[CONF_TOTAL_BANDWIDTH]
|
||||
|
||||
ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap)
|
||||
ts_data = StartcaData(websession, apikey, bandwidthcap)
|
||||
ret = await ts_data.async_update()
|
||||
if ret is False:
|
||||
_LOGGER.error("Invalid Start.ca API key: %s", apikey)
|
||||
@@ -176,7 +177,9 @@ async def async_setup_platform(
|
||||
class StartcaSensor(SensorEntity):
|
||||
"""Representation of Start.ca Bandwidth sensor."""
|
||||
|
||||
def __init__(self, startcadata, name, description: SensorEntityDescription) -> None:
|
||||
def __init__(
|
||||
self, startcadata: StartcaData, name: str, description: SensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self.startcadata = startcadata
|
||||
@@ -194,9 +197,10 @@ class StartcaSensor(SensorEntity):
|
||||
class StartcaData:
|
||||
"""Get data from Start.ca API."""
|
||||
|
||||
def __init__(self, loop, websession, api_key, bandwidth_cap):
|
||||
def __init__(
|
||||
self, websession: ClientSession, api_key: str, bandwidth_cap: int
|
||||
) -> None:
|
||||
"""Initialize the data object."""
|
||||
self.loop = loop
|
||||
self.websession = websession
|
||||
self.api_key = api_key
|
||||
self.bandwidth_cap = bandwidth_cap
|
||||
@@ -215,7 +219,7 @@ class StartcaData:
|
||||
return float(value) * 10**-9
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> bool:
|
||||
"""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}"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Integration for temperature triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "temperature"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"trigger": "mdi:thermometer"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"trigger": "mdi:thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"domain": "temperature",
|
||||
"name": "Temperature",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/temperature",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"selector": {
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_temperature_unit": {
|
||||
"options": {
|
||||
"celsius": "Celsius (°C)",
|
||||
"fahrenheit": "Fahrenheit (°F)"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Temperature",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers when the temperature changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "The unit of temperature for the trigger values. Defaults to the system unit.",
|
||||
"name": "Unit"
|
||||
}
|
||||
},
|
||||
"name": "Temperature changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers when the temperature crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::temperature::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::temperature::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "The lower limit of the threshold.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "The type of threshold to use.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::temperature::triggers::changed::fields::unit::description%]",
|
||||
"name": "[%key:component::temperature::triggers::changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "The upper limit of the threshold.",
|
||||
"name": "Upper limit"
|
||||
}
|
||||
},
|
||||
"name": "Temperature crossed threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Provides triggers for temperature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE as CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.water_heater import (
|
||||
ATTR_CURRENT_TEMPERATURE as WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as WATER_HEATER_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_OPTIONS,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ATTR_BEHAVIOR,
|
||||
BEHAVIOR_ANY,
|
||||
BEHAVIOR_FIRST,
|
||||
BEHAVIOR_LAST,
|
||||
CONF_LOWER_LIMIT,
|
||||
CONF_THRESHOLD_TYPE,
|
||||
CONF_UPPER_LIMIT,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityNumericalStateChangedTriggerBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerBase,
|
||||
EntityNumericalStateTriggerBase,
|
||||
ThresholdType,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
_number_or_entity,
|
||||
_validate_limits_for_threshold_type,
|
||||
_validate_range,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
CONF_UNIT = "unit"
|
||||
|
||||
_UNIT_MAP = {
|
||||
"celsius": UnitOfTemperature.CELSIUS,
|
||||
"fahrenheit": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
def _validate_temperature_unit(value: str) -> str:
|
||||
"""Convert temperature unit option to UnitOfTemperature."""
|
||||
if value in _UNIT_MAP:
|
||||
return _UNIT_MAP[value]
|
||||
raise vol.Invalid(f"Unknown temperature unit: {value}")
|
||||
|
||||
|
||||
_UNIT_VALIDATOR = _validate_temperature_unit
|
||||
|
||||
TEMPERATURE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
{
|
||||
vol.Optional(CONF_ABOVE): _number_or_entity,
|
||||
vol.Optional(CONF_BELOW): _number_or_entity,
|
||||
vol.Optional(CONF_UNIT): _UNIT_VALIDATOR,
|
||||
},
|
||||
_validate_range(CONF_ABOVE, CONF_BELOW),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TEMPERATURE_CROSSED_THRESHOLD_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
{
|
||||
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
|
||||
),
|
||||
vol.Optional(CONF_LOWER_LIMIT): _number_or_entity,
|
||||
vol.Optional(CONF_UPPER_LIMIT): _number_or_entity,
|
||||
vol.Required(CONF_THRESHOLD_TYPE): vol.Coerce(ThresholdType),
|
||||
vol.Optional(CONF_UNIT): _UNIT_VALIDATOR,
|
||||
},
|
||||
_validate_range(CONF_LOWER_LIMIT, CONF_UPPER_LIMIT),
|
||||
_validate_limits_for_threshold_type,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
TEMPERATURE_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WATER_HEATER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class _TemperatureTriggerMixin(EntityNumericalStateTriggerBase):
|
||||
"""Mixin for temperature triggers providing entity filtering, value extraction, and unit conversion."""
|
||||
|
||||
_domain_specs = TEMPERATURE_DOMAIN_SPECS
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._trigger_unit: str = self._options.get(
|
||||
CONF_UNIT, hass.config.units.temperature_unit
|
||||
)
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
"""Get the temperature unit of an entity from its state."""
|
||||
domain = split_entity_id(state.entity_id)[0]
|
||||
if domain == SENSOR_DOMAIN:
|
||||
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if domain == WEATHER_DOMAIN:
|
||||
return state.attributes.get(
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
self._hass.config.units.temperature_unit,
|
||||
)
|
||||
# Climate and water_heater: show_temp converts to system unit
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
def _get_tracked_value(self, state: State) -> Any:
|
||||
"""Get the temperature value converted to the trigger's configured unit."""
|
||||
raw_value = super()._get_tracked_value(state)
|
||||
if raw_value is None:
|
||||
return None
|
||||
|
||||
entity_unit = self._get_entity_unit(state)
|
||||
if entity_unit is None or entity_unit == self._trigger_unit:
|
||||
return raw_value
|
||||
|
||||
try:
|
||||
return TemperatureConverter.convert(
|
||||
float(raw_value), entity_unit, self._trigger_unit
|
||||
)
|
||||
except TypeError, ValueError:
|
||||
return raw_value # Let the base class converter handle the error
|
||||
|
||||
|
||||
class TemperatureChangedTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateChangedTriggerBase
|
||||
):
|
||||
"""Trigger for temperature value changes across multiple domains."""
|
||||
|
||||
_schema = TEMPERATURE_CHANGED_TRIGGER_SCHEMA
|
||||
|
||||
|
||||
class TemperatureCrossedThresholdTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
|
||||
):
|
||||
"""Trigger for temperature value crossing a threshold across multiple domains."""
|
||||
|
||||
_schema = TEMPERATURE_CROSSED_THRESHOLD_TRIGGER_SCHEMA
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"changed": TemperatureChangedTrigger,
|
||||
"crossed_threshold": TemperatureCrossedThresholdTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for temperature."""
|
||||
return TRIGGERS
|
||||
@@ -1,75 +0,0 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
.trigger_unit: &trigger_unit
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- celsius
|
||||
- fahrenheit
|
||||
translation_key: trigger_temperature_unit
|
||||
|
||||
.trigger_target: &trigger_target
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: climate
|
||||
- domain: water_heater
|
||||
- domain: weather
|
||||
|
||||
changed:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
|
||||
crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
@@ -35,7 +35,13 @@ 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 = {
|
||||
|
||||
@@ -317,6 +317,11 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_value="alarm",
|
||||
),
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CHARGE_STATE,
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
),
|
||||
DeviceCategory.WK: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
|
||||
@@ -1233,6 +1233,7 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
),
|
||||
*BATTERY_SENSORS,
|
||||
),
|
||||
DeviceCategory.WG2: (*BATTERY_SENSORS,),
|
||||
DeviceCategory.WK: (*BATTERY_SENSORS,),
|
||||
DeviceCategory.WKCZ: (
|
||||
TuyaSensorEntityDescription(
|
||||
|
||||
@@ -6,7 +6,6 @@ from contextlib import suppress
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyViCare.PyViCare import PyViCare
|
||||
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
|
||||
from PyViCare.PyViCareUtils import (
|
||||
PyViCareInvalidConfigurationError,
|
||||
@@ -68,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ViCareConfigEntry) -> bo
|
||||
return True
|
||||
|
||||
|
||||
def setup_vicare_api(hass: HomeAssistant, entry: ViCareConfigEntry) -> PyViCare:
|
||||
def setup_vicare_api(hass: HomeAssistant, entry: ViCareConfigEntry) -> ViCareData:
|
||||
"""Set up PyVicare API."""
|
||||
client = login(hass, entry.data)
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
discovery_info = self._discovered_devices_info[self._discovered_device]
|
||||
title = discovery_info.name
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
# see if we can create a device with the access token
|
||||
device = VictronBluetoothDeviceData(user_input[CONF_ACCESS_TOKEN])
|
||||
@@ -76,12 +77,13 @@ class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=title,
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_abort(reason="invalid_access_token")
|
||||
errors["base"] = "invalid_access_token"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="access_token",
|
||||
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
|
||||
description_placeholders={"title": title},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["victron-ble-ha-parser==0.6.1"]
|
||||
"requirements": ["victron-ble-ha-parser==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -148,7 +148,10 @@ def error_to_state(value: float | str | None) -> str | None:
|
||||
"network_c": "network",
|
||||
"network_d": "network",
|
||||
}
|
||||
return value_map.get(value)
|
||||
mapped = value_map.get(value)
|
||||
if mapped is not None:
|
||||
return mapped
|
||||
return value if isinstance(value, str) and value in CHARGER_ERROR_OPTIONS else None
|
||||
|
||||
|
||||
DEVICE_STATE_OPTIONS = [
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_access_token": "Invalid encryption key for instant readout",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_access_token": "Invalid encryption key for instant readout"
|
||||
},
|
||||
"flow_title": "{title}",
|
||||
"step": {
|
||||
"access_token": {
|
||||
|
||||
@@ -2,20 +2,34 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from pyvizio import VizioAsync
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, Platform
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import CONF_APPS, DOMAIN
|
||||
from .coordinator import VizioAppsDataUpdateCoordinator
|
||||
from .const import DEFAULT_TIMEOUT, DEVICE_ID, DOMAIN, VIZIO_DEVICE_CLASSES
|
||||
from .coordinator import (
|
||||
VizioAppsDataUpdateCoordinator,
|
||||
VizioConfigEntry,
|
||||
VizioDeviceCoordinator,
|
||||
VizioRuntimeData,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
DATA_APPS: HassKey[VizioAppsDataUpdateCoordinator] = HassKey(f"{DOMAIN}_apps")
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
@@ -26,38 +40,54 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: VizioConfigEntry) -> bool:
|
||||
"""Load the saved entities."""
|
||||
host = entry.data[CONF_HOST]
|
||||
token = entry.data.get(CONF_ACCESS_TOKEN)
|
||||
device_class = entry.data[CONF_DEVICE_CLASS]
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if (
|
||||
CONF_APPS not in hass.data[DOMAIN]
|
||||
and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
):
|
||||
store: Store[list[dict[str, Any]]] = Store(hass, 1, DOMAIN)
|
||||
coordinator = VizioAppsDataUpdateCoordinator(hass, store)
|
||||
await coordinator.async_setup()
|
||||
hass.data[DOMAIN][CONF_APPS] = coordinator
|
||||
await coordinator.async_refresh()
|
||||
# Create device
|
||||
device = VizioAsync(
|
||||
DEVICE_ID,
|
||||
host,
|
||||
entry.data[CONF_NAME],
|
||||
auth_token=token,
|
||||
device_type=VIZIO_DEVICE_CLASSES[device_class],
|
||||
session=async_get_clientsession(hass, False),
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
# Create device coordinator
|
||||
device_coordinator = VizioDeviceCoordinator(hass, entry, device)
|
||||
await device_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Create apps coordinator for TVs (shared across entries)
|
||||
if device_class == MediaPlayerDeviceClass.TV and DATA_APPS not in hass.data:
|
||||
apps_coordinator = VizioAppsDataUpdateCoordinator(hass, Store(hass, 1, DOMAIN))
|
||||
await apps_coordinator.async_setup()
|
||||
hass.data[DATA_APPS] = apps_coordinator
|
||||
await apps_coordinator.async_refresh()
|
||||
|
||||
entry.runtime_data = VizioRuntimeData(
|
||||
device_coordinator=device_coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: VizioConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
if not any(
|
||||
entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
):
|
||||
if coordinator := hass.data[DOMAIN].pop(CONF_APPS, None):
|
||||
await coordinator.async_shutdown()
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
# Clean up apps coordinator if no TV entries remain
|
||||
if unload_ok and not any(
|
||||
e.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
for e in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if e.entry_id != entry.entry_id
|
||||
):
|
||||
if apps_coordinator := hass.data.pop(DATA_APPS, None):
|
||||
await apps_coordinator.async_shutdown()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -8,13 +8,12 @@ import socket
|
||||
from typing import Any
|
||||
|
||||
from pyvizio import VizioAsync, async_guess_device_type
|
||||
from pyvizio.const import APP_HOME
|
||||
from pyvizio.const import APP_HOME, APPS
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
@@ -34,6 +33,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from . import DATA_APPS
|
||||
from .const import (
|
||||
CONF_APPS,
|
||||
CONF_APPS_TO_INCLUDE_OR_EXCLUDE,
|
||||
@@ -45,6 +45,7 @@ from .const import (
|
||||
DEVICE_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import VizioConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -106,6 +107,14 @@ def _host_is_same(host1: str, host2: str) -> bool:
|
||||
class VizioOptionsConfigFlow(OptionsFlow):
|
||||
"""Handle Vizio options."""
|
||||
|
||||
def _get_app_list(self) -> list[dict[str, Any]]:
|
||||
"""Return the current apps list, falling back to defaults."""
|
||||
if (
|
||||
apps_coordinator := self.hass.data.get(DATA_APPS)
|
||||
) and apps_coordinator.data:
|
||||
return apps_coordinator.data
|
||||
return APPS
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -157,10 +166,7 @@ class VizioOptionsConfigFlow(OptionsFlow):
|
||||
): cv.multi_select(
|
||||
[
|
||||
APP_HOME["name"],
|
||||
*(
|
||||
app["name"]
|
||||
for app in self.hass.data[DOMAIN][CONF_APPS].data
|
||||
),
|
||||
*(app["name"] for app in self._get_app_list()),
|
||||
]
|
||||
),
|
||||
}
|
||||
@@ -176,7 +182,9 @@ class VizioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> VizioOptionsConfigFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: VizioConfigEntry,
|
||||
) -> VizioOptionsConfigFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return VizioOptionsConfigFlow()
|
||||
|
||||
|
||||
@@ -2,22 +2,150 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyvizio.const import APPS
|
||||
from pyvizio import VizioAsync
|
||||
from pyvizio.api.apps import AppConfig
|
||||
from pyvizio.api.input import InputItem
|
||||
from pyvizio.const import APPS, INPUT_APPS
|
||||
from pyvizio.util import gen_apps_list_from_url
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE
|
||||
|
||||
type VizioConfigEntry = ConfigEntry[VizioRuntimeData]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VizioRuntimeData:
|
||||
"""Runtime data for Vizio integration."""
|
||||
|
||||
device_coordinator: VizioDeviceCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VizioDeviceData:
|
||||
"""Raw data fetched from Vizio device."""
|
||||
|
||||
# Power state
|
||||
is_on: bool
|
||||
|
||||
# Audio settings from get_all_settings("audio")
|
||||
audio_settings: dict[str, Any] | None = None
|
||||
|
||||
# Sound mode options from get_setting_options("audio", "eq")
|
||||
sound_mode_list: list[str] | None = None
|
||||
|
||||
# Current input from get_current_input()
|
||||
current_input: str | None = None
|
||||
|
||||
# Available inputs from get_inputs_list()
|
||||
input_list: list[InputItem] | None = None
|
||||
|
||||
# Current app config from get_current_app_config() (TVs only)
|
||||
current_app_config: AppConfig | None = None
|
||||
|
||||
|
||||
class VizioDeviceCoordinator(DataUpdateCoordinator[VizioDeviceData]):
|
||||
"""Coordinator for Vizio device data."""
|
||||
|
||||
config_entry: VizioConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: VizioConfigEntry,
|
||||
device: VizioAsync,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.device = device
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Fetch device info and update device registry."""
|
||||
model = await self.device.get_model_name(log_api_exception=False)
|
||||
version = await self.device.get_version(log_api_exception=False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.config_entry.unique_id
|
||||
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={(DOMAIN, self.config_entry.unique_id)},
|
||||
manufacturer="VIZIO",
|
||||
name=self.config_entry.data[CONF_NAME],
|
||||
model=model,
|
||||
sw_version=version,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> VizioDeviceData:
|
||||
"""Fetch all device data."""
|
||||
is_on = await self.device.get_power_state(log_api_exception=False)
|
||||
|
||||
if is_on is None:
|
||||
raise UpdateFailed(
|
||||
f"Unable to connect to {self.config_entry.data[CONF_HOST]}"
|
||||
)
|
||||
|
||||
if not is_on:
|
||||
return VizioDeviceData(is_on=False)
|
||||
|
||||
# Device is on - fetch all data
|
||||
audio_settings = await self.device.get_all_settings(
|
||||
VIZIO_AUDIO_SETTINGS, log_api_exception=False
|
||||
)
|
||||
|
||||
sound_mode_list = None
|
||||
if audio_settings and VIZIO_SOUND_MODE in audio_settings:
|
||||
sound_mode_list = await self.device.get_setting_options(
|
||||
VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, log_api_exception=False
|
||||
)
|
||||
|
||||
current_input = await self.device.get_current_input(log_api_exception=False)
|
||||
input_list = await self.device.get_inputs_list(log_api_exception=False)
|
||||
|
||||
current_app_config = None
|
||||
# Only attempt to fetch app config if the device is a TV and supports apps
|
||||
if (
|
||||
self.config_entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
and input_list
|
||||
and any(input_item.name in INPUT_APPS for input_item in input_list)
|
||||
):
|
||||
current_app_config = await self.device.get_current_app_config(
|
||||
log_api_exception=False
|
||||
)
|
||||
|
||||
return VizioDeviceData(
|
||||
is_on=True,
|
||||
audio_settings=audio_settings,
|
||||
sound_mode_list=sound_mode_list,
|
||||
current_input=current_input,
|
||||
input_list=input_list,
|
||||
current_app_config=current_app_config,
|
||||
)
|
||||
|
||||
|
||||
class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
"""Define an object to hold Vizio app config data."""
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyvizio import AppConfig, VizioAsync
|
||||
from pyvizio.api.apps import find_app_name
|
||||
from pyvizio.api.apps import AppConfig, find_app_name
|
||||
from pyvizio.const import APP_HOME, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -15,58 +11,45 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_EXCLUDE,
|
||||
CONF_HOST,
|
||||
CONF_INCLUDE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_EXCLUDE, CONF_INCLUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import DATA_APPS
|
||||
from .const import (
|
||||
CONF_ADDITIONAL_CONFIGS,
|
||||
CONF_APPS,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DEVICE_ID,
|
||||
DOMAIN,
|
||||
SUPPORTED_COMMANDS,
|
||||
VIZIO_AUDIO_SETTINGS,
|
||||
VIZIO_DEVICE_CLASSES,
|
||||
VIZIO_MUTE,
|
||||
VIZIO_MUTE_ON,
|
||||
VIZIO_SOUND_MODE,
|
||||
VIZIO_VOLUME,
|
||||
)
|
||||
from .coordinator import VizioAppsDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
VizioAppsDataUpdateCoordinator,
|
||||
VizioConfigEntry,
|
||||
VizioDeviceCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: VizioConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a Vizio media player entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
token = config_entry.data.get(CONF_ACCESS_TOKEN)
|
||||
name = config_entry.data[CONF_NAME]
|
||||
device_class = config_entry.data[CONF_DEVICE_CLASS]
|
||||
|
||||
# If config entry options not set up, set them up,
|
||||
@@ -105,59 +88,51 @@ async def async_setup_entry(
|
||||
**params, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
device = VizioAsync(
|
||||
DEVICE_ID,
|
||||
host,
|
||||
name,
|
||||
auth_token=token,
|
||||
device_type=VIZIO_DEVICE_CLASSES[device_class],
|
||||
session=async_get_clientsession(hass, False),
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
entity = VizioDevice(
|
||||
config_entry,
|
||||
device_class,
|
||||
config_entry.runtime_data.device_coordinator,
|
||||
hass.data.get(DATA_APPS) if device_class == MediaPlayerDeviceClass.TV else None,
|
||||
)
|
||||
|
||||
apps_coordinator = hass.data[DOMAIN].get(CONF_APPS)
|
||||
|
||||
entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator)
|
||||
|
||||
async_add_entities([entity], update_before_add=True)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class VizioDevice(MediaPlayerEntity):
|
||||
class VizioDevice(CoordinatorEntity[VizioDeviceCoordinator], MediaPlayerEntity):
|
||||
"""Media Player implementation which performs REST requests to device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_received_device_info = False
|
||||
_current_input: str | None = None
|
||||
_current_app_config: AppConfig | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
device: VizioAsync,
|
||||
name: str,
|
||||
config_entry: VizioConfigEntry,
|
||||
device_class: MediaPlayerDeviceClass,
|
||||
coordinator: VizioDeviceCoordinator,
|
||||
apps_coordinator: VizioAppsDataUpdateCoordinator | None,
|
||||
) -> None:
|
||||
"""Initialize Vizio device."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._config_entry = config_entry
|
||||
self._apps_coordinator = apps_coordinator
|
||||
|
||||
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||
self._current_input: str | None = None
|
||||
self._current_app_config: AppConfig | None = None
|
||||
self._attr_sound_mode_list = []
|
||||
self._available_inputs: list[str] = []
|
||||
self._available_apps: list[str] = []
|
||||
|
||||
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||
self._all_apps = apps_coordinator.data if apps_coordinator else None
|
||||
self._conf_apps = config_entry.options.get(CONF_APPS, {})
|
||||
self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get(
|
||||
CONF_ADDITIONAL_CONFIGS, []
|
||||
)
|
||||
self._device = device
|
||||
self._max_volume = float(device.get_max_volume())
|
||||
self._attr_assumed_state = True
|
||||
self._device = coordinator.device
|
||||
self._max_volume = float(coordinator.device.get_max_volume())
|
||||
|
||||
# Entity class attributes that will change with each update (we only include
|
||||
# the ones that are initialized differently from the defaults)
|
||||
self._attr_sound_mode_list = []
|
||||
self._attr_supported_features = SUPPORTED_COMMANDS[device_class]
|
||||
|
||||
# Entity class attributes that will not change
|
||||
@@ -165,11 +140,7 @@ class VizioDevice(MediaPlayerEntity):
|
||||
assert unique_id
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_class = device_class
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer="VIZIO",
|
||||
name=name,
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, unique_id)})
|
||||
|
||||
def _apps_list(self, apps: list[str]) -> list[str]:
|
||||
"""Return process apps list based on configured filters."""
|
||||
@@ -181,112 +152,72 @@ class VizioDevice(MediaPlayerEntity):
|
||||
|
||||
return apps
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Retrieve latest state of the device."""
|
||||
if (
|
||||
is_on := await self._device.get_power_state(log_api_exception=False)
|
||||
) is None:
|
||||
if self._attr_available:
|
||||
_LOGGER.warning(
|
||||
"Lost connection to %s", self._config_entry.data[CONF_HOST]
|
||||
)
|
||||
self._attr_available = False
|
||||
return
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
data = self.coordinator.data
|
||||
|
||||
if not self._attr_available:
|
||||
_LOGGER.warning(
|
||||
"Restored connection to %s", self._config_entry.data[CONF_HOST]
|
||||
)
|
||||
self._attr_available = True
|
||||
|
||||
if not self._received_device_info:
|
||||
device_reg = dr.async_get(self.hass)
|
||||
assert self._config_entry.unique_id
|
||||
device = device_reg.async_get_device(
|
||||
identifiers={(DOMAIN, self._config_entry.unique_id)}
|
||||
)
|
||||
if device:
|
||||
device_reg.async_update_device(
|
||||
device.id,
|
||||
model=await self._device.get_model_name(log_api_exception=False),
|
||||
sw_version=await self._device.get_version(log_api_exception=False),
|
||||
)
|
||||
self._received_device_info = True
|
||||
|
||||
if not is_on:
|
||||
# Handle device off
|
||||
if not data.is_on:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self._attr_volume_level = None
|
||||
self._attr_is_volume_muted = None
|
||||
self._current_input = None
|
||||
self._attr_app_name = None
|
||||
self._current_app_config = None
|
||||
self._attr_sound_mode = None
|
||||
self._attr_app_name = None
|
||||
self._current_input = None
|
||||
self._current_app_config = None
|
||||
super()._handle_coordinator_update()
|
||||
return
|
||||
|
||||
# Device is on - apply coordinator data
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
|
||||
if audio_settings := await self._device.get_all_settings(
|
||||
VIZIO_AUDIO_SETTINGS, log_api_exception=False
|
||||
):
|
||||
# Audio settings
|
||||
if data.audio_settings:
|
||||
self._attr_volume_level = (
|
||||
float(audio_settings[VIZIO_VOLUME]) / self._max_volume
|
||||
float(data.audio_settings[VIZIO_VOLUME]) / self._max_volume
|
||||
)
|
||||
if VIZIO_MUTE in audio_settings:
|
||||
if VIZIO_MUTE in data.audio_settings:
|
||||
self._attr_is_volume_muted = (
|
||||
audio_settings[VIZIO_MUTE].lower() == VIZIO_MUTE_ON
|
||||
data.audio_settings[VIZIO_MUTE].lower() == VIZIO_MUTE_ON
|
||||
)
|
||||
else:
|
||||
self._attr_is_volume_muted = None
|
||||
|
||||
if VIZIO_SOUND_MODE in audio_settings:
|
||||
if VIZIO_SOUND_MODE in data.audio_settings:
|
||||
self._attr_supported_features |= (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
)
|
||||
self._attr_sound_mode = audio_settings[VIZIO_SOUND_MODE]
|
||||
self._attr_sound_mode = data.audio_settings[VIZIO_SOUND_MODE]
|
||||
if not self._attr_sound_mode_list:
|
||||
self._attr_sound_mode_list = await self._device.get_setting_options(
|
||||
VIZIO_AUDIO_SETTINGS,
|
||||
VIZIO_SOUND_MODE,
|
||||
log_api_exception=False,
|
||||
)
|
||||
self._attr_sound_mode_list = data.sound_mode_list or []
|
||||
else:
|
||||
# Explicitly remove MediaPlayerEntityFeature.SELECT_SOUND_MODE from supported features
|
||||
self._attr_supported_features &= (
|
||||
~MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
)
|
||||
|
||||
if input_ := await self._device.get_current_input(log_api_exception=False):
|
||||
self._current_input = input_
|
||||
# Input state
|
||||
if data.current_input:
|
||||
self._current_input = data.current_input
|
||||
if data.input_list:
|
||||
self._available_inputs = [i.name for i in data.input_list]
|
||||
|
||||
# If no inputs returned, end update
|
||||
if not (inputs := await self._device.get_inputs_list(log_api_exception=False)):
|
||||
return
|
||||
|
||||
self._available_inputs = [input_.name for input_ in inputs]
|
||||
|
||||
# Return before setting app variables if INPUT_APPS isn't in available inputs
|
||||
if self._attr_device_class == MediaPlayerDeviceClass.SPEAKER or not any(
|
||||
app for app in INPUT_APPS if app in self._available_inputs
|
||||
# App state (TV only) - check if device supports apps
|
||||
if (
|
||||
self._attr_device_class == MediaPlayerDeviceClass.TV
|
||||
and self._available_inputs
|
||||
and any(app in self._available_inputs for app in INPUT_APPS)
|
||||
):
|
||||
return
|
||||
all_apps = self._all_apps or ()
|
||||
self._available_apps = self._apps_list([app["name"] for app in all_apps])
|
||||
self._current_app_config = data.current_app_config
|
||||
self._attr_app_name = find_app_name(
|
||||
self._current_app_config,
|
||||
[APP_HOME, *all_apps, *self._additional_app_configs],
|
||||
)
|
||||
if self._attr_app_name == NO_APP_RUNNING:
|
||||
self._attr_app_name = None
|
||||
|
||||
# Create list of available known apps from known app list after
|
||||
# filtering by CONF_INCLUDE/CONF_EXCLUDE
|
||||
self._available_apps = self._apps_list(
|
||||
[app["name"] for app in self._all_apps or ()]
|
||||
)
|
||||
|
||||
self._current_app_config = await self._device.get_current_app_config(
|
||||
log_api_exception=False
|
||||
)
|
||||
|
||||
self._attr_app_name = find_app_name(
|
||||
self._current_app_config,
|
||||
[APP_HOME, *(self._all_apps or ()), *self._additional_app_configs],
|
||||
)
|
||||
|
||||
if self._attr_app_name == NO_APP_RUNNING:
|
||||
self._attr_app_name = None
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _get_additional_app_names(self) -> list[str]:
|
||||
"""Return list of additional apps that were included in configuration.yaml."""
|
||||
@@ -296,7 +227,7 @@ class VizioDevice(MediaPlayerEntity):
|
||||
|
||||
@staticmethod
|
||||
async def _async_send_update_options_signal(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: VizioConfigEntry
|
||||
) -> None:
|
||||
"""Send update event when Vizio config entry is updated."""
|
||||
# Move this method to component level if another entity ever gets added for a
|
||||
@@ -304,7 +235,7 @@ class VizioDevice(MediaPlayerEntity):
|
||||
# See here: https://github.com/home-assistant/core/pull/30653#discussion_r366426121
|
||||
async_dispatcher_send(hass, config_entry.entry_id, config_entry)
|
||||
|
||||
async def _async_update_options(self, config_entry: ConfigEntry) -> None:
|
||||
async def _async_update_options(self, config_entry: VizioConfigEntry) -> None:
|
||||
"""Update options if the update signal comes from this entity."""
|
||||
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
|
||||
# Update so that CONF_ADDITIONAL_CONFIGS gets retained for imports
|
||||
@@ -323,6 +254,11 @@ class VizioDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Process initial coordinator data
|
||||
self._handle_coordinator_update()
|
||||
|
||||
# Register callback for when config entry is updated.
|
||||
self.async_on_remove(
|
||||
self._config_entry.add_update_listener(
|
||||
@@ -337,21 +273,17 @@ class VizioDevice(MediaPlayerEntity):
|
||||
)
|
||||
)
|
||||
|
||||
if not self._apps_coordinator:
|
||||
if not (apps_coordinator := self._apps_coordinator):
|
||||
return
|
||||
|
||||
# Register callback for app list updates if device is a TV
|
||||
@callback
|
||||
def apps_list_update() -> None:
|
||||
"""Update list of all apps."""
|
||||
if not self._apps_coordinator:
|
||||
return
|
||||
self._all_apps = self._apps_coordinator.data
|
||||
self._all_apps = apps_coordinator.data
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
self._apps_coordinator.async_add_listener(apps_list_update)
|
||||
)
|
||||
self.async_on_remove(apps_coordinator.async_add_listener(apps_list_update))
|
||||
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
|
||||
@@ -186,7 +186,7 @@ class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
||||
async def _async_determine_api_calls(
|
||||
self,
|
||||
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
|
||||
raise NotImplementedError
|
||||
"""Determine which API calls to make for this coordinator."""
|
||||
|
||||
|
||||
class VolvoVerySlowIntervalCoordinator(VolvoBaseCoordinator):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyweatherflowudp"],
|
||||
"requirements": ["pyweatherflowudp==1.5.0"]
|
||||
"requirements": ["pyweatherflowudp==1.5.2"]
|
||||
}
|
||||
|
||||
29
homeassistant/components/window/condition.py
Normal file
29
homeassistant/components/window/condition.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Provides conditions for windows."""
|
||||
|
||||
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_WINDOW: dict[str, str] = {
|
||||
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.WINDOW,
|
||||
COVER_DOMAIN: CoverDeviceClass.WINDOW,
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_WINDOW),
|
||||
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_WINDOW),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for windows."""
|
||||
return CONDITIONS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user