Compare commits

..

3 Commits

Author SHA1 Message Date
Franck Nijhof b7a7f267f1 Merge remote-tracking branch 'origin/dev' into pr/litterrobot-lr5-sleep-schedule
# Conflicts:
#	homeassistant/components/litterrobot/entity.py
#	homeassistant/components/litterrobot/switch.py
#	homeassistant/components/litterrobot/time.py
2026-06-22 18:31:47 +00:00
Legendberg 0b4821db01 Rename update failure placeholder to entity_id 2026-06-12 09:18:14 -07:00
Legendberg 4de6b4498c Add per-day sleep schedule entities for Litter-Robot 5
Time entities for each day's sleep start and end, plus switches to enable
or disable each day's schedule. Rejected updates (the library returns
False without raising) now surface as HomeAssistantError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:48:54 -07:00
4375 changed files with 73231 additions and 97275 deletions
-1
View File
@@ -53,4 +53,3 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
+6 -6
View File
@@ -38,7 +38,7 @@ jobs:
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -102,7 +102,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -245,7 +245,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -292,7 +292,7 @@ jobs:
contents: read
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -469,7 +469,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -516,7 +516,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -8,11 +8,15 @@ name: Check requirements (deterministic)
# yamllint disable-line rule:truthy
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "requirements*.txt"
- "homeassistant/package_constraints.txt"
# Auto-trigger on PRs that touch tracked requirement files is disabled
# for now while we iterate — testing the workflow_run handoff to the
# agentic stage is hard with an auto-trigger. Re-enable once the chain
# has been validated end-to-end.
# pull_request:
# types: [opened, synchronize, reopened]
# paths:
# - "**/requirements*.txt"
# - "homeassistant/package_constraints.txt"
workflow_dispatch:
inputs:
pull_request_number:
@@ -36,7 +40,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
+4 -4
View File
@@ -31,7 +31,7 @@
# - GITHUB_TOKEN
#
# Custom actions used:
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -155,7 +155,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- name: Checkout .github and .agents folders
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
sparse-checkout: |
@@ -404,7 +404,7 @@ jobs:
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
} >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Create gh-aw temp directory
@@ -1236,7 +1236,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
- name: Checkout repository for patch context
if: needs.agent.outputs.has_patch == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# --- Threat Detection ---
+21 -21
View File
@@ -39,7 +39,7 @@ on:
env:
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.8"
HA_SHORT_VERSION: "2026.7"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
@@ -98,7 +98,7 @@ jobs:
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Generate partial Python venv restore key
@@ -264,7 +264,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Register problem matchers
@@ -291,7 +291,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run zizmor
@@ -318,7 +318,7 @@ jobs:
- script/hassfest/docker/Dockerfile
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Register hadolint problem matcher
@@ -341,7 +341,7 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
@@ -469,7 +469,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -512,7 +512,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -548,7 +548,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -576,7 +576,7 @@ jobs:
&& github.event_name == 'pull_request'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Dependency review
@@ -603,7 +603,7 @@ jobs:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
@@ -654,7 +654,7 @@ jobs:
|| github.event.inputs.pylint-only == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -707,7 +707,7 @@ jobs:
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -758,7 +758,7 @@ jobs:
|| github.event.inputs.mypy-only == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Python
@@ -825,7 +825,7 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -889,7 +889,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1030,7 +1030,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1179,7 +1179,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1317,7 +1317,7 @@ jobs:
if: needs.info.outputs.skip_coverage != 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Download all coverage artifacts
@@ -1355,7 +1355,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install additional OS dependencies
@@ -1476,7 +1476,7 @@ jobs:
- pytest-partial
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Download all coverage artifacts
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+3 -3
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -116,7 +116,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
@@ -167,7 +167,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
+2
View File
@@ -43,6 +43,7 @@ homeassistant.components
homeassistant.components.abode.*
homeassistant.components.acaia.*
homeassistant.components.accuweather.*
homeassistant.components.acer_projector.*
homeassistant.components.acmeda.*
homeassistant.components.actiontec.*
homeassistant.components.actron_air.*
@@ -76,6 +77,7 @@ homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.*
homeassistant.components.ambient_station.*
homeassistant.components.amcrest.*
homeassistant.components.ampio.*
homeassistant.components.analytics.*
homeassistant.components.analytics_insights.*
homeassistant.components.android_ip_webcam.*
-1
View File
@@ -42,4 +42,3 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
- Do not add section or divider comments (e.g. `# --- XYZ Triggers ---`) inside or outside of functions, since those can easily become stale and be misleading.
Generated
+10 -8
View File
@@ -230,6 +230,7 @@ CLAUDE.md @home-assistant/core
/tests/components/battery/ @home-assistant/core
/homeassistant/components/bayesian/ @HarvsG
/tests/components/bayesian/ @HarvsG
/homeassistant/components/beewi_smartclim/ @alemuro
/homeassistant/components/binary_sensor/ @home-assistant/core
/tests/components/binary_sensor/ @home-assistant/core
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
@@ -253,8 +254,8 @@ CLAUDE.md @home-assistant/core
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm @mosandlt
/tests/components/bosch_shc/ @tschamm @mosandlt
/homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm
/homeassistant/components/brands/ @home-assistant/core
/tests/components/brands/ @home-assistant/core
/homeassistant/components/braviatv/ @bieniu @Drafteed
@@ -790,8 +791,8 @@ CLAUDE.md @home-assistant/core
/tests/components/html5/ @alexyao2015 @tr4nt0r
/homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @fphammerle
/tests/components/huawei_lte/ @fphammerle
/homeassistant/components/huawei_lte/ @scop @fphammerle
/tests/components/huawei_lte/ @scop @fphammerle
/homeassistant/components/hue/ @marcelveldt
/tests/components/hue/ @marcelveldt
/homeassistant/components/hue_ble/ @flip-dots
@@ -1602,8 +1603,8 @@ CLAUDE.md @home-assistant/core
/tests/components/sensorpush/ @bdraco
/homeassistant/components/sensorpush_cloud/ @sstallion
/tests/components/sensorpush_cloud/ @sstallion
/homeassistant/components/sensoterra/ @SanderBakkumCuriousInc @curious-florian @markruys
/tests/components/sensoterra/ @SanderBakkumCuriousInc @curious-florian @markruys
/homeassistant/components/sensoterra/ @markruys
/tests/components/sensoterra/ @markruys
/homeassistant/components/sentry/ @dcramer @frenck
/tests/components/sentry/ @dcramer @frenck
/homeassistant/components/senz/ @milanmeu
@@ -1711,8 +1712,8 @@ CLAUDE.md @home-assistant/core
/tests/components/sql/ @gjohansson-ST @dougiteixeira
/homeassistant/components/squeezebox/ @rajlaud @pssc @peteS-UK
/tests/components/squeezebox/ @rajlaud @pssc @peteS-UK
/homeassistant/components/srp_energy/ @briglx @ammmze
/tests/components/srp_energy/ @briglx @ammmze
/homeassistant/components/srp_energy/ @briglx
/tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
@@ -1899,6 +1900,7 @@ CLAUDE.md @home-assistant/core
/tests/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifi_discovery/ @RaHehl
/tests/components/unifi_discovery/ @RaHehl
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl
/tests/components/unifiprotect/ @RaHehl
/homeassistant/components/upb/ @gwww
+1 -3
View File
@@ -6,7 +6,7 @@ from collections.abc import Mapping
from datetime import datetime, timedelta
from functools import partial
import time
from typing import Any, cast, override
from typing import Any, cast
import jwt
@@ -109,7 +109,6 @@ class AuthManagerFlowManager(
super().__init__(hass)
self.auth_manager = auth_manager
@override
async def async_create_flow(
self,
handler_key: tuple[str, str],
@@ -123,7 +122,6 @@ class AuthManagerFlowManager(
raise KeyError(f"Unknown auth provider {handler_key}")
return await auth_provider.async_login_flow(context)
@override
async def async_finish_flow(
self,
flow: FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
-1
View File
@@ -39,7 +39,6 @@ class _PyJWSWithLoadCache(PyJWS):
# We only ever have a global instance of this class
# so we do not have to worry about the LRU growing
# each time we create a new instance.
@override
def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict, bytes]:
"""Load a JWS."""
return super()._load(jwt)
@@ -1,6 +1,6 @@
"""Example auth module."""
from typing import Any, override
from typing import Any
import voluptuous as vol
@@ -35,7 +35,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
self._data = config["data"]
@property
@override
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({vol.Required("pin"): str})
@@ -45,7 +44,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
"""Validate async_setup_user input data."""
return vol.Schema({vol.Required("pin"): str})
@override
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
@@ -53,7 +51,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
"""
return SetupFlow(self, self.setup_schema, user_id)
@override
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up user to use mfa module."""
# data shall has been validate in caller
@@ -67,7 +64,6 @@ class InsecureExampleModule(MultiFactorAuthModule):
self._data.append({"user_id": user_id, "pin": pin})
@override
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
found = None
@@ -78,12 +74,10 @@ class InsecureExampleModule(MultiFactorAuthModule):
if found:
self._data.remove(found)
@override
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
return any(data["user_id"] == user_id for data in self._data)
@override
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
return any(
+1 -8
View File
@@ -5,7 +5,7 @@ Sending HOTP through notify service
import asyncio
import logging
from typing import Any, cast, override
from typing import Any, cast
import attr
import voluptuous as vol
@@ -107,7 +107,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
self._init_lock = asyncio.Lock()
@property
@override
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
@@ -160,7 +159,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
return sorted(unordered_services)
@override
async def async_setup_flow(self, user_id: str) -> NotifySetupFlow:
"""Return a data entry flow handler for setup module.
@@ -170,7 +168,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
self, self.input_schema, user_id, self.aync_get_available_notify_services()
)
@override
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up auth module for user."""
if self._user_settings is None:
@@ -184,7 +181,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
await self._async_save()
@override
async def async_depose_user(self, user_id: str) -> None:
"""Depose auth module for user."""
if self._user_settings is None:
@@ -194,7 +190,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
if self._user_settings.pop(user_id, None):
await self._async_save()
@override
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
if self._user_settings is None:
@@ -203,7 +198,6 @@ class NotifyAuthModule(MultiFactorAuthModule):
return user_id in self._user_settings
@override
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._user_settings is None:
@@ -289,7 +283,6 @@ class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
self._notify_service: str | None = None
self._target: str | None = None
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
+1 -8
View File
@@ -2,7 +2,7 @@
import asyncio
from io import BytesIO
from typing import Any, cast, override
from typing import Any, cast
import voluptuous as vol
@@ -87,7 +87,6 @@ class TotpAuthModule(MultiFactorAuthModule):
self._init_lock = asyncio.Lock()
@property
@override
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({vol.Required(INPUT_FIELD_CODE): str})
@@ -116,7 +115,6 @@ class TotpAuthModule(MultiFactorAuthModule):
self._users[user_id] = ota_secret # type: ignore[index]
return ota_secret
@override
async def async_setup_flow(self, user_id: str) -> TotpSetupFlow:
"""Return a data entry flow handler for setup module.
@@ -126,7 +124,6 @@ class TotpAuthModule(MultiFactorAuthModule):
assert user is not None
return TotpSetupFlow(self, self.input_schema, user)
@override
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
"""Set up auth module for user."""
if self._users is None:
@@ -139,7 +136,6 @@ class TotpAuthModule(MultiFactorAuthModule):
await self._async_save()
return result
@override
async def async_depose_user(self, user_id: str) -> None:
"""Depose auth module for user."""
if self._users is None:
@@ -148,7 +144,6 @@ class TotpAuthModule(MultiFactorAuthModule):
if self._users.pop(user_id, None): # type: ignore[union-attr]
await self._async_save()
@override
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
if self._users is None:
@@ -156,7 +151,6 @@ class TotpAuthModule(MultiFactorAuthModule):
return user_id in self._users # type: ignore[operator]
@override
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
@@ -195,7 +189,6 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
super().__init__(auth_module, setup_schema, user.id)
self._user = user
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
+1 -6
View File
@@ -1,7 +1,7 @@
"""Permissions for Home Assistant."""
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, override
from typing import TYPE_CHECKING
import voluptuous as vol
@@ -68,17 +68,14 @@ class PolicyPermissions(AbstractPermissions):
self._policy = policy
self._perm_lookup = perm_lookup
@override
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return test_all(self._policy.get(CAT_ENTITIES), key)
@override
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
@override
def __eq__(self, other: object) -> bool:
"""Equals check."""
return isinstance(other, PolicyPermissions) and other._policy == self._policy
@@ -87,12 +84,10 @@ class PolicyPermissions(AbstractPermissions):
class _OwnerPermissions(AbstractPermissions):
"""Owner permissions."""
@override
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return True
@override
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return lambda entity_id, key: True
+1 -5
View File
@@ -4,7 +4,7 @@ import asyncio
from collections.abc import Mapping
import logging
import os
from typing import Any, override
from typing import Any
import voluptuous as vol
@@ -57,7 +57,6 @@ class CommandLineAuthProvider(AuthProvider):
super().__init__(*args, **kwargs)
self._user_meta: dict[str, dict[str, Any]] = {}
@override
async def async_login_flow(
self, context: AuthFlowContext | None
) -> CommandLineLoginFlow:
@@ -106,7 +105,6 @@ class CommandLineAuthProvider(AuthProvider):
meta[key] = value
self._user_meta[username] = meta
@override
async def async_get_or_create_credentials(
self, flow_result: Mapping[str, str]
) -> Credentials:
@@ -119,7 +117,6 @@ class CommandLineAuthProvider(AuthProvider):
# Create new credentials.
return self.async_create_credentials({"username": username})
@override
async def async_user_meta_for_credentials(
self, credentials: Credentials
) -> UserMeta:
@@ -139,7 +136,6 @@ class CommandLineAuthProvider(AuthProvider):
class CommandLineLoginFlow(LoginFlow[CommandLineAuthProvider]):
"""Handler for the login flow."""
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> AuthFlowResult:
@@ -4,7 +4,7 @@ import asyncio
import base64
from collections.abc import Mapping
import logging
from typing import Any, cast, override
from typing import Any, cast
import bcrypt
import voluptuous as vol
@@ -302,7 +302,6 @@ class HassAuthProvider(AuthProvider):
self.data: Data | None = None
self._init_lock = asyncio.Lock()
@override
async def async_initialize(self) -> None:
"""Initialize the auth provider."""
async with self._init_lock:
@@ -313,7 +312,6 @@ class HassAuthProvider(AuthProvider):
await data.async_load()
self.data = data
@override
async def async_login_flow(self, context: AuthFlowContext | None) -> HassLoginFlow:
"""Return a flow to login."""
return HassLoginFlow(self)
@@ -371,7 +369,6 @@ class HassAuthProvider(AuthProvider):
)
await self.data.async_save()
@override
async def async_get_or_create_credentials(
self, flow_result: Mapping[str, str]
) -> Credentials:
@@ -390,7 +387,6 @@ class HassAuthProvider(AuthProvider):
# Create new credentials.
return self.async_create_credentials({"username": username})
@override
async def async_user_meta_for_credentials(
self, credentials: Credentials
) -> UserMeta:
@@ -414,7 +410,6 @@ class HassAuthProvider(AuthProvider):
class HassLoginFlow(LoginFlow[HassAuthProvider]):
"""Handler for the login flow."""
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> AuthFlowResult:
@@ -2,7 +2,6 @@
from collections.abc import Mapping
import hmac
from typing import override
import voluptuous as vol
@@ -34,7 +33,6 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
@override
async def async_login_flow(
self, context: AuthFlowContext | None
) -> ExampleLoginFlow:
@@ -63,7 +61,6 @@ class ExampleAuthProvider(AuthProvider):
):
raise InvalidAuthError
@override
async def async_get_or_create_credentials(
self, flow_result: Mapping[str, str]
) -> Credentials:
@@ -77,7 +74,6 @@ class ExampleAuthProvider(AuthProvider):
# Create new credentials.
return self.async_create_credentials({"username": username})
@override
async def async_user_meta_for_credentials(
self, credentials: Credentials
) -> UserMeta:
@@ -99,7 +95,6 @@ class ExampleAuthProvider(AuthProvider):
class ExampleLoginFlow(LoginFlow[ExampleAuthProvider]):
"""Handler for the login flow."""
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> AuthFlowResult:
@@ -13,7 +13,7 @@ from ipaddress import (
ip_address,
ip_network,
)
from typing import Any, cast, override
from typing import Any, cast
import voluptuous as vol
@@ -98,12 +98,10 @@ class TrustedNetworksAuthProvider(AuthProvider):
]
@property
@override
def support_mfa(self) -> bool:
"""Trusted Networks auth provider does not support MFA."""
return False
@override
async def async_login_flow(
self, context: AuthFlowContext | None
) -> TrustedNetworksLoginFlow:
@@ -146,7 +144,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
self.config[CONF_ALLOW_BYPASS_LOGIN],
)
@override
async def async_get_or_create_credentials(
self, flow_result: Mapping[str, str]
) -> Credentials:
@@ -175,7 +172,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
# We only allow login as exist user
raise InvalidUserError
@override
async def async_user_meta_for_credentials(
self, credentials: Credentials
) -> UserMeta:
@@ -207,7 +203,6 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise InvalidAuthError("Can't allow access from Home Assistant Cloud")
@callback
@override
def async_validate_refresh_token(
self, refresh_token: RefreshToken, remote_ip: str | None = None
) -> None:
@@ -235,7 +230,6 @@ class TrustedNetworksLoginFlow(LoginFlow[TrustedNetworksAuthProvider]):
self._ip_address = ip_addr
self._allow_bypass_login = allow_bypass_login
@override
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> AuthFlowResult:
+1 -2
View File
@@ -14,7 +14,7 @@ import platform
import sys
import threading
from time import monotonic
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, Any
# Import cryptography early since import openssl is not thread-safe
# _frozen_importlib._DeadlockError: deadlock detected by
@@ -697,7 +697,6 @@ def _create_log_file(
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
"""RotatingFileHandler that does not check if it should roll over on every log."""
@override
def shouldRollover(self, record: logging.LogRecord) -> bool:
"""Never roll over.
+1
View File
@@ -7,6 +7,7 @@
"unifi_access",
"unifi_direct",
"unifi_discovery",
"unifiled",
"unifiprotect"
]
}
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
No custom actions are defined.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -0,0 +1 @@
"""The acer_projector component."""
@@ -0,0 +1,34 @@
"""Use serial protocol of Acer projector to obtain state of the projector."""
from typing import Final
from homeassistant.const import STATE_OFF, STATE_ON
CONF_READ_TIMEOUT: Final = "timeout"
CONF_WRITE_TIMEOUT: Final = "write_timeout"
DEFAULT_NAME: Final = "Acer Projector"
DEFAULT_READ_TIMEOUT: Final = 1
DEFAULT_WRITE_TIMEOUT: Final = 1
ECO_MODE: Final = "ECO Mode"
ICON: Final = "mdi:projector"
INPUT_SOURCE: Final = "Input Source"
LAMP: Final = "Lamp"
LAMP_HOURS: Final = "Lamp Hours"
MODEL: Final = "Model"
# Commands known to the projector
CMD_DICT: Final[dict[str, str]] = {
LAMP: "* 0 Lamp ?\r",
LAMP_HOURS: "* 0 Lamp\r",
INPUT_SOURCE: "* 0 Src ?\r",
ECO_MODE: "* 0 IR 052\r",
MODEL: "* 0 IR 035\r",
STATE_ON: "* 0 IR 001\r",
STATE_OFF: "* 0 IR 002\r",
}
@@ -0,0 +1,9 @@
{
"domain": "acer_projector",
"name": "Acer Projector",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["serialx==1.8.2"]
}
@@ -0,0 +1,155 @@
"""Use serial protocol of Acer projector to obtain state of the projector."""
import logging
import re
from typing import Any, override
from serialx import Serial, SerialException
import voluptuous as vol
from homeassistant.components.switch import (
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
SwitchEntity,
)
from homeassistant.const import (
CONF_FILENAME,
CONF_NAME,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CMD_DICT,
CONF_READ_TIMEOUT,
CONF_WRITE_TIMEOUT,
DEFAULT_NAME,
DEFAULT_READ_TIMEOUT,
DEFAULT_WRITE_TIMEOUT,
ECO_MODE,
ICON,
INPUT_SOURCE,
LAMP,
LAMP_HOURS,
)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_FILENAME): cv.isdevice,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_READ_TIMEOUT, default=DEFAULT_READ_TIMEOUT): cv.positive_int,
vol.Optional(
CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT
): cv.positive_int,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Connect with serial port and return Acer Projector."""
serial_port = config[CONF_FILENAME]
name = config[CONF_NAME]
read_timeout = config[CONF_READ_TIMEOUT]
write_timeout = config[CONF_WRITE_TIMEOUT]
add_entities([AcerSwitch(serial_port, name, read_timeout, write_timeout)], True)
class AcerSwitch(SwitchEntity):
"""Represents an Acer Projector as a switch."""
_attr_icon = ICON
def __init__(
self,
serial_port: str,
name: str,
read_timeout: int,
write_timeout: int,
) -> None:
"""Init of the Acer projector."""
self._serial_port = serial_port
self._read_timeout = read_timeout
self._write_timeout = write_timeout
self._attr_name = name
self._attributes = {
LAMP_HOURS: STATE_UNKNOWN,
INPUT_SOURCE: STATE_UNKNOWN,
ECO_MODE: STATE_UNKNOWN,
}
def _write_read(self, msg: str) -> str:
"""Write to the projector and read the return."""
# Sometimes the projector won't answer for no reason or the projector
# was disconnected during runtime.
# This way the projector can be reconnected and will still work
try:
with Serial.from_url(
self._serial_port,
read_timeout=self._read_timeout,
write_timeout=self._write_timeout,
) as serial:
serial.write(msg.encode("utf-8"))
# Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually
# need to wait for timeout
return serial.read_until(size=20).decode("utf-8")
except (OSError, SerialException, TimeoutError) as exc:
raise HomeAssistantError(
f"Problem communicating with {self._serial_port}"
) from exc
def _write_read_format(self, msg: str) -> str:
"""Write msg, obtain answer and format output."""
# answers are formatted as ***\answer\r***
awns = self._write_read(msg)
if match := re.search(r"\r(.+)\r", awns):
return match.group(1)
return STATE_UNKNOWN
def update(self) -> None:
"""Get the latest state from the projector."""
awns = self._write_read_format(CMD_DICT[LAMP])
if awns == "Lamp 1":
self._attr_is_on = True
self._attr_available = True
elif awns == "Lamp 0":
self._attr_is_on = False
self._attr_available = True
else:
self._attr_available = False
for key in self._attributes:
if msg := CMD_DICT.get(key):
awns = self._write_read_format(msg)
self._attributes[key] = awns
self._attr_extra_state_attributes = self._attributes
@override
def turn_on(self, **kwargs: Any) -> None:
"""Turn the projector on."""
msg = CMD_DICT[STATE_ON]
self._write_read(msg)
self._attr_is_on = True
@override
def turn_off(self, **kwargs: Any) -> None:
"""Turn the projector off."""
msg = CMD_DICT[STATE_OFF]
self._write_read(msg)
self._attr_is_on = False
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: This integration does not have custom service actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: This integration does not subscribe to external events.
@@ -18,15 +18,9 @@ rules:
comment: Data descriptions missing
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: todo
docs-removal-instructions: todo
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Entities do not explicitly subscribe to events.
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: This integration does not register any events.
@@ -279,7 +279,7 @@
"title": "Air Quality",
"triggers": {
"co2_changed": {
"description": "Triggers when one or more carbon dioxide levels change.",
"description": "Triggers after one or more carbon dioxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -288,7 +288,7 @@
"name": "Carbon dioxide level changed"
},
"co2_crossed_threshold": {
"description": "Triggers when one or more carbon dioxide levels cross a threshold.",
"description": "Triggers after one or more carbon dioxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -303,7 +303,7 @@
"name": "Carbon dioxide level crossed threshold"
},
"co_changed": {
"description": "Triggers when one or more carbon monoxide levels change.",
"description": "Triggers after one or more carbon monoxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -312,7 +312,7 @@
"name": "Carbon monoxide level changed"
},
"co_cleared": {
"description": "Triggers when one or more carbon monoxide sensors stop detecting carbon monoxide.",
"description": "Triggers after one or more carbon monoxide sensors stop detecting carbon monoxide.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -324,7 +324,7 @@
"name": "Carbon monoxide cleared"
},
"co_crossed_threshold": {
"description": "Triggers when one or more carbon monoxide levels cross a threshold.",
"description": "Triggers after one or more carbon monoxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -339,7 +339,7 @@
"name": "Carbon monoxide level crossed threshold"
},
"co_detected": {
"description": "Triggers when one or more carbon monoxide sensors start detecting carbon monoxide.",
"description": "Triggers after one or more carbon monoxide sensors start detecting carbon monoxide.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -351,7 +351,7 @@
"name": "Carbon monoxide detected"
},
"gas_cleared": {
"description": "Triggers when one or more gas sensors stop detecting gas.",
"description": "Triggers after one or more gas sensors stop detecting gas.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -363,7 +363,7 @@
"name": "Gas cleared"
},
"gas_detected": {
"description": "Triggers when one or more gas sensors start detecting gas.",
"description": "Triggers after one or more gas sensors start detecting gas.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -375,7 +375,7 @@
"name": "Gas detected"
},
"n2o_changed": {
"description": "Triggers when one or more nitrous oxide levels change.",
"description": "Triggers after one or more nitrous oxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -384,7 +384,7 @@
"name": "Nitrous oxide level changed"
},
"n2o_crossed_threshold": {
"description": "Triggers when one or more nitrous oxide levels cross a threshold.",
"description": "Triggers after one or more nitrous oxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -399,7 +399,7 @@
"name": "Nitrous oxide level crossed threshold"
},
"no2_changed": {
"description": "Triggers when one or more nitrogen dioxide levels change.",
"description": "Triggers after one or more nitrogen dioxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -408,7 +408,7 @@
"name": "Nitrogen dioxide level changed"
},
"no2_crossed_threshold": {
"description": "Triggers when one or more nitrogen dioxide levels cross a threshold.",
"description": "Triggers after one or more nitrogen dioxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -423,7 +423,7 @@
"name": "Nitrogen dioxide level crossed threshold"
},
"no_changed": {
"description": "Triggers when one or more nitrogen monoxide levels change.",
"description": "Triggers after one or more nitrogen monoxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -432,7 +432,7 @@
"name": "Nitrogen monoxide level changed"
},
"no_crossed_threshold": {
"description": "Triggers when one or more nitrogen monoxide levels cross a threshold.",
"description": "Triggers after one or more nitrogen monoxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -447,7 +447,7 @@
"name": "Nitrogen monoxide level crossed threshold"
},
"ozone_changed": {
"description": "Triggers when one or more ozone levels change.",
"description": "Triggers after one or more ozone levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -456,7 +456,7 @@
"name": "Ozone level changed"
},
"ozone_crossed_threshold": {
"description": "Triggers when one or more ozone levels cross a threshold.",
"description": "Triggers after one or more ozone levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -471,7 +471,7 @@
"name": "Ozone level crossed threshold"
},
"pm10_changed": {
"description": "Triggers when one or more PM10 levels change.",
"description": "Triggers after one or more PM10 levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -480,7 +480,7 @@
"name": "PM10 level changed"
},
"pm10_crossed_threshold": {
"description": "Triggers when one or more PM10 levels cross a threshold.",
"description": "Triggers after one or more PM10 levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -495,7 +495,7 @@
"name": "PM10 level crossed threshold"
},
"pm1_changed": {
"description": "Triggers when one or more PM1 levels change.",
"description": "Triggers after one or more PM1 levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -504,7 +504,7 @@
"name": "PM1 level changed"
},
"pm1_crossed_threshold": {
"description": "Triggers when one or more PM1 levels cross a threshold.",
"description": "Triggers after one or more PM1 levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -519,7 +519,7 @@
"name": "PM1 level crossed threshold"
},
"pm25_changed": {
"description": "Triggers when one or more PM2.5 levels change.",
"description": "Triggers after one or more PM2.5 levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -528,7 +528,7 @@
"name": "PM2.5 level changed"
},
"pm25_crossed_threshold": {
"description": "Triggers when one or more PM2.5 levels cross a threshold.",
"description": "Triggers after one or more PM2.5 levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -543,7 +543,7 @@
"name": "PM2.5 level crossed threshold"
},
"pm4_changed": {
"description": "Triggers when one or more PM4 levels change.",
"description": "Triggers after one or more PM4 levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -552,7 +552,7 @@
"name": "PM4 level changed"
},
"pm4_crossed_threshold": {
"description": "Triggers when one or more PM4 levels cross a threshold.",
"description": "Triggers after one or more PM4 levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -567,7 +567,7 @@
"name": "PM4 level crossed threshold"
},
"smoke_cleared": {
"description": "Triggers when one or more smoke sensors stop detecting smoke.",
"description": "Triggers after one or more smoke sensors stop detecting smoke.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -579,7 +579,7 @@
"name": "Smoke cleared"
},
"smoke_detected": {
"description": "Triggers when one or more smoke sensors start detecting smoke.",
"description": "Triggers after one or more smoke sensors start detecting smoke.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -591,7 +591,7 @@
"name": "Smoke detected"
},
"so2_changed": {
"description": "Triggers when one or more sulphur dioxide levels change.",
"description": "Triggers after one or more sulphur dioxide levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -600,7 +600,7 @@
"name": "Sulphur dioxide level changed"
},
"so2_crossed_threshold": {
"description": "Triggers when one or more sulphur dioxide levels cross a threshold.",
"description": "Triggers after one or more sulphur dioxide levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -615,7 +615,7 @@
"name": "Sulphur dioxide level crossed threshold"
},
"voc_changed": {
"description": "Triggers when one or more volatile organic compound levels change.",
"description": "Triggers after one or more volatile organic compound levels change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -624,7 +624,7 @@
"name": "Volatile organic compounds level changed"
},
"voc_crossed_threshold": {
"description": "Triggers when one or more volatile organic compounds levels cross a threshold.",
"description": "Triggers after one or more volatile organic compounds levels cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -639,7 +639,7 @@
"name": "Volatile organic compounds level crossed threshold"
},
"voc_ratio_changed": {
"description": "Triggers when one or more volatile organic compound ratios change.",
"description": "Triggers after one or more volatile organic compound ratios change.",
"fields": {
"threshold": {
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
@@ -648,7 +648,7 @@
"name": "Volatile organic compounds ratio changed"
},
"voc_ratio_crossed_threshold": {
"description": "Triggers when one or more volatile organic compounds ratios cross a threshold.",
"description": "Triggers after one or more volatile organic compounds ratios cross a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -14,15 +14,9 @@ rules:
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Integration does not subscribe to events.
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Integration does not use event subscriptions.
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["airos==0.6.9"]
"requirements": ["airos==0.6.8"]
}
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: airOS does not have actions
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: local_polling without events
@@ -10,15 +10,9 @@ rules:
docs-actions:
status: exempt
comment: Integration does not provide custom actions
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: Integration does not register any service actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Integration does not subscribe to external events.
@@ -8,7 +8,7 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( # noqa: F401
from homeassistant.const import (
ATTR_CODE,
ATTR_CODE_FORMAT,
SERVICE_ALARM_ARM_AWAY,
@@ -28,12 +28,11 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401
from .const import (
ATTR_CHANGED_BY,
ATTR_CODE_ARM_REQUIRED,
DOMAIN,
AlarmControlPanelEntityFeature,
AlarmControlPanelEntityStateAttribute,
AlarmControlPanelState,
CodeFormat,
)
@@ -304,11 +303,9 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
return {
AlarmControlPanelEntityStateAttribute.CODE_FORMAT: self.code_format,
AlarmControlPanelEntityStateAttribute.CHANGED_BY: self.changed_by,
AlarmControlPanelEntityStateAttribute.CODE_ARM_REQUIRED: (
self.code_arm_required
),
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
ATTR_CODE_ARM_REQUIRED: self.code_arm_required,
}
@override
@@ -9,14 +9,6 @@ ATTR_CHANGED_BY: Final = "changed_by"
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
class AlarmControlPanelEntityStateAttribute(StrEnum):
"""State attributes for alarm control panel entities."""
CODE_FORMAT = "code_format"
CHANGED_BY = "changed_by"
CODE_ARM_REQUIRED = "code_arm_required"
class AlarmControlPanelState(StrEnum):
"""Alarm control panel entity states."""
@@ -238,7 +238,7 @@
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers when one or more alarms become armed, regardless of the mode.",
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -250,7 +250,7 @@
"name": "Alarm armed"
},
"armed_away": {
"description": "Triggers when one or more alarms become armed in away mode.",
"description": "Triggers after one or more alarms become armed in away mode.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -262,7 +262,7 @@
"name": "Alarm armed away"
},
"armed_home": {
"description": "Triggers when one or more alarms become armed in home mode.",
"description": "Triggers after one or more alarms become armed in home mode.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -274,7 +274,7 @@
"name": "Alarm armed home"
},
"armed_night": {
"description": "Triggers when one or more alarms become armed in night mode.",
"description": "Triggers after one or more alarms become armed in night mode.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -286,7 +286,7 @@
"name": "Alarm armed night"
},
"armed_vacation": {
"description": "Triggers when one or more alarms become armed in vacation mode.",
"description": "Triggers after one or more alarms become armed in vacation mode.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -298,7 +298,7 @@
"name": "Alarm armed vacation"
},
"disarmed": {
"description": "Triggers when one or more alarms become disarmed.",
"description": "Triggers after one or more alarms become disarmed.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -310,7 +310,7 @@
"name": "Alarm disarmed"
},
"triggered": {
"description": "Triggers when one or more alarms become triggered.",
"description": "Triggers after one or more alarms become triggered.",
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
@@ -16,7 +16,6 @@ PLATFORMS = [
Platform.EVENT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TODO,
@@ -5,16 +5,6 @@
"default": "mdi:chat-processing"
}
},
"select": {
"dropin": {
"default": "mdi:account-multiple-plus",
"state": {
"all": "mdi:account-multiple-plus",
"home": "mdi:home-plus",
"off": "mdi:phone-off"
}
}
},
"sensor": {
"voc_index": {
"default": "mdi:molecule"
@@ -23,7 +23,6 @@ class AmazonNotifyEntityDescription(NotifyEntityDescription):
"""Alexa Devices notify entity description."""
is_supported: Callable[[AmazonDevice], bool] = lambda _device: True
is_available_fn: Callable[[AmazonDevice], bool] = lambda _device: True
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
subkey: str
@@ -40,9 +39,6 @@ NOTIFY: Final = (
key="announce",
translation_key="announce",
subkey="AUDIO_PLAYER",
is_available_fn=lambda device: (
device.communication_settings.get("communications") != "OFF"
),
method=lambda api, device, message: api.call_alexa_announcement(
device, message
),
@@ -83,14 +79,6 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
entity_description: AmazonNotifyEntityDescription
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return (
self.entity_description.is_available_fn(self.device) and super().available
)
@override
async def async_send_message(
self, message: str, title: str | None = None, **kwargs: Any
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: no actions
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: entities do not explicitly subscribe to events
@@ -1,105 +0,0 @@
"""Support for select entities."""
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final, override
from aioamazondevices.structures import AmazonDropInStatus
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry, AmazonDevice, alexa_api_call
from .entity import AmazonEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonSelectEntityDescription(SelectEntityDescription):
"""Alexa Devices select entity description."""
method: str
SELECTS: Final = (
AmazonSelectEntityDescription(
key="dropin",
translation_key="dropin",
entity_category=EntityCategory.CONFIG,
method="set_dropin_status",
# API values: "All", "Home", "Off"
options=[status.value.lower() for status in AmazonDropInStatus],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up select entities based on a config entry."""
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
select_entities = [
AmazonSelectEntity(coordinator, serial_num, select_desc)
for select_desc in SELECTS
for serial_num in new_devices
if select_desc.key
in coordinator.data[serial_num].communication_settings
]
async_add_entities(select_entities)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSelectEntity(AmazonEntity, SelectEntity):
"""Representation of a select entity for the default Alexa device."""
entity_description: AmazonSelectEntityDescription
@property
@override
def options(self) -> list[str]:
"""Return a list of available options."""
if TYPE_CHECKING:
assert self.entity_description.options is not None
return self.entity_description.options
@property
def _device(self) -> AmazonDevice:
"""Return the device."""
return self.coordinator.data[self._serial_num]
@override
async def async_added_to_hass(self) -> None:
"""Restore last known option."""
await super().async_added_to_hass()
self._attr_current_option = self._device.communication_settings[
self.entity_description.key
].lower()
@override
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
method = getattr(self.coordinator.api, self.entity_description.method)
if TYPE_CHECKING:
assert method is not None
async with alexa_api_call(self.coordinator):
await method(self.device, AmazonDropInStatus(option.capitalize()))
self._attr_current_option = option
self.async_write_ha_state()
@@ -78,16 +78,6 @@
"name": "Speak"
}
},
"select": {
"dropin": {
"name": "Drop In",
"state": {
"all": "Allow drop in from anyone",
"home": "Allow drop in from household members",
"off": "Don't allow drop in"
}
}
},
"sensor": {
"alarm": {
"name": "Next alarm"
@@ -150,9 +140,6 @@
"invalid_sound_value": {
"message": "Invalid sound {sound} specified"
},
"select_option_not_found": {
"message": "Selected option not found: {option}"
},
"unknown_exception": {
"message": "Unknown error occurred: {error}"
}
@@ -1,6 +1,6 @@
"""Platform for Alexa To-do integration."""
from typing import TYPE_CHECKING, override
from typing import TYPE_CHECKING
from aioamazondevices.structures import (
AmazonListInfo,
@@ -88,7 +88,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
)
@property
@override
def todo_items(self) -> list[TodoItem]:
"""Return all to-do items in the list."""
@@ -105,7 +104,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
for item in todo_items
]
@override
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
_LOGGER.debug(
@@ -126,7 +124,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
self._list.name,
)
@override
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete items from the to-do list."""
_LOGGER.debug("Called async_delete_todo_items for %s item(s)", len(uids))
@@ -153,7 +150,6 @@ class AlexaToDoList(AmazonServiceEntity, TodoListEntity):
existing_item.version,
)
@override
async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update an item in the To-do list."""
list_items_lookup = self.coordinator.todo_list_items[self._list.id]
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -0,0 +1 @@
"""The Ampio component."""
@@ -0,0 +1,105 @@
"""Support for Ampio Air Quality data."""
import logging
from typing import Final, override
from asmog import AmpioSmog
import voluptuous as vol
from homeassistant.components.air_quality import (
PLATFORM_SCHEMA as AIR_QUALITY_PLATFORM_SCHEMA,
AirQualityEntity,
)
from homeassistant.const import CONF_NAME
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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle
from .const import CONF_STATION_ID, SCAN_INTERVAL
_LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA: Final = AIR_QUALITY_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Ampio Smog air quality platform."""
name = config.get(CONF_NAME)
station_id = config[CONF_STATION_ID]
session = async_get_clientsession(hass)
api = AmpioSmogMapData(AmpioSmog(station_id, hass.loop, session))
await api.async_update()
if not api.api.data:
_LOGGER.error("Station %s is not available", station_id)
return
async_add_entities([AmpioSmogQuality(api, station_id, name)], True)
class AmpioSmogQuality(AirQualityEntity):
"""Implementation of an Ampio Smog air quality entity."""
_attr_attribution = "Data provided by Ampio"
def __init__(
self, api: AmpioSmogMapData, station_id: str, name: str | None
) -> None:
"""Initialize the air quality entity."""
self._ampio = api
self._station_id = station_id
self._name = name or api.api.name
@property
@override
def name(self) -> str:
"""Return the name of the air quality entity."""
return self._name
@property
@override
def unique_id(self) -> str:
"""Return unique_name."""
return f"ampio_smog_{self._station_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-domain
@property
@override
def particulate_matter_2_5(self) -> str | None:
"""Return the particulate matter 2.5 level."""
return self._ampio.api.pm2_5 # type: ignore[no-any-return]
@property
@override
def particulate_matter_10(self) -> str | None:
"""Return the particulate matter 10 level."""
return self._ampio.api.pm10 # type: ignore[no-any-return]
async def async_update(self) -> None:
"""Get the latest data from the AmpioMap API."""
await self._ampio.async_update()
class AmpioSmogMapData:
"""Get the latest data and update the states."""
def __init__(self, api: AmpioSmog) -> None:
"""Initialize the data object."""
self.api = api
@Throttle(SCAN_INTERVAL)
async def async_update(self) -> None:
"""Get the latest data from AmpioMap."""
await self.api.get_data()
+7
View File
@@ -0,0 +1,7 @@
"""Constants for Ampio Air Quality platform."""
from datetime import timedelta
from typing import Final
CONF_STATION_ID: Final = "station_id"
SCAN_INTERVAL: Final = timedelta(minutes=10)
@@ -0,0 +1,10 @@
{
"domain": "ampio",
"name": "Ampio Smart Smog System",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ampio",
"iot_class": "cloud_polling",
"loggers": ["asmog"],
"quality_scale": "legacy",
"requirements": ["asmog==0.0.6"]
}
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -12,15 +12,9 @@ rules:
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
No custom actions are defined.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -8,6 +8,6 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "gold",
"quality_scale": "silver",
"requirements": ["anthropic==0.108.0"]
}
@@ -4,7 +4,10 @@ rules:
status: exempt
comment: |
Integration has no actions.
appropriate-polling: done
appropriate-polling:
status: exempt
comment: |
Integration does not poll.
brands: done
common-modules: done
config-flow-test-coverage: done
@@ -14,15 +17,9 @@ rules:
status: exempt
comment: |
Integration has no actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -11,15 +11,9 @@ rules:
status: exempt
comment: |
The integration does not provide any actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -210,7 +210,6 @@ class AqvifyAggrDataCoordinator(
end_time = base_time.replace(minute=59).strftime(date_time_fmt)
return beg_time, end_time
@override
async def _async_update_data(self) -> dict[str, AqvifyHourAggregatedValues]:
"""Fetch device state."""
devices = self.config_entry.runtime_data.coordinator.data.devices
+7 -7
View File
@@ -1,23 +1,23 @@
{
"entity": {
"sensor": {
"available_volume": {
"default": "mdi:car-coolant-level"
},
"ground_water_level": {
"default": "mdi:arrow-collapse-down"
},
"in_flow": {
"default": "mdi:water-plus-outline"
},
"level_from_sensor": {
"meter_value": {
"default": "mdi:waves-arrow-up"
},
"level_from_top": {
"default": "mdi:waves"
},
"out_volume": {
"default": "mdi:water-pump"
},
"stored_volume": {
"default": "mdi:car-coolant-level"
},
"water_level": {
"default": "mdi:waves"
}
}
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "platinum",
"requirements": ["pyaqvify==0.0.12"]
"requirements": ["pyaqvify==0.0.11"]
}
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
The integration does not provide any actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
+6 -7
View File
@@ -46,8 +46,8 @@ class AqvifySensorAggrEntityDescription(SensorEntityDescription):
ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
AqvifySensorEntityDescription(
key="level_from_sensor",
translation_key="level_from_sensor",
key="meter_value",
translation_key="meter_value",
native_unit_of_measurement=UnitOfLength.METERS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
@@ -55,8 +55,8 @@ ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
value_fn=lambda value: value.meter_value,
),
AqvifySensorEntityDescription(
key="level_from_top",
translation_key="level_from_top",
key="water_level",
translation_key="water_level",
native_unit_of_measurement=UnitOfLength.METERS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DISTANCE,
@@ -64,8 +64,8 @@ ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
value_fn=lambda value: value.water_level,
),
AqvifySensorEntityDescription(
key="available_volume",
translation_key="available_volume",
key="stored_volume",
translation_key="stored_volume",
native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLUME_STORAGE,
@@ -171,7 +171,6 @@ class AqvifyAggrSensor(AqvifyAggrEntity, SensorEntity):
entity_description: AqvifySensorAggrEntityDescription
@property
@override
def native_value(self) -> StateType | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data[self.device_key])
+8 -8
View File
@@ -34,23 +34,23 @@
},
"entity": {
"sensor": {
"available_volume": {
"name": "Available volume"
},
"ground_water_level": {
"name": "Ground water level"
},
"in_flow": {
"name": "Inflow"
},
"level_from_sensor": {
"name": "Level from sensor"
},
"level_from_top": {
"name": "Level from top"
"meter_value": {
"name": "Meter value"
},
"out_volume": {
"name": "Outflow"
},
"stored_volume": {
"name": "Stored volume"
},
"water_level": {
"name": "Water level"
}
}
},
@@ -157,7 +157,7 @@
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers when one or more Assist satellites become idle after having processed a command.",
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -169,7 +169,7 @@
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers when one or more Assist satellites start listening for a command from someone.",
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -181,7 +181,7 @@
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers when one or more Assist satellites start processing a command after having heard it.",
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -193,7 +193,7 @@
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers when one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
+1 -1
View File
@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyatome"],
"quality_scale": "legacy",
"requirements": ["pyAtome==0.1.2"]
"requirements": ["pyAtome==0.1.1"]
}
@@ -14,15 +14,9 @@ rules:
status: exempt
comment: |
This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.const import ( # noqa: F401
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
ATTR_FLOOR_ID,
@@ -58,7 +58,7 @@ from homeassistant.helpers.issue_registry import (
async_delete_issue,
)
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.script import ( # noqa: F401
from homeassistant.helpers.script import (
ATTR_CUR,
ATTR_MAX,
CONF_MAX,
@@ -91,8 +91,6 @@ from .const import (
DEFAULT_INITIAL_STATE,
DOMAIN,
LOGGER,
AutomationEntityCapabilityAttribute,
AutomationEntityStateAttribute,
)
from .helpers import async_get_blueprints
from .trace import trace_automation
@@ -320,13 +318,7 @@ class BaseAutomationEntity(ToggleEntity, ABC):
"""Base class for automation entities."""
_entity_component_unrecorded_attributes = frozenset(
(
AutomationEntityStateAttribute.LAST_TRIGGERED,
AutomationEntityStateAttribute.MODE,
AutomationEntityStateAttribute.CUR,
AutomationEntityStateAttribute.MAX,
AutomationEntityCapabilityAttribute.ID,
)
(ATTR_LAST_TRIGGERED, ATTR_MODE, ATTR_CUR, ATTR_MAX, CONF_ID)
)
raw_config: ConfigType | None
@@ -335,7 +327,7 @@ class BaseAutomationEntity(ToggleEntity, ABC):
def capability_attributes(self) -> dict[str, Any] | None:
"""Return capability attributes."""
if self.unique_id is not None:
return {AutomationEntityCapabilityAttribute.ID: self.unique_id}
return {CONF_ID: self.unique_id}
return None
@cached_property
@@ -515,15 +507,13 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
@override
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the entity state attributes."""
attrs: dict[str, Any] = {
AutomationEntityStateAttribute.LAST_TRIGGERED: (
self.action_script.last_triggered
),
AutomationEntityStateAttribute.MODE: self.action_script.script_mode,
AutomationEntityStateAttribute.CUR: self.action_script.runs,
attrs = {
ATTR_LAST_TRIGGERED: self.action_script.last_triggered,
ATTR_MODE: self.action_script.script_mode,
ATTR_CUR: self.action_script.runs,
}
if self.action_script.supports_max:
attrs[AutomationEntityStateAttribute.MAX] = self.action_script.max_runs
attrs[ATTR_MAX] = self.action_script.max_runs
return attrs
@property
@@ -1,27 +1,10 @@
"""Constants for the automation integration."""
from enum import StrEnum
import logging
CONF_TRIGGER_VARIABLES = "trigger_variables"
DOMAIN = "automation"
class AutomationEntityCapabilityAttribute(StrEnum):
"""Capability attributes for automation entities."""
ID = "id"
class AutomationEntityStateAttribute(StrEnum):
"""State attributes for automation entities."""
LAST_TRIGGERED = "last_triggered"
MODE = "mode"
CUR = "current"
MAX = "max"
CONF_HIDE_ENTITY = "hide_entity"
CONF_CONDITION_TYPE = "condition_type"
@@ -16,15 +16,9 @@ rules:
status: exempt
comment: |
Integration does not provide custom services.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -38,7 +38,7 @@
"data_description": {
"host": "The URL of your Autoskope API endpoint. Only change this if you use a white-label portal."
},
"name": "Additional settings"
"name": "Advanced settings"
}
},
"title": "Connect to Autoskope"
@@ -0,0 +1 @@
"""The avion component."""
+123
View File
@@ -0,0 +1,123 @@
"""Support for Avion dimmers."""
import importlib
import time
from typing import Any, override
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_DEVICES,
CONF_ID,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ID): cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
}
)
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up an Avion switch."""
avion = importlib.import_module("avion")
lights = [
AvionLight(
avion.Avion(
mac=address,
passphrase=device_config[CONF_API_KEY],
name=device_config.get(CONF_NAME),
object_id=device_config.get(CONF_ID),
connect=False,
)
)
for address, device_config in config[CONF_DEVICES].items()
]
if CONF_USERNAME in config and CONF_PASSWORD in config:
lights.extend(
AvionLight(device)
for device in avion.get_devices(
config[CONF_USERNAME], config[CONF_PASSWORD]
)
)
add_entities(lights)
class AvionLight(LightEntity):
"""Representation of an Avion light."""
_attr_support_color_mode = ColorMode.BRIGHTNESS
_attr_support_color_modes = {ColorMode.BRIGHTNESS}
_attr_should_poll = False
_attr_assumed_state = True
_attr_is_on = True
def __init__(self, device):
"""Initialize the light."""
self._attr_name = device.name
self._attr_unique_id = device.mac
self._attr_brightness = 255
self._switch = device
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
avion = importlib.import_module("avion")
# Bluetooth LE is unreliable, and the connection may drop at any
# time. Make an effort to re-establish the link.
initial = time.monotonic()
while True:
if time.monotonic() - initial >= 10:
return False
try:
self._switch.set_brightness(brightness)
break
except avion.AvionException:
self._switch.connect()
return True
@override
def turn_on(self, **kwargs: Any) -> None:
"""Turn the specified or all lights on."""
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
self._attr_brightness = brightness
self.set_state(self.brightness)
self._attr_is_on = True
@override
def turn_off(self, **kwargs: Any) -> None:
"""Turn the specified or all lights off."""
self.set_state(0)
self._attr_is_on = False
@@ -0,0 +1,9 @@
{
"domain": "avion",
"name": "Avi-on",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/avion",
"iot_class": "assumed_state",
"quality_scale": "legacy",
"requirements": ["avion==0.10"]
}
@@ -28,15 +28,9 @@ rules:
comment: |
Dependency is not built in the CI
docs-actions: todo
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: No explicit event subscription
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: This integration does not have any custom actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Entities of this integration does not explicitly subscribe to events.
+20 -24
View File
@@ -27,7 +27,6 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import TextSelector, TextSelectorConfig
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.ssdp import (
ATTR_UPNP_FRIENDLY_NAME,
@@ -99,11 +98,8 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
else:
if not self.unique_id:
if (serial := self._get_formatted_serial(api)) is None:
return self.async_abort(reason="no_serial_number")
await self.async_set_unique_id(serial)
if (serial := self._get_serial_number(api)) is None:
return self.async_abort(reason="no_serial_number")
config = {
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_HOST: user_input[CONF_HOST],
@@ -112,6 +108,8 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
await self.async_set_unique_id(format_mac(serial))
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_and_abort(
@@ -126,7 +124,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
self.config = config | {CONF_MODEL: api.vapix.product_number}
return await self._create_entry()
return await self._create_entry(serial)
data = self.discovery_schema or {
vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),
@@ -143,7 +141,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def _create_entry(self) -> ConfigFlowResult:
async def _create_entry(self, serial: str) -> ConfigFlowResult:
"""Create entry for device.
Use the discovered device name when available.
@@ -151,7 +149,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
if (title_placeholders := self.context.get("title_placeholders")) is not None:
name = title_placeholders[CONF_NAME]
else:
name = f"{self.config[CONF_MODEL]} - {self.unique_id}"
name = f"{self.config[CONF_MODEL]} - {serial}"
self.config[CONF_NAME] = name
return self.async_create_entry(title=name, data=self.config)
@@ -198,7 +196,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._process_discovered_device(
{
CONF_HOST: discovery_info.ip,
CONF_MAC: discovery_info.macaddress,
CONF_MAC: format_mac(discovery_info.macaddress),
CONF_NAME: discovery_info.hostname,
CONF_PORT: 80,
}
@@ -213,7 +211,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._process_discovered_device(
{
CONF_HOST: url.hostname,
CONF_MAC: discovery_info.upnp[ATTR_UPNP_SERIAL],
CONF_MAC: format_mac(discovery_info.upnp[ATTR_UPNP_SERIAL]),
CONF_NAME: f"{discovery_info.upnp[ATTR_UPNP_FRIENDLY_NAME]}",
CONF_PORT: url.port,
}
@@ -227,7 +225,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._process_discovered_device(
{
CONF_HOST: discovery_info.host,
CONF_MAC: discovery_info.properties["macaddress"],
CONF_MAC: format_mac(discovery_info.properties["macaddress"]),
CONF_NAME: discovery_info.name.split(".", 1)[0],
CONF_PORT: discovery_info.port,
}
@@ -237,17 +235,17 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
self, discovery_info: dict[str, Any]
) -> ConfigFlowResult:
"""Prepare configuration for a discovered Axis device."""
serial = format_mac(discovery_info[CONF_MAC])
if serial[:8] not in AXIS_OUI:
if discovery_info[CONF_MAC][:8] not in AXIS_OUI:
return self.async_abort(reason="not_axis_device")
if is_link_local(ip_address(discovery_info[CONF_HOST])):
return self.async_abort(reason="link_local_address")
if await self.async_set_unique_id(serial):
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]}, reload_on_update=False
)
await self.async_set_unique_id(discovery_info[CONF_MAC])
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]}, reload_on_update=False
)
self.context.update(
{
@@ -261,9 +259,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
self.discovery_schema = {
vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),
vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): TextSelector(
TextSelectorConfig(read_only=True)
),
vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
@@ -272,16 +268,16 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_user()
@staticmethod
def _get_formatted_serial(api: axis.AxisDevice) -> str | None:
def _get_serial_number(api: axis.AxisDevice) -> str | None:
"""Retrieve the device serial number from the Axis API.
Tries basic_device_info first, then property_handler. Returns None if not found.
"""
vapix = api.vapix
if vapix.basic_device_info.initialized:
return format_mac(vapix.basic_device_info["0"].serial_number)
return vapix.basic_device_info["0"].serial_number
if vapix.params.property_handler.initialized:
return format_mac(vapix.params.property_handler["0"].system_serial_number)
return vapix.params.property_handler["0"].system_serial_number
return None
@@ -16,15 +16,9 @@ rules:
status: exempt
comment: |
This integration does not have any custom actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
@@ -14,15 +14,9 @@ rules:
docs-actions:
status: exempt
comment: This integration does not have any custom actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Entities of this integration do not explicitly subscribe to events.
@@ -14,28 +14,26 @@ from homeassistant.helpers.condition import (
make_entity_state_condition,
)
BATTERY_LOW_DOMAIN_SPECS: dict[str, DomainSpec] = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY),
BATTERY_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY)
}
BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
BATTERY_CHARGING_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
),
)
}
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
}
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_LOW_DOMAIN_SPECS,
BATTERY_DOMAIN_SPECS,
STATE_ON,
primary_entities_only=False,
),
"is_not_low": make_entity_state_condition(
BATTERY_LOW_DOMAIN_SPECS,
BATTERY_DOMAIN_SPECS,
STATE_OFF,
primary_entities_only=False,
),
@@ -0,0 +1 @@
"""The beewi_smartclim component."""
@@ -0,0 +1,10 @@
{
"domain": "beewi_smartclim",
"name": "BeeWi SmartClim BLE sensor",
"codeowners": ["@alemuro"],
"documentation": "https://www.home-assistant.io/integrations/beewi_smartclim",
"iot_class": "local_polling",
"loggers": ["beewi_smartclim"],
"quality_scale": "legacy",
"requirements": ["beewi-smartclim==0.0.10"]
}
@@ -0,0 +1,84 @@
"""Platform for beewi_smartclim integration."""
from beewi_smartclim import BeewiSmartClimPoller
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
)
from homeassistant.const import CONF_MAC, CONF_NAME, PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
# Default values
DEFAULT_NAME = "BeeWi SmartClim"
# Sensor config
SENSOR_TYPES = [
[SensorDeviceClass.TEMPERATURE, "Temperature", UnitOfTemperature.CELSIUS],
[SensorDeviceClass.HUMIDITY, "Humidity", PERCENTAGE],
[SensorDeviceClass.BATTERY, "Battery", PERCENTAGE],
]
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_MAC): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the beewi_smartclim platform."""
mac = config[CONF_MAC]
prefix = config[CONF_NAME]
poller = BeewiSmartClimPoller(mac)
sensors = []
for sensor_type in SENSOR_TYPES:
device = sensor_type[0]
name = sensor_type[1]
unit = sensor_type[2]
# `prefix` is the name configured by the user for the sensor, we're appending
# the device type at the end of the name (garden -> garden temperature)
if prefix:
name = f"{prefix} {name}"
sensors.append(BeewiSmartclimSensor(poller, name, mac, device, unit))
add_entities(sensors)
class BeewiSmartclimSensor(SensorEntity):
"""Representation of a Sensor."""
def __init__(self, poller, name, mac, device, unit):
"""Initialize the sensor."""
self._poller = poller
self._attr_name = name
self._device = device
self._attr_native_unit_of_measurement = unit
self._attr_device_class = self._device
self._attr_unique_id = f"{mac}_{device}"
def update(self) -> None:
"""Fetch new state data from the poller."""
self._poller.update_sensor()
self._attr_native_value = None
if self._device == SensorDeviceClass.TEMPERATURE:
self._attr_native_value = self._poller.get_temperature()
if self._device == SensorDeviceClass.HUMIDITY:
self._attr_native_value = self._poller.get_humidity()
if self._device == SensorDeviceClass.BATTERY:
self._attr_native_value = self._poller.get_battery()
@@ -17,10 +17,10 @@
"requirements": [
"bleak==3.0.2",
"bleak-retry-connector==4.6.1",
"bluetooth-adapters==2.4.0",
"bluetooth-adapters==2.3.0",
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.22",
"habluetooth==6.24.0"
"dbus-fast==5.0.16",
"habluetooth==6.8.3"
]
}
@@ -14,15 +14,9 @@ rules:
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -2,13 +2,13 @@
"domain": "bosch_shc",
"name": "Bosch SHC",
"after_dependencies": ["zeroconf"],
"codeowners": ["@tschamm", "@mosandlt"],
"codeowners": ["@tschamm"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.3.5"],
"requirements": ["boschshcpy==0.2.111"],
"zeroconf": [
{
"name": "bosch shc*",
@@ -10,15 +10,9 @@ rules:
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: The integration registers no events
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: The integration does not register services.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -8,15 +8,9 @@ rules:
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: |
+1 -1
View File
@@ -30,7 +30,7 @@
"title": "Button",
"triggers": {
"pressed": {
"description": "Triggers when one or more buttons are pressed.",
"description": "Triggers when a button was pressed",
"name": "Button pressed"
}
}
+7 -14
View File
@@ -68,7 +68,6 @@ from .const import (
EVENT_UID,
LIST_EVENT_FIELDS,
CalendarEntityFeature,
CalendarEntityStateAttribute,
)
# mypy: disallow-any-generics
@@ -520,9 +519,7 @@ class CalendarEntity(Entity):
entity_description: CalendarEntityDescription
_entity_component_unrecorded_attributes = frozenset(
{CalendarEntityStateAttribute.DESCRIPTION}
)
_entity_component_unrecorded_attributes = frozenset({"description"})
_alarm_unsubs: list[CALLBACK_TYPE] | None = None
_event_listeners: (
@@ -576,16 +573,12 @@ class CalendarEntity(Entity):
return None
return {
CalendarEntityStateAttribute.MESSAGE: event.summary,
CalendarEntityStateAttribute.ALL_DAY: event.all_day,
CalendarEntityStateAttribute.START_TIME: event.start_datetime_local.strftime(
DATE_STR_FORMAT
),
CalendarEntityStateAttribute.END_TIME: event.end_datetime_local.strftime(
DATE_STR_FORMAT
),
CalendarEntityStateAttribute.LOCATION: event.location or "",
CalendarEntityStateAttribute.DESCRIPTION: event.description or "",
"message": event.summary,
"all_day": event.all_day,
"start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT),
"end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT),
"location": event.location or "",
"description": event.description or "",
}
@final
+1 -12
View File
@@ -1,6 +1,6 @@
"""Constants for calendar components."""
from enum import IntFlag, StrEnum
from enum import IntFlag
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
@@ -14,17 +14,6 @@ DOMAIN = "calendar"
DATA_COMPONENT: HassKey[EntityComponent[CalendarEntity]] = HassKey(DOMAIN)
class CalendarEntityStateAttribute(StrEnum):
"""State attributes for calendar entities."""
MESSAGE = "message"
ALL_DAY = "all_day"
START_TIME = "start_time"
END_TIME = "end_time"
LOCATION = "location"
DESCRIPTION = "description"
class CalendarEntityFeature(IntFlag):
"""Supported features of the calendar entity."""
@@ -132,7 +132,7 @@
"title": "Calendar",
"triggers": {
"event_ended": {
"description": "Triggers when one or more calendar events end.",
"description": "Triggers when a calendar event ends.",
"fields": {
"offset": {
"description": "Offset from the end of the event.",
@@ -146,7 +146,7 @@
"name": "Calendar event ended"
},
"event_started": {
"description": "Triggers when one or more calendar events start.",
"description": "Triggers when a calendar event starts.",
"fields": {
"offset": {
"description": "Offset from the start of the event.",
@@ -14,15 +14,9 @@ rules:
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
+6 -11
View File
@@ -68,7 +68,6 @@ from .const import (
PREF_ORIENTATION,
PREF_PRELOAD_STREAM,
SERVICE_RECORD,
CameraEntityStateAttribute,
CameraState,
StreamType,
)
@@ -422,7 +421,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""The base class for camera entities."""
_entity_component_unrecorded_attributes = frozenset(
{CameraEntityStateAttribute.ACCESS_TOKEN, "entity_picture"}
{"access_token", "entity_picture"}
)
# Entity Properties
@@ -650,22 +649,18 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@property
@override
def state_attributes(self) -> dict[str, str | bool | None]:
def state_attributes(self) -> dict[str, str | None]:
"""Return the camera state attributes."""
attrs: dict[str, str | bool | None] = {
CameraEntityStateAttribute.ACCESS_TOKEN: self.access_tokens[-1]
}
attrs = {"access_token": self.access_tokens[-1]}
if model := self.model:
attrs[CameraEntityStateAttribute.MODEL_NAME] = model
attrs["model_name"] = model
if brand := self.brand:
attrs[CameraEntityStateAttribute.BRAND] = brand
attrs["brand"] = brand
if motion_detection_enabled := self.motion_detection_enabled:
attrs[CameraEntityStateAttribute.MOTION_DETECTION] = (
motion_detection_enabled
)
attrs["motion_detection"] = motion_detection_enabled
return attrs
-9
View File
@@ -28,15 +28,6 @@ CAMERA_STREAM_SOURCE_TIMEOUT: Final = 10
CAMERA_IMAGE_TIMEOUT: Final = 10
class CameraEntityStateAttribute(StrEnum):
"""State attributes for camera entities."""
ACCESS_TOKEN = "access_token"
MODEL_NAME = "model_name"
BRAND = "brand"
MOTION_DETECTION = "motion_detection"
class CameraState(StrEnum):
"""Camera entity states."""
@@ -12,15 +12,9 @@ rules:
docs-actions:
status: exempt
comment: No custom actions/services.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
@@ -174,13 +174,6 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
"""Set the fan mode."""
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
@override
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode."""
await self.coordinator.async_set_swing_mode(
self._ac_index, self.data, swing_mode
)
@override
async def async_turn_off(self) -> None:
"""Turn off."""

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