Compare commits

..

1 Commits

Author SHA1 Message Date
epenet
37170796c3 Drop ignore-missing-annotations from pylint 2025-12-02 11:00:59 +01:00
477 changed files with 3109 additions and 23207 deletions

View File

@@ -13,7 +13,6 @@ core: &core
# Our base platforms, that are used by other integrations
base_platforms: &base_platforms
- homeassistant/components/ai_task/**
- homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/**

View File

@@ -27,6 +27,7 @@
"charliermarsh.ruff",
"ms-python.pylint",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github",

View File

@@ -30,7 +30,7 @@ jobs:
architectures: ${{ env.ARCHITECTURES }}
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -96,7 +96,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -273,7 +273,7 @@ jobs:
- green
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set build additional args
run: |
@@ -311,7 +311,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -416,19 +416,9 @@ jobs:
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
@@ -474,7 +464,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -519,7 +509,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0

View File

@@ -41,8 +41,8 @@ env:
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.1"
DEFAULT_PYTHON: "3.13.9"
ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -99,7 +99,7 @@ jobs:
steps:
- &checkout
name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -686,14 +686,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y homeassistant
pylint homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint-tests:
name: Check pylint on tests

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6

View File

@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0

View File

@@ -31,7 +31,7 @@ jobs:
steps:
- &checkout
name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python

9
CODEOWNERS generated
View File

@@ -571,8 +571,6 @@ build.json @home-assistant/supervisor
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -1358,8 +1356,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter
@@ -1765,7 +1763,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
/tests/components/vivotek/ @HarlemSquirrel
/homeassistant/components/vizio/ @raman325
/tests/components/vizio/ @raman325
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
@@ -1805,8 +1802,6 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/web_rtc/ @home-assistant/core
/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core

View File

@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
),
}
),
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
),
}
),

View File

@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:

View File

@@ -44,7 +44,7 @@ rules:
discovery: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
@@ -54,7 +54,7 @@ rules:
comment: Single device integration, no dynamic device discovery needed.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: done
icon-translations: todo

View File

@@ -1,134 +0,0 @@
"""Sensor platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from pyairobotrest.models import ThermostatStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import AirobotConfigEntry
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotSensorEntityDescription(SensorEntityDescription):
"""Describes Airobot sensor entity."""
value_fn: Callable[[ThermostatStatus], StateType]
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
AirobotSensorEntityDescription(
key="air_temperature",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_air,
),
AirobotSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.hum_air,
),
AirobotSensorEntityDescription(
key="floor_temperature",
translation_key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_floor,
supported_fn=lambda status: status.has_floor_sensor,
),
AirobotSensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.co2,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.aqi,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="heating_uptime",
translation_key="heating_uptime",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.heating_uptime,
entity_registry_enabled_default=False,
),
AirobotSensorEntityDescription(
key="errors",
translation_key="errors",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.errors,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotSensor(coordinator, description)
for description in SENSOR_TYPES
if description.supported_fn(coordinator.data.status)
)
class AirobotSensor(AirobotEntity, SensorEntity):
"""Representation of an Airobot sensor."""
entity_description: AirobotSensorEntityDescription
def __init__(
self,
coordinator,
description: AirobotSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.status)

View File

@@ -43,25 +43,6 @@
}
}
},
"entity": {
"sensor": {
"air_temperature": {
"name": "Air temperature"
},
"device_uptime": {
"name": "Device uptime"
},
"errors": {
"name": "Error count"
},
"floor_temperature": {
"name": "Floor temperature"
},
"heating_uptime": {
"name": "Heating uptime"
}
}
},
"exceptions": {
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."

View File

@@ -159,74 +159,74 @@
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
"description": "Triggers when an alarm is armed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed"
"name": "When an alarm is armed"
},
"armed_away": {
"description": "Triggers after one or more alarms become armed in away mode.",
"description": "Triggers when an alarm is armed away.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed away"
"name": "When an alarm is armed away"
},
"armed_home": {
"description": "Triggers after one or more alarms become armed in home mode.",
"description": "Triggers when an alarm is armed home.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed home"
"name": "When an alarm is armed home"
},
"armed_night": {
"description": "Triggers after one or more alarms become armed in night mode.",
"description": "Triggers when an alarm is armed night.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed night"
"name": "When an alarm is armed night"
},
"armed_vacation": {
"description": "Triggers after one or more alarms become armed in vacation mode.",
"description": "Triggers when an alarm is armed vacation.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed vacation"
"name": "When an alarm is armed vacation"
},
"disarmed": {
"description": "Triggers after one or more alarms become disarmed.",
"description": "Triggers when an alarm is disarmed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm disarmed"
"name": "When an alarm is disarmed"
},
"triggered": {
"description": "Triggers after one or more alarms become triggered.",
"description": "Triggers when an alarm is triggered.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm triggered"
"name": "When an alarm is triggered"
}
}
}

View File

@@ -30,7 +30,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
@@ -69,19 +68,34 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input[CONF_ACCOUNT_NUMBER],
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
)
)
if isinstance(validation_response, BaseAuth):
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
title=account_number,
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0"]
"requirements": ["hassil==3.4.0"]
}

View File

@@ -112,44 +112,44 @@
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"description": "Triggers when an Assist satellite becomes idle.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite became idle"
"name": "When an Assist satellite becomes idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"description": "Triggers when an Assist satellite starts listening.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started listening"
"name": "When an Assist satellite starts listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"description": "Triggers when an Assist satellite is processing.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started processing"
"name": "When an Assist satellite is processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers when an Assist satellite is responding.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started responding"
"name": "When an Assist satellite is responding"
}
}
}

View File

@@ -124,7 +124,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"climate",
"cover",
"fan",

View File

@@ -8,8 +8,6 @@
"integration_type": "system",
"preview_features": {
"new_triggers_conditions": {
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
}
},

View File

@@ -69,10 +69,10 @@
},
"preview_features": {
"new_triggers_conditions": {
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Purpose-specific triggers and conditions"
"description": "Enables new intuitive triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own intuitive triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new intuitive triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new intuitive triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Intuitive triggers and conditions"
}
},
"services": {

View File

@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .websocket import BeoWebsocket
from .websocket import BangOlufsenWebsocket
@dataclass
class BeoData:
class BangOlufsenData:
"""Dataclass for API client and WebSocket client."""
websocket: BeoWebsocket
websocket: BangOlufsenWebsocket
client: MozartClient
type BeoConfigEntry = ConfigEntry[BeoData]
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove casts to str
assert entry.unique_id
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BeoWebsocket(hass, entry, client)
websocket = BangOlufsenWebsocket(hass, entry, client)
# Add the websocket and API client
entry.runtime_data = BeoData(websocket, client)
entry.runtime_data = BangOlufsenData(websocket, client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -82,7 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
"""Unload a config entry."""
# Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications()

View File

@@ -47,7 +47,7 @@ _exception_map = {
}
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
_beolink_jid = ""

View File

@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
)
class BeoSource:
class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
@@ -26,7 +26,7 @@ class BeoSource:
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BEO_STATES: dict[str, MediaPlayerState] = {
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
# Dict used for translating device states to Home Assistant states.
"started": MediaPlayerState.PLAYING,
"buffering": MediaPlayerState.PLAYING,
@@ -40,19 +40,19 @@ BEO_STATES: dict[str, MediaPlayerState] = {
}
# Dict used for translating Home Assistant settings to device repeat settings.
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BEO_REPEAT_FROM_HA.items()
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
}
# Media types for play_media
class BeoMediaType(StrEnum):
class BangOlufsenMediaType(StrEnum):
"""Bang & Olufsen specific media types."""
FAVOURITE = "favourite"
@@ -63,7 +63,7 @@ class BeoMediaType(StrEnum):
OVERLAY_TTS = "overlay_tts"
class BeoModel(StrEnum):
class BangOlufsenModel(StrEnum):
"""Enum for compatible model names."""
# Mozart devices
@@ -82,7 +82,7 @@ class BeoModel(StrEnum):
BEOREMOTE_ONE = "Beoremote One"
class BeoAttribute(StrEnum):
class BangOlufsenAttribute(StrEnum):
"""Enum for extra_state_attribute keys."""
BEOLINK = "beolink"
@@ -93,7 +93,7 @@ class BeoAttribute(StrEnum):
# Physical "buttons" on devices
class BeoButtons(StrEnum):
class BangOlufsenButtons(StrEnum):
"""Enum for device buttons."""
BLUETOOTH = "Bluetooth"
@@ -140,7 +140,7 @@ class WebsocketNotification(StrEnum):
DOMAIN: Final[str] = "bang_olufsen"
# Default values for configuration.
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
# Configuration.
CONF_SERIAL_NUMBER: Final = "serial_number"
@@ -148,7 +148,7 @@ CONF_BEOLINK_JID: Final = "jid"
# Models to choose from in manual configuration.
SELECTABLE_MODELS: list[str] = [
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
]
MANUFACTURER: Final[str] = "Bang & Olufsen"
@@ -160,15 +160,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
ATTR_FRIENDLY_NAME: Final[str] = "fn"
# Power states.
BEO_ON: Final[str] = "on"
BANG_OLUFSEN_ON: Final[str] = "on"
VALID_MEDIA_TYPES: Final[tuple] = (
BeoMediaType.FAVOURITE,
BeoMediaType.DEEZER,
BeoMediaType.RADIO,
BeoMediaType.TTS,
BeoMediaType.TIDAL,
BeoMediaType.OVERLAY_TTS,
BangOlufsenMediaType.FAVOURITE,
BangOlufsenMediaType.DEEZER,
BangOlufsenMediaType.RADIO,
BangOlufsenMediaType.TTS,
BangOlufsenMediaType.TIDAL,
BangOlufsenMediaType.OVERLAY_TTS,
MediaType.MUSIC,
MediaType.URL,
MediaType.CHANNEL,
@@ -246,7 +246,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
)
# Device events
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = {
@@ -263,7 +263,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [

View File

@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import DOMAIN
from .util import get_device_buttons
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BeoConfigEntry
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""

View File

@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class BeoBase:
"""Base class for Bang & Olufsen Home Assistant objects."""
class BangOlufsenBase:
"""Base class for BangOlufsen Home Assistant objects."""
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the object."""
@@ -51,8 +51,8 @@ class BeoBase:
)
class BeoEntity(Entity, BeoBase):
"""Base Entity for Bang & Olufsen entities."""
class BangOlufsenEntity(Entity, BangOlufsenBase):
"""Base Entity for BangOlufsen entities."""
_attr_has_entity_name = True
_attr_should_poll = False

View File

@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS,
@@ -25,10 +25,10 @@ from .const import (
DEVICE_BUTTON_EVENTS,
DOMAIN,
MANUFACTURER,
BeoModel,
BangOlufsenModel,
WebsocketNotification,
)
from .entity import BeoEntity
from .entity import BangOlufsenEntity
from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Event entities from config entry."""
entities: list[BeoEvent] = []
entities: list[BangOlufsenEvent] = []
async_add_entities(
BeoButtonEvent(config_entry, button_type)
BangOlufsenButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
)
@@ -54,7 +54,7 @@ async def async_setup_entry(
# Add Light keys
entities.extend(
[
BeoRemoteKeyEvent(
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
@@ -66,7 +66,7 @@ async def async_setup_entry(
# Add Control keys
entities.extend(
[
BeoRemoteKeyEvent(
BangOlufsenRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
@@ -84,9 +84,10 @@ async def async_setup_entry(
config_entry.entry_id
)
for device in devices:
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
remote.serial_number for remote in remotes
}:
if (
device.model == BangOlufsenModel.BEOREMOTE_ONE
and device.serial_number not in {remote.serial_number for remote in remotes}
):
device_registry.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id
)
@@ -94,13 +95,13 @@ async def async_setup_entry(
async_add_entities(new_entities=entities)
class BeoEvent(BeoEntity, EventEntity):
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
"""Base Event class."""
_attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False
def __init__(self, config_entry: BeoConfigEntry) -> None:
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
"""Initialize Event."""
super().__init__(config_entry, config_entry.runtime_data.client)
@@ -111,12 +112,12 @@ class BeoEvent(BeoEntity, EventEntity):
self.async_write_ha_state()
class BeoButtonEvent(BeoEvent):
class BangOlufsenButtonEvent(BangOlufsenEvent):
"""Event class for Button events."""
_attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
"""Initialize Button."""
super().__init__(config_entry)
@@ -145,14 +146,14 @@ class BeoButtonEvent(BeoEvent):
)
class BeoRemoteKeyEvent(BeoEvent):
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
"""Event class for Beoremote One key events."""
_attr_event_types = BEO_REMOTE_KEY_EVENTS
def __init__(
self,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
remote: PairedRemote,
key_type: str,
) -> None:
@@ -165,8 +166,8 @@ class BeoRemoteKeyEvent(BeoEvent):
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BeoModel.BEOREMOTE_ONE,
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BangOlufsenModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["mozart-api==5.3.1.108.0"],
"requirements": ["mozart-api==5.1.0.247.1"],
"zeroconf": ["_bangolufsen._tcp.local."]
}

View File

@@ -69,11 +69,11 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.util.dt import utcnow
from . import BeoConfigEntry
from . import BangOlufsenConfigEntry
from .const import (
BEO_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA,
BEO_STATES,
BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID,
@@ -82,12 +82,12 @@ from .const import (
FALLBACK_SOURCES,
MANUFACTURER,
VALID_MEDIA_TYPES,
BeoAttribute,
BeoMediaType,
BeoSource,
BangOlufsenAttribute,
BangOlufsenMediaType,
BangOlufsenSource,
WebsocketNotification,
)
from .entity import BeoEntity
from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0
@@ -96,7 +96,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BEO_FEATURES = (
BANG_OLUFSEN_FEATURES = (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.GROUPING
@@ -119,13 +119,15 @@ BEO_FEATURES = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Media Player entity from config entry."""
# Add MediaPlayer entity
async_add_entities(
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)],
new_entities=[
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
],
update_before_add=True,
)
@@ -185,7 +187,7 @@ async def async_setup_entry(
)
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player."""
_attr_name = None
@@ -286,7 +288,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None:
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat]
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
@@ -406,8 +408,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Check if source is line-in or optical and progress should be updated
if self._source_change.id in (
BeoSource.LINE_IN.id,
BeoSource.SPDIF.id,
BangOlufsenSource.LINE_IN.id,
BangOlufsenSource.SPDIF.id,
):
self._playback_progress = PlaybackProgress(progress=0)
@@ -448,8 +450,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add Beolink self
self._beolink_attributes = {
BeoAttribute.BEOLINK: {
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid}
BangOlufsenAttribute.BEOLINK: {
BangOlufsenAttribute.BEOLINK_SELF: {
self.device_entry.name: self._beolink_jid
}
}
}
@@ -457,12 +461,12 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
peers = await self._client.get_beolink_peers()
if len(peers) > 0:
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_PEERS
] = {}
for peer in peers:
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_PEERS
][peer.friendly_name] = peer.jid
# Add Beolink listeners / leader
@@ -484,8 +488,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add self
group_members.append(self.entity_id)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LEADER
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_LEADER
] = {
self._remote_leader.friendly_name: self._remote_leader.jid,
}
@@ -523,8 +527,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
beolink_listener.jid
)
break
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LISTENERS
self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BangOlufsenAttribute.BEOLINK_LISTENERS
] = beolink_listeners_attribute
self._attr_group_members = group_members
@@ -583,7 +587,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
for sound_mode in sound_modes:
label = f"{sound_mode.name} ({sound_mode.id})"
self._sound_modes[label] = cast(int, sound_mode.id)
self._sound_modes[label] = sound_mode.id
if sound_mode.id == active_sound_mode.id:
self._attr_sound_mode = label
@@ -596,7 +600,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BEO_FEATURES
features = BANG_OLUFSEN_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
@@ -607,7 +611,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
return BEO_STATES[self._state]
return BANG_OLUFSEN_STATES[self._state]
@property
def volume_level(self) -> float | None:
@@ -627,10 +631,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
def media_content_type(self) -> MediaType | str | None:
"""Return the current media type."""
content_type = {
BeoSource.URI_STREAMER.id: MediaType.URL,
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
BangOlufsenSource.URI_STREAMER.id: MediaType.URL,
BangOlufsenSource.DEEZER.id: BangOlufsenMediaType.DEEZER,
BangOlufsenSource.TIDAL.id: BangOlufsenMediaType.TIDAL,
BangOlufsenSource.NET_RADIO.id: BangOlufsenMediaType.RADIO,
}
# Hard to determine content type.
if self._source_change.id in content_type:
@@ -761,7 +765,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat])
play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
)
async def async_set_shuffle(self, shuffle: bool) -> None:
@@ -865,7 +871,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
self._volume.level.level + offset_volume, 100
)
if media_type == BeoMediaType.OVERLAY_TTS:
if media_type == BangOlufsenMediaType.OVERLAY_TTS:
# Bang & Olufsen cloud TTS
overlay_play_request.text_to_speech = (
OverlayPlayRequestTextToSpeechTextToSpeech(
@@ -882,14 +888,14 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# The "provider" media_type may not be suitable for overlay all the time.
# Use it for now.
elif media_type == BeoMediaType.TTS:
elif media_type == BangOlufsenMediaType.TTS:
await self._client.post_overlay_play(
overlay_play_request=OverlayPlayRequest(
uri=Uri(location=media_id),
)
)
elif media_type == BeoMediaType.RADIO:
elif media_type == BangOlufsenMediaType.RADIO:
await self._client.run_provided_scene(
scene_properties=SceneProperties(
action_list=[
@@ -901,13 +907,13 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
)
)
elif media_type == BeoMediaType.FAVOURITE:
elif media_type == BangOlufsenMediaType.FAVOURITE:
await self._client.activate_preset(id=int(media_id))
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL):
elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
try:
# Play Deezer flow.
if media_id == "flow" and media_type == BeoMediaType.DEEZER:
if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
deezer_id = None
if "id" in kwargs[ATTR_MEDIA_EXTRA]:

View File

@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -40,27 +40,16 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
]
def get_device_buttons(model: BeoModel) -> list[str]:
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
"""Get supported buttons for a given model."""
# Beoconnect Core does not have any buttons
if model == BeoModel.BEOCONNECT_CORE:
return []
buttons = DEVICE_BUTTONS.copy()
# Models that don't have a microphone button
if model in (
BeoModel.BEOSOUND_A5,
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Beosound Premiere does not have a bluetooth button
if model == BangOlufsenModel.BEOSOUND_PREMIERE:
buttons.remove(BangOlufsenButtons.BLUETOOTH)
# Models that don't have a Bluetooth button
if model in (
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
# Beoconnect Core does not have any buttons
elif model == BangOlufsenModel.BEOCONNECT_CORE:
buttons = []
return buttons

View File

@@ -27,20 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum
from .const import (
BEO_WEBSOCKET_EVENT,
BANG_OLUFSEN_WEBSOCKET_EVENT,
CONNECTION_STATUS,
DOMAIN,
EVENT_TRANSLATION_MAP,
BeoModel,
BangOlufsenModel,
WebsocketNotification,
)
from .entity import BeoBase
from .entity import BangOlufsenBase
from .util import get_device, get_remotes
_LOGGER = logging.getLogger(__name__)
class BeoWebsocket(BeoBase):
class BangOlufsenWebsocket(BangOlufsenBase):
"""The WebSocket listeners."""
def __init__(
@@ -48,7 +48,7 @@ class BeoWebsocket(BeoBase):
) -> None:
"""Initialize the WebSocket listeners."""
BeoBase.__init__(self, entry, client)
BangOlufsenBase.__init__(self, entry, client)
self.hass = hass
self._device = get_device(hass, self._unique_id)
@@ -178,7 +178,7 @@ class BeoWebsocket(BeoBase):
self.entry.entry_id
)
if device.serial_number is not None
and device.model == BeoModel.BEOREMOTE_ONE
and device.model == BangOlufsenModel.BEOREMOTE_ONE
]
# Get paired remotes from device
remote_serial_numbers = [
@@ -274,4 +274,4 @@ class BeoWebsocket(BeoBase):
}
_LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)

View File

@@ -174,13 +174,5 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}

View File

@@ -1,8 +1,4 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -321,36 +317,5 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
"title": "Binary sensor"
}

View File

@@ -1,67 +0,0 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domain: str = DOMAIN
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_state = to_state
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS

View File

@@ -1,25 +0,0 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
occupancy_cleared:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy
occupancy_detected:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy

View File

@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==2.0.0",
"bleak==1.0.1",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.1.2",
"habluetooth==5.8.0"
"habluetooth==5.7.0"
]
}

View File

@@ -36,7 +36,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/broadlink",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["broadlink"],
"requirements": ["broadlink==0.19.0"]

View File

@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit
from webrtc_models import RTCIceCandidateInit, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -37,7 +37,6 @@ from homeassistant.components.stream import (
Stream,
create_stream,
)
from homeassistant.components.web_rtc import async_get_ice_servers
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -85,6 +84,7 @@ from .prefs import (
get_dynamic_camera_stream_settings,
)
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
@@ -93,6 +93,7 @@ from .webrtc import (
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
async_register_ice_servers,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -399,6 +400,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
@callback
def get_ice_servers() -> list[RTCIceServer]:
if hass.config.webrtc.ice_servers:
return hass.config.webrtc.ice_servers
return [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:80",
"stun:stun.home-assistant.io:3478",
]
),
]
async_register_ice_servers(hass, get_ice_servers)
return True
@@ -716,7 +731,11 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
ice_servers = async_get_ice_servers(self.hass)
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
return config

View File

@@ -3,7 +3,7 @@
"name": "Camera",
"after_dependencies": ["media_player"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["http", "web_rtc"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
@@ -12,7 +12,12 @@ from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceCandidateInit
from webrtc_models import (
RTCConfiguration,
RTCIceCandidate,
RTCIceCandidateInit,
RTCIceServer,
)
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
@@ -33,6 +38,9 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
_WEBRTC = "WebRTC"
@@ -359,3 +367,21 @@ async def async_get_supported_provider(
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
) -> Callable[[], None]:
"""Register a ICE server.
The registering integration is responsible to implement caching if needed.
"""
servers = hass.data.setdefault(DATA_ICE_SERVERS, [])
def remove() -> None:
servers.remove(get_ice_server_fn)
servers.append(get_ice_server_fn)
return remove

View File

@@ -299,54 +299,54 @@
"title": "Climate",
"triggers": {
"started_cooling": {
"description": "Triggers after one or more climate-control devices start cooling.",
"description": "Triggers when a climate started cooling.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started cooling"
"name": "When a climate started cooling"
},
"started_drying": {
"description": "Triggers after one or more climate-control devices start drying.",
"description": "Triggers when a climate started drying.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started drying"
"name": "When a climate started drying"
},
"started_heating": {
"description": "Triggers after one or more climate-control devices start heating.",
"description": "Triggers when a climate starts to heat.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started heating"
"name": "When a climate starts to heat"
},
"turned_off": {
"description": "Triggers after one or more climate-control devices turn off.",
"description": "Triggers when a climate is turned off.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned off"
"name": "When a climate is turned off"
},
"turned_on": {
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"description": "Triggers when a climate is turned on.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned on"
"name": "When a climate is turned on"
}
}
}

View File

@@ -4,13 +4,12 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from contextlib import suppress
from datetime import datetime, timedelta
from enum import Enum
import logging
from typing import Any, cast
from hass_nabucasa import Cloud, NabuCasaBaseError
from hass_nabucasa import Cloud
import voluptuous as vol
from homeassistant.components import alexa, google_assistant
@@ -79,16 +78,13 @@ from .subscription import async_subscription_info
DEFAULT_MODE = MODE_PROD
PLATFORMS = [
Platform.AI_TASK,
Platform.BINARY_SENSOR,
Platform.CONVERSATION,
Platform.STT,
Platform.TTS,
]
LLM_PLATFORMS = [
Platform.AI_TASK,
Platform.CONVERSATION,
]
SERVICE_REMOTE_CONNECT = "remote_connect"
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
@@ -435,14 +431,7 @@ def _handle_prefs_updated(hass: HomeAssistant, cloud: Cloud[CloudClient]) -> Non
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
platforms = PLATFORMS.copy()
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
with suppress(NabuCasaBaseError):
await cloud.llm.async_ensure_token()
platforms += LLM_PLATFORMS
await hass.config_entries.async_forward_entry_setups(entry, platforms)
entry.runtime_data = {"platforms": platforms}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
stt_tts_entities_added.set()
@@ -451,9 +440,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
entry, entry.runtime_data["platforms"]
)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@callback

View File

@@ -6,6 +6,7 @@ import io
from json import JSONDecodeError
import logging
from hass_nabucasa import NabuCasaBaseError
from hass_nabucasa.llm import (
LLMAuthenticationError,
LLMError,
@@ -19,7 +20,7 @@ from PIL import Image
from homeassistant.components import ai_task, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
@@ -93,11 +94,17 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Home Assistant Cloud AI Task entity."""
cloud = hass.data[DATA_CLOUD]
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
return
try:
await cloud.llm.async_ensure_token()
except (LLMError, NabuCasaBaseError):
return
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
"""Home Assistant Cloud AI Task entity."""
_attr_has_entity_name = True
@@ -174,7 +181,7 @@ class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
attachments=attachments,
)
except LLMAuthenticationError as err:
raise HomeAssistantError("Cloud LLM authentication failed") from err
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:

View File

@@ -19,8 +19,8 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
@@ -71,7 +71,6 @@ class CloudClient(Interface):
self._google_config_init_lock = asyncio.Lock()
self._relayer_region: str | None = None
self._cloud_ice_servers_listener: Callable[[], None] | None = None
self._ice_servers: list[RTCIceServer] = []
@property
def base_path(self) -> Path:
@@ -118,11 +117,6 @@ class CloudClient(Interface):
"""Return the connected relayer region."""
return self._relayer_region
@property
def ice_servers(self) -> list[RTCIceServer]:
"""Return the current ICE servers."""
return self._ice_servers
async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig:
"""Return Alexa config."""
if self._alexa_config is None:
@@ -209,8 +203,11 @@ class CloudClient(Interface):
ice_servers: list[RTCIceServer],
) -> Callable[[], None]:
"""Register cloud ice server."""
self._ice_servers = ice_servers
return async_register_ice_servers(self._hass, lambda: self._ice_servers)
def get_ice_servers() -> list[RTCIceServer]:
return ice_servers
return async_register_ice_servers(self._hass, get_ice_servers)
async def async_register_cloud_ice_servers_listener(
prefs: CloudPreferences,
@@ -271,7 +268,6 @@ class CloudClient(Interface):
async def logout_cleanups(self) -> None:
"""Cleanup some stuff after logout."""
self._ice_servers = []
await self.prefs.async_set_username(None)
if self._alexa_config:

View File

@@ -4,6 +4,9 @@ from __future__ import annotations
from typing import Literal
from hass_nabucasa import NabuCasaBaseError
from hass_nabucasa.llm import LLMError
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
@@ -21,13 +24,19 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Home Assistant Cloud conversation entity."""
cloud = hass.data[DATA_CLOUD]
if not (cloud := hass.data[DATA_CLOUD]).is_logged_in:
return
try:
await cloud.llm.async_ensure_token()
except (LLMError, NabuCasaBaseError):
return
async_add_entities([CloudConversationEntity(cloud, config_entry)])
class CloudConversationEntity(
BaseCloudLLMEntity,
conversation.ConversationEntity,
BaseCloudLLMEntity,
):
"""Home Assistant Cloud conversation agent."""

View File

@@ -8,9 +8,10 @@ import logging
import re
from typing import Any, Literal, cast
from hass_nabucasa import Cloud, NabuCasaBaseError
from hass_nabucasa import Cloud
from hass_nabucasa.llm import (
LLMAuthenticationError,
LLMError,
LLMRateLimitError,
LLMResponseError,
LLMServiceError,
@@ -36,7 +37,7 @@ from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
@@ -561,7 +562,7 @@ class BaseCloudLLMEntity(Entity):
"schema": _format_structured_output(
structure, chat_log.llm_api
),
"strict": False,
"strict": True,
},
}
@@ -600,14 +601,14 @@ class BaseCloudLLMEntity(Entity):
)
except LLMAuthenticationError as err:
raise HomeAssistantError("Cloud LLM authentication failed") from err
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:
raise HomeAssistantError(str(err)) from err
except LLMServiceError as err:
raise HomeAssistantError("Error talking to Cloud LLM") from err
except NabuCasaBaseError as err:
except LLMError as err:
raise HomeAssistantError(str(err)) from err
if not chat_log.unresponded_tool_results:

View File

@@ -99,7 +99,6 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_hook_delete)
websocket_api.async_register_command(hass, websocket_remote_connect)
websocket_api.async_register_command(hass, websocket_remote_disconnect)
websocket_api.async_register_command(hass, websocket_webrtc_ice_servers)
websocket_api.async_register_command(hass, google_assistant_get)
websocket_api.async_register_command(hass, google_assistant_list)
@@ -1108,7 +1107,6 @@ async def alexa_sync(
@websocket_api.websocket_command({"type": "cloud/tts/info"})
@callback
def tts_info(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
@@ -1136,22 +1134,3 @@ def tts_info(
)
connection.send_result(msg["id"], {"languages": result})
@websocket_api.websocket_command(
{
vol.Required("type"): "cloud/webrtc/ice_servers",
}
)
@_require_cloud_login
@callback
def websocket_webrtc_ice_servers(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Handle get WebRTC ICE servers websocket command."""
connection.send_result(
msg["id"],
[server.to_dict() for server in hass.data[DATA_CLOUD].client.ice_servers],
)

View File

@@ -8,11 +8,11 @@
"google_assistant"
],
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
"dependencies": ["auth", "http", "repairs", "webhook"],
"documentation": "https://www.home-assistant.io/integrations/cloud",
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.7.0"],
"requirements": ["hass-nabucasa==1.6.2"],
"single_config_entry": true
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
"requirements": ["hassil==3.4.0", "home-assistant-intents==2025.11.24"]
}

View File

@@ -56,6 +56,7 @@ class DeviceAutomationConditionProtocol(Protocol):
class DeviceCondition(Condition):
"""Device condition."""
_hass: HomeAssistant
_config: ConfigType
@classmethod
@@ -86,7 +87,7 @@ class DeviceCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config)
self._hass = hass
assert config.options is not None
self._config = config.options

View File

@@ -6,7 +6,6 @@
"config_flow": true,
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],

View File

@@ -6,7 +6,6 @@
"config_flow": true,
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.46.0"],
"ssdp": [

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.6.0"]
"requirements": ["aiodns==3.5.0"]
}

View File

@@ -102,12 +102,6 @@ class ConfiguredDoorBird:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
@@ -152,7 +146,13 @@ class ConfiguredDoorBird:
async def _async_register_events(self) -> dict[str, Any]:
"""Register events on device."""
hass_url = self._get_hass_url()
# Override url if another is specified in the configuration
if custom_url := self.custom_url:
hass_url = custom_url
else:
# Get the URL of this server
hass_url = get_url(self._hass, prefer_external=False)
http_fav = await self._async_get_http_favorites()
if any(
# Note that a list comp is used here to ensure all
@@ -191,14 +191,10 @@ class ConfiguredDoorBird:
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))

View File

@@ -2,22 +2,33 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine, Sequence
from datetime import datetime, timedelta
import logging
from typing import Any, cast
from aiohttp import ClientSession
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import (
CALLBACK_TYPE,
HassJob,
HomeAssistant,
ServiceCall,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from .const import ATTR_CONFIG_ENTRY
from .coordinator import DuckDnsConfigEntry, DuckDnsUpdateCoordinator
from .helpers import update_duckdns
_LOGGER = logging.getLogger(__name__)
@@ -25,8 +36,17 @@ ATTR_TXT = "txt"
DOMAIN = "duckdns"
INTERVAL = timedelta(minutes=5)
BACKOFF_INTERVALS = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
SERVICE_SET_TXT = "set_txt"
UPDATE_URL = "https://www.duckdns.org/update"
CONFIG_SCHEMA = vol.Schema(
{
@@ -51,6 +71,8 @@ SERVICE_TXT_SCHEMA = vol.Schema(
}
)
type DuckDnsConfigEntry = ConfigEntry
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Initialize the DuckDNS component."""
@@ -77,12 +99,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Set up Duck DNS from a config entry."""
coordinator = DuckDnsUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
session = async_get_clientsession(hass)
# Add a dummy listener as we do not have regular entities
entry.async_on_unload(coordinator.async_add_listener(lambda: None))
async def update_domain_interval(_now: datetime) -> bool:
"""Update the DuckDNS entry."""
return await _update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
)
entry.async_on_unload(
async_track_time_interval_backoff(
hass, update_domain_interval, BACKOFF_INTERVALS
)
)
return True
@@ -122,7 +153,7 @@ async def update_domain_service(call: ServiceCall) -> None:
session = async_get_clientsession(call.hass)
await update_duckdns(
await _update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
@@ -133,3 +164,73 @@ async def update_domain_service(call: ServiceCall) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Unload a config entry."""
return True
_SENTINEL = object()
async def _update_duckdns(
session: ClientSession,
domain: str,
token: str,
*,
txt: str | None | object = _SENTINEL,
clear: bool = False,
) -> bool:
"""Update DuckDNS."""
params = {"domains": domain, "token": token}
if txt is not _SENTINEL:
if txt is None:
# Pass in empty txt value to indicate it's clearing txt record
params["txt"] = ""
clear = True
else:
params["txt"] = cast(str, txt)
if clear:
params["clear"] = "true"
resp = await session.get(UPDATE_URL, params=params)
body = await resp.text()
if body != "OK":
_LOGGER.warning("Updating DuckDNS domain failed: %s", domain)
return False
return True
@callback
@bind_hass
def async_track_time_interval_backoff(
hass: HomeAssistant,
action: Callable[[datetime], Coroutine[Any, Any, bool]],
intervals: Sequence[timedelta],
) -> CALLBACK_TYPE:
"""Add a listener that fires repetitively at every timedelta interval."""
remove: CALLBACK_TYPE | None = None
failed = 0
async def interval_listener(now: datetime) -> None:
"""Handle elapsed intervals with backoff."""
nonlocal failed, remove
try:
failed += 1
if await action(now):
failed = 0
finally:
delay = intervals[failed] if failed < len(intervals) else intervals[-1]
remove = async_call_later(
hass, delay.total_seconds(), interval_listener_job
)
interval_listener_job = HassJob(interval_listener, cancel_on_shutdown=True)
hass.async_run_hass_job(interval_listener_job, dt_util.utcnow())
def remove_listener() -> None:
"""Remove interval listener."""
if remove:
remove()
return remove_listener

View File

@@ -8,7 +8,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN, CONF_NAME
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
@@ -16,8 +16,8 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
from . import _update_duckdns
from .const import DOMAIN
from .helpers import update_duckdns
from .issue import deprecate_yaml_issue
_LOGGER = logging.getLogger(__name__)
@@ -31,8 +31,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Duck DNS."""
@@ -46,7 +44,7 @@ class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_DOMAIN: user_input[CONF_DOMAIN]})
session = async_get_clientsession(self.hass)
try:
if not await update_duckdns(
if not await _update_duckdns(
session,
user_input[CONF_DOMAIN],
user_input[CONF_ACCESS_TOKEN],
@@ -81,37 +79,3 @@ class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
deprecate_yaml_issue(self.hass, import_success=True)
return result
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfigure flow."""
errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()
if user_input is not None:
session = async_get_clientsession(self.hass)
try:
if not await update_duckdns(
session,
entry.data[CONF_DOMAIN],
user_input[CONF_ACCESS_TOKEN],
):
errors["base"] = "update_failed"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
return self.async_update_reload_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_NAME: entry.title},
)

View File

@@ -1,83 +0,0 @@
"""Coordinator for the Duck DNS integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aiohttp import ClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .helpers import update_duckdns
_LOGGER = logging.getLogger(__name__)
type DuckDnsConfigEntry = ConfigEntry[DuckDnsUpdateCoordinator]
INTERVAL = timedelta(minutes=5)
BACKOFF_INTERVALS = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
class DuckDnsUpdateCoordinator(DataUpdateCoordinator[None]):
"""Duck DNS update coordinator."""
config_entry: DuckDnsConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: DuckDnsConfigEntry) -> None:
"""Initialize the Duck DNS update coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=INTERVAL,
)
self.session = async_get_clientsession(hass)
self.failed = 0
async def _async_update_data(self) -> None:
"""Update Duck DNS."""
retry_after = BACKOFF_INTERVALS[
min(self.failed, len(BACKOFF_INTERVALS))
].total_seconds()
try:
if not await update_duckdns(
self.session,
self.config_entry.data[CONF_DOMAIN],
self.config_entry.data[CONF_ACCESS_TOKEN],
):
self.failed += 1
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={
CONF_DOMAIN: self.config_entry.data[CONF_DOMAIN],
},
retry_after=retry_after,
)
except ClientError as e:
self.failed += 1
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={
CONF_DOMAIN: self.config_entry.data[CONF_DOMAIN],
},
retry_after=retry_after,
) from e
self.failed = 0

View File

@@ -1,35 +0,0 @@
"""Helpers for Duck DNS integration."""
from aiohttp import ClientSession
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
UPDATE_URL = "https://www.duckdns.org/update"
async def update_duckdns(
session: ClientSession,
domain: str,
token: str,
*,
txt: str | None | UndefinedType = UNDEFINED,
clear: bool = False,
) -> bool:
"""Update DuckDNS."""
params = {"domains": domain, "token": token}
if txt is not UNDEFINED:
if txt is None:
# Pass in empty txt value to indicate it's clearing txt record
params["txt"] = ""
clear = True
else:
params["txt"] = txt
if clear:
params["clear"] = "true"
resp = await session.get(UPDATE_URL, params=params)
body = await resp.text()
return body == "OK"

View File

@@ -1,23 +1,13 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]",
"update_failed": "Updating Duck DNS failed"
},
"step": {
"reconfigure": {
"data": {
"access_token": "[%key:component::duckdns::config::step::user::data::access_token%]"
},
"data_description": {
"access_token": "[%key:component::duckdns::config::step::user::data_description::access_token%]"
},
"title": "Re-configure {name}"
},
"user": {
"data": {
"access_token": "Token",
@@ -32,17 +22,11 @@
}
},
"exceptions": {
"connection_error": {
"message": "Updating Duck DNS domain {domain} failed due to a connection error"
},
"entry_not_found": {
"message": "Duck DNS integration entry not found"
},
"entry_not_selected": {
"message": "Duck DNS integration entry not selected"
},
"update_failed": {
"message": "Updating Duck DNS domain {domain} failed"
}
},
"issues": {

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==16.4.0"]
}

View File

@@ -17,7 +17,7 @@ DEFAULT_TTS_MODEL = "eleven_multilingual_v2"
DEFAULT_STABILITY = 0.5
DEFAULT_SIMILARITY = 0.75
DEFAULT_STT_AUTO_LANGUAGE = False
DEFAULT_STT_MODEL = "scribe_v2"
DEFAULT_STT_MODEL = "scribe_v1"
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
@@ -129,5 +129,4 @@ STT_LANGUAGES = [
STT_MODELS = {
"scribe_v1": "Scribe v1",
"scribe_v1_experimental": "Scribe v1 Experimental",
"scribe_v2": "Scribe v2 Realtime",
}

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.6"]
"requirements": ["evohome-async==1.0.5"]
}

View File

@@ -166,24 +166,24 @@
"title": "Fan",
"triggers": {
"turned_off": {
"description": "Triggers after one or more fans turn off.",
"description": "Triggers when a fan is turned off.",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",
"name": "[%key:component::fan::common::trigger_behavior_name%]"
}
},
"name": "Fan turned off"
"name": "When a fan is turned off"
},
"turned_on": {
"description": "Triggers after one or more fans turn on.",
"description": "Triggers when a fan is turned on.",
"fields": {
"behavior": {
"description": "[%key:component::fan::common::trigger_behavior_description%]",
"name": "[%key:component::fan::common::trigger_behavior_name%]"
}
},
"name": "Fan turned on"
"name": "When a fan is turned on"
}
}
}

View File

@@ -12,13 +12,7 @@ from .coordinator import (
FressnapfTrackerDataUpdateCoordinator,
)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
]
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]
async def async_setup_entry(

View File

@@ -1,65 +0,0 @@
"""Binary Sensor platform for fressnapf_tracker."""
from collections.abc import Callable
from dataclasses import dataclass
from fressnapftracker import Tracker
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class FressnapfTrackerBinarySensorDescription(BinarySensorEntityDescription):
"""Class describing Fressnapf Tracker binary_sensor entities."""
value_fn: Callable[[Tracker], bool]
BINARY_SENSOR_ENTITY_DESCRIPTIONS: tuple[
FressnapfTrackerBinarySensorDescription, ...
] = (
FressnapfTrackerBinarySensorDescription(
key="charging",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.charging,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker binary_sensors."""
async_add_entities(
FressnapfTrackerBinarySensor(coordinator, sensor_description)
for sensor_description in BINARY_SENSOR_ENTITY_DESCRIPTIONS
for coordinator in entry.runtime_data
)
class FressnapfTrackerBinarySensor(FressnapfTrackerEntity, BinarySensorEntity):
"""Fressnapf Tracker binary_sensor for general information."""
entity_description: FressnapfTrackerBinarySensorDescription
@property
def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -8,9 +8,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry, FressnapfTrackerDataUpdateCoordinator
from .entity import FressnapfTrackerBaseEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
@@ -42,11 +39,6 @@ class FressnapfTrackerDeviceTracker(FressnapfTrackerBaseEntity, TrackerEntity):
"""Return if entity is available."""
return super().available and self.coordinator.data.position is not None
@property
def entity_picture(self) -> str | None:
"""Return the entity picture url."""
return self.coordinator.data.icon
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""

View File

@@ -1,7 +1,6 @@
"""fressnapf_tracker class."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import FressnapfTrackerDataUpdateCoordinator
@@ -26,17 +25,3 @@ class FressnapfTrackerBaseEntity(
manufacturer="Fressnapf",
serial_number=str(self.id),
)
class FressnapfTrackerEntity(FressnapfTrackerBaseEntity):
"""Entity for fressnapf_tracker."""
def __init__(
self,
coordinator: FressnapfTrackerDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{self.id}_{entity_description.key}"

View File

@@ -4,14 +4,6 @@
"pet": {
"default": "mdi:paw"
}
},
"switch": {
"energy_saving": {
"default": "mdi:sleep",
"state": {
"off": "mdi:sleep-off"
}
}
}
}
}

View File

@@ -1,95 +0,0 @@
"""Light platform for fressnapf_tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ColorMode,
LightEntity,
LightEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .const import DOMAIN
from .entity import FressnapfTrackerEntity
PARALLEL_UPDATES = 1
LIGHT_ENTITY_DESCRIPTION = LightEntityDescription(
translation_key="led",
entity_category=EntityCategory.CONFIG,
key="led_brightness_value",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker lights."""
async_add_entities(
FressnapfTrackerLight(coordinator, LIGHT_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.led_activatable is not None
and coordinator.data.led_activatable.has_led
and coordinator.data.tracker_settings.features.flash_light
)
class FressnapfTrackerLight(FressnapfTrackerEntity, LightEntity):
"""Fressnapf Tracker light."""
_attr_color_mode: ColorMode = ColorMode.BRIGHTNESS
_attr_supported_color_modes: set[ColorMode] = {ColorMode.BRIGHTNESS}
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
if TYPE_CHECKING:
# The entity is not created if led_brightness_value is None
assert self.coordinator.data.led_brightness_value is not None
return int(round((self.coordinator.data.led_brightness_value / 100) * 255))
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
self.raise_if_not_activatable()
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
brightness = int((brightness / 255) * 100)
await self.coordinator.client.set_led_brightness(brightness)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_led_brightness(0)
await self.coordinator.async_request_refresh()
def raise_if_not_activatable(self) -> None:
"""Raise error with reasoning if light is not activatable."""
if TYPE_CHECKING:
# The entity is not created if led_activatable is None
assert self.coordinator.data.led_activatable is not None
error_type: str | None = None
if not self.coordinator.data.led_activatable.seen_recently:
error_type = "not_seen_recently"
elif not self.coordinator.data.led_activatable.not_charging:
error_type = "charging"
elif not self.coordinator.data.led_activatable.nonempty_battery:
error_type = "low_battery"
if error_type is not None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=error_type,
)
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if self.coordinator.data.led_brightness_value is not None:
return self.coordinator.data.led_brightness_value > 0
return False

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["fressnapftracker==0.2.0"]
"requirements": ["fressnapftracker==0.1.2"]
}

View File

@@ -28,26 +28,20 @@ rules:
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration is a cloud service and thus does not support discovery.
discovery:
status: exempt
comment: |
This integration is a cloud service and thus does not support discovery.
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
@@ -56,15 +50,14 @@ rules:
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations:
status: exempt
comment: |
This integration does not have many entities. All of them are fundamental.
entity-translations: done
exception-translations: done
icon-translations: done
comment: No entities to translate
exception-translations: todo
icon-translations: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo

View File

@@ -1,66 +0,0 @@
"""Sensor platform for fressnapf_tracker."""
from collections.abc import Callable
from dataclasses import dataclass
from fressnapftracker import Tracker
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class FressnapfTrackerSensorDescription(SensorEntityDescription):
"""Class describing Fressnapf Tracker sensor entities."""
value_fn: Callable[[Tracker], int]
SENSOR_ENTITY_DESCRIPTIONS: tuple[FressnapfTrackerSensorDescription, ...] = (
FressnapfTrackerSensorDescription(
key="battery",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.battery,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker sensors."""
async_add_entities(
FressnapfTrackerSensor(coordinator, sensor_description)
for sensor_description in SENSOR_ENTITY_DESCRIPTIONS
for coordinator in entry.runtime_data
)
class FressnapfTrackerSensor(FressnapfTrackerEntity, SensorEntity):
"""fressnapf_tracker sensor for general information."""
entity_description: FressnapfTrackerSensorDescription
@property
def native_value(self) -> int:
"""Return the state of the resources if it has been received yet."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -45,28 +45,5 @@
}
}
}
},
"entity": {
"light": {
"led": {
"name": "Flashlight"
}
},
"switch": {
"energy_saving": {
"name": "Sleep mode"
}
}
},
"exceptions": {
"charging": {
"message": "The flashlight cannot be activated while charging."
},
"low_battery": {
"message": "The flashlight cannot be activated due to low battery."
},
"not_seen_recently": {
"message": "The flashlight cannot be activated when the tracker has not moved recently."
}
}
}

View File

@@ -1,60 +0,0 @@
"""Switch platform for Fressnapf Tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
PARALLEL_UPDATES = 1
SWITCH_ENTITY_DESCRIPTION = SwitchEntityDescription(
translation_key="energy_saving",
entity_category=EntityCategory.CONFIG,
device_class=SwitchDeviceClass.SWITCH,
key="energy_saving",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker switches."""
async_add_entities(
FressnapfTrackerSwitch(coordinator, SWITCH_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.tracker_settings.features.energy_saving_mode
)
class FressnapfTrackerSwitch(FressnapfTrackerEntity, SwitchEntity):
"""Fressnapf Tracker switch."""
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
await self.coordinator.client.set_energy_saving(True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_energy_saving(False)
await self.coordinator.async_request_refresh()
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if TYPE_CHECKING:
# The entity is not created if energy_saving is None
assert self.coordinator.data.energy_saving is not None
return self.coordinator.data.energy_saving.value == 1

View File

@@ -9,7 +9,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/fronius",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.1"]
"requirements": ["home-assistant-frontend==20251201.0"]
}

View File

@@ -1,58 +0,0 @@
"""The homelink integration."""
from __future__ import annotations
from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
from .const import DOMAIN
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator, HomeLinkData
PLATFORMS: list[Platform] = [Platform.EVENT]
async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Set up homelink from a config entry."""
auth_implementation = oauth2.SRPAuthImplementation(hass, DOMAIN)
config_entry_oauth2_flow.async_register_implementation(
hass, DOMAIN, auth_implementation
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
provider = MQTTProvider(authenticated_session)
coordinator = HomeLinkCoordinator(hass, provider, entry)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, coordinator.async_on_unload
)
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = HomeLinkData(
provider=provider, coordinator=coordinator, last_update_id=None
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.coordinator.async_on_unload(None)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,14 +0,0 @@
"""application_credentials platform for the gentex homelink integration."""
from homeassistant.components.application_credentials import ClientCredential
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from . import oauth2
async def async_get_auth_implementation(
hass: HomeAssistant, auth_domain: str, _credential: ClientCredential
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
"""Return custom SRPAuth implementation."""
return oauth2.SRPAuthImplementation(hass, auth_domain)

View File

@@ -1,66 +0,0 @@
"""Config flow for homelink."""
import logging
from typing import Any
import botocore.exceptions
from homelink.auth.srp_auth import SRPAuth
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DOMAIN
from .oauth2 import SRPAuthImplementation
_LOGGER = logging.getLogger(__name__)
class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle homelink OAuth2 authentication."""
DOMAIN = DOMAIN
def __init__(self) -> None:
"""Set up the flow handler."""
super().__init__()
self.flow_impl = SRPAuthImplementation(self.hass, DOMAIN)
@property
def logger(self):
"""Get the logger."""
return _LOGGER
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Ask for username and password."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
srp_auth = SRPAuth()
try:
tokens = await self.hass.async_add_executor_job(
srp_auth.async_get_access_token,
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
)
except botocore.exceptions.ClientError:
_LOGGER.exception("Error authenticating homelink account")
errors["base"] = "srp_auth_failed"
except Exception:
_LOGGER.exception("An unexpected error occurred")
errors["base"] = "unknown"
else:
self.external_data = {"tokens": tokens}
return await self.async_step_creation()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
),
errors=errors,
)

View File

@@ -1,7 +0,0 @@
"""Constants for the homelink integration."""
DOMAIN = "gentex_homelink"
OAUTH2_TOKEN = "https://auth.homelinkcloud.com/oauth2/token"
POLLING_INTERVAL = 5
EVENT_PRESSED = "Pressed"

View File

@@ -1,113 +0,0 @@
"""Makes requests to the state server and stores the resulting data so that the buttons can access it."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
import logging
from typing import TYPE_CHECKING, TypedDict
from homelink.model.device import Device
from homelink.mqtt_provider import MQTTProvider
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.ssl import get_default_context
if TYPE_CHECKING:
from .event import HomeLinkEventEntity
_LOGGER = logging.getLogger(__name__)
type HomeLinkConfigEntry = ConfigEntry[HomeLinkData]
type EventCallback = Callable[[HomeLinkEventData], None]
@dataclass
class HomeLinkData:
"""Class for HomeLink integration runtime data."""
provider: MQTTProvider
coordinator: HomeLinkCoordinator
last_update_id: str | None
class HomeLinkEventData(TypedDict):
"""Data for a single event."""
requestId: str
timestamp: int
class HomeLinkMQTTMessage(TypedDict):
"""HomeLink MQTT Event message."""
type: str
data: dict[str, HomeLinkEventData] # Each key is a button id
class HomeLinkCoordinator:
"""HomeLink integration coordinator."""
def __init__(
self,
hass: HomeAssistant,
provider: MQTTProvider,
config_entry: HomeLinkConfigEntry,
) -> None:
"""Initialize my coordinator."""
self.hass = hass
self.config_entry = config_entry
self.provider = provider
self.device_data: list[Device] = []
self.buttons: list[HomeLinkEventEntity] = []
self._listeners: dict[str, EventCallback] = {}
@callback
def async_add_event_listener(
self, update_callback: EventCallback, target_event_id: str
) -> Callable[[], None]:
"""Listen for updates."""
self._listeners[target_event_id] = update_callback
return partial(self.__async_remove_listener_internal, target_event_id)
def __async_remove_listener_internal(self, listener_id: str):
del self._listeners[listener_id]
@callback
def async_handle_state_data(self, data: dict[str, HomeLinkEventData]):
"""Notify listeners."""
for button_id, event in data.items():
if listener := self._listeners.get(button_id):
listener(event)
async def async_config_entry_first_refresh(self) -> None:
"""Refresh data for the first time when a config entry is setup."""
await self._async_setup()
async def async_on_unload(self, _event):
"""Disconnect and unregister when unloaded."""
await self.provider.disable()
async def _async_setup(self) -> None:
"""Set up the coordinator."""
await self.provider.enable(get_default_context())
await self.discover_devices()
self.provider.listen(self.on_message)
async def discover_devices(self):
"""Discover devices and build the Entities."""
self.device_data = await self.provider.discover()
def on_message(
self: HomeLinkCoordinator, _topic: str, message: HomeLinkMQTTMessage
):
"MQTT Callback function."
if message["type"] == "state":
self.hass.add_job(self.async_handle_state_data, message["data"])
if message["type"] == "requestSync":
self.hass.add_job(
self.hass.config_entries.async_reload,
self.config_entry.entry_id,
)

View File

@@ -1,83 +0,0 @@
"""Platform for Event integration."""
from __future__ import annotations
from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, EVENT_PRESSED
from .coordinator import HomeLinkCoordinator, HomeLinkEventData
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the entities for the binary sensor."""
coordinator = config_entry.runtime_data.coordinator
for device in coordinator.device_data:
buttons = [
HomeLinkEventEntity(b.id, b.name, device.id, device.name, coordinator)
for b in device.buttons
]
coordinator.buttons.extend(buttons)
async_add_entities(coordinator.buttons)
# Updates are centralized by the coordinator.
PARALLEL_UPDATES = 0
class HomeLinkEventEntity(EventEntity):
"""Event Entity."""
_attr_has_entity_name = True
_attr_event_types = [EVENT_PRESSED]
_attr_device_class = EventDeviceClass.BUTTON
def __init__(
self,
id: str,
param_name: str,
device_id: str,
device_name: str,
coordinator: HomeLinkCoordinator,
) -> None:
"""Initialize the event entity."""
self.id: str = id
self._attr_name: str = param_name
self._attr_unique_id: str = id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=device_name,
)
self.coordinator = coordinator
self.last_request_id: str | None = None
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_add_event_listener(
self._handle_event_data_update, self.id
)
)
@callback
def _handle_event_data_update(self, update_data: HomeLinkEventData) -> None:
"""Update this button."""
if update_data["requestId"] != self.last_request_id:
self._trigger_event(EVENT_PRESSED)
self.last_request_id = update_data["requestId"]
self.async_write_ha_state()
async def async_update(self):
"""Request early polling. Left intentionally blank because it's not possible in this implementation."""

View File

@@ -1,11 +0,0 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.1"]
}

View File

@@ -1,114 +0,0 @@
"""API for homelink bound to Home Assistant OAuth."""
from json import JSONDecodeError
import logging
import time
from typing import cast
from aiohttp import ClientError, ClientSession
from homelink.auth.abstract_auth import AbstractAuth
from homelink.settings import COGNITO_CLIENT_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import OAUTH2_TOKEN
_LOGGER = logging.getLogger(__name__)
class SRPAuthImplementation(config_entry_oauth2_flow.AbstractOAuth2Implementation):
"""Base class to abstract OAuth2 authentication."""
def __init__(self, hass: HomeAssistant, domain) -> None:
"""Initialize the SRP Auth implementation."""
self.hass = hass
self._domain = domain
self.client_id = COGNITO_CLIENT_ID
@property
def name(self) -> str:
"""Name of the implementation."""
return "SRPAuth"
@property
def domain(self) -> str:
"""Domain that is providing the implementation."""
return self._domain
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Left intentionally blank because the auth is handled by SRP."""
return ""
async def async_resolve_external_data(self, external_data) -> dict:
"""Format the token from the source appropriately for HomeAssistant."""
tokens = external_data["tokens"]
new_token = {}
new_token["access_token"] = tokens["AuthenticationResult"]["AccessToken"]
new_token["refresh_token"] = tokens["AuthenticationResult"]["RefreshToken"]
new_token["token_type"] = tokens["AuthenticationResult"]["TokenType"]
new_token["expires_in"] = tokens["AuthenticationResult"]["ExpiresIn"]
new_token["expires_at"] = (
time.time() + tokens["AuthenticationResult"]["ExpiresIn"]
)
return new_token
async def _token_request(self, data: dict) -> dict:
"""Make a token request."""
session = async_get_clientsession(self.hass)
data["client_id"] = self.client_id
_LOGGER.debug("Sending token request to %s", OAUTH2_TOKEN)
resp = await session.post(OAUTH2_TOKEN, data=data)
if resp.status >= 400:
try:
error_response = await resp.json()
except (ClientError, JSONDecodeError):
error_response = {}
error_code = error_response.get("error", "unknown")
error_description = error_response.get(
"error_description", "unknown error"
)
_LOGGER.error(
"Token request for %s failed (%s): %s",
self.domain,
error_code,
error_description,
)
resp.raise_for_status()
return cast(dict, await resp.json())
async def _async_refresh_token(self, token: dict) -> dict:
"""Refresh tokens."""
new_token = await self._token_request(
{
"grant_type": "refresh_token",
"client_id": self.client_id,
"refresh_token": token["refresh_token"],
}
)
return {**token, **new_token}
class AsyncConfigEntryAuth(AbstractAuth):
"""Provide homelink authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize homelink auth."""
super().__init__(websession)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]

View File

@@ -1,76 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register any service actions
appropriate-polling:
status: exempt
comment: Integration does not poll
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register any service actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register any service actions
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: It is not necessary to update IP addresses of devices or services in this Integration
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: done
entity-category: todo
entity-device-class: todo
entity-disabled-by-default:
status: exempt
comment: Entities are not noisy and are expected to be enabled by default
entity-translations:
status: exempt
comment: Entity properties are user-defined, and therefore cannot be translated
exception-translations: todo
icon-translations:
status: exempt
comment: Entities in this integration do not use icons, and therefore do not require translation
reconfiguration-flow: todo
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -1,38 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"error": {
"srp_auth_failed": "Error authenticating HomeLink account",
"unknown": "An unknown error occurred. Please try again later"
},
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Email address associated with your HomeLink account",
"password": "Password associated with your HomeLink account"
}
}
}
}
}

View File

@@ -6,7 +6,6 @@ from dataclasses import dataclass
import logging
from secrets import token_hex
import shutil
from tempfile import mkdtemp
from aiohttp import BasicAuth, ClientSession, UnixConnector
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
@@ -63,11 +62,11 @@ from .const import (
CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE,
DOMAIN,
HA_MANAGED_UNIX_SOCKET,
HA_MANAGED_URL,
RECOMMENDED_VERSION,
)
from .server import Server
from .util import get_go2rtc_unix_socket_path
_LOGGER = logging.getLogger(__name__)
@@ -155,12 +154,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
auth = BasicAuth(username, password)
# HA will manage the binary
temp_dir = mkdtemp(prefix="go2rtc-")
# Manually created session (not using the helper) needs to be closed manually
# See on_stop listener below
session = ClientSession(
connector=UnixConnector(path=get_go2rtc_unix_socket_path(temp_dir)),
auth=auth,
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth
)
server = Server(
hass,
@@ -169,7 +166,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
username=username,
password=password,
working_dir=temp_dir,
)
try:
await server.start()

View File

@@ -6,6 +6,7 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock"
# When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA)
# in script/hassfest/docker.py.
RECOMMENDED_VERSION = "1.9.12"

View File

@@ -12,13 +12,13 @@ from go2rtc_client import Go2RtcRestClient
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
from .util import get_go2rtc_unix_socket_path
from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL
_LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5
_SETUP_TIMEOUT = 30
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
_LOCALHOST_IP = "127.0.0.1"
_LOG_BUFFER_SIZE = 512
_RESPAWN_COOLDOWN = 1
@@ -122,9 +122,7 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
return f"[{formatted_items}]"
def _create_temp_file(
enable_ui: bool, username: str, password: str, working_dir: str
) -> str:
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
"""Create temporary config file."""
app_modules: tuple[str, ...] = _APP_MODULES
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
@@ -141,13 +139,11 @@ def _create_temp_file(
# Set delete=False to prevent the file from being deleted when the file is closed
# Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(
prefix="go2rtc_", suffix=".yaml", dir=working_dir, delete=False
) as file:
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write(
_GO2RTC_CONFIG_FORMAT.format(
listen_config=listen_config,
unix_socket=get_go2rtc_unix_socket_path(working_dir),
unix_socket=HA_MANAGED_UNIX_SOCKET,
app_modules=_format_list_for_yaml(app_modules),
api_allow_paths=_format_list_for_yaml(api_paths),
username=username,
@@ -169,7 +165,6 @@ class Server:
enable_ui: bool = False,
username: str,
password: str,
working_dir: str,
) -> None:
"""Initialize the server."""
self._hass = hass
@@ -178,7 +173,6 @@ class Server:
self._enable_ui = enable_ui
self._username = username
self._password = password
self._working_dir = working_dir
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event()
@@ -196,11 +190,7 @@ class Server:
"""Start the server."""
_LOGGER.debug("Starting go2rtc server")
config_file = await self._hass.async_add_executor_job(
_create_temp_file,
self._enable_ui,
self._username,
self._password,
self._working_dir,
_create_temp_file, self._enable_ui, self._username, self._password
)
self._startup_complete.clear()

View File

@@ -1,12 +0,0 @@
"""Go2rtc utility functions."""
from pathlib import Path
_HA_MANAGED_UNIX_SOCKET_FILE = "go2rtc.sock"
def get_go2rtc_unix_socket_path(path: str | Path) -> str:
"""Get the Go2rtc unix socket path."""
if not isinstance(path, Path):
path = Path(path)
return str(path / _HA_MANAGED_UNIX_SOCKET_FILE)

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/google",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]

View File

@@ -59,14 +59,9 @@
"user": "Add location"
},
"step": {
"location": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"location": "[%key:component::google_air_quality::config::step::user::data_description::location%]",
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
"location": "[%key:common::config_flow::data::location%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"title": "Air quality data location"

View File

@@ -149,7 +149,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
),
supports_response=SupportsResponse.ONLY,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)
return True

View File

@@ -23,7 +23,7 @@ CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image-preview"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_TOP_P = "top_p"

View File

@@ -162,7 +162,7 @@
"fields": {
"filenames": {
"description": "Attachments to add to the prompt (images, PDFs, etc)",
"example": "{example_image_path}",
"example": "/config/www/image.jpg",
"name": "Attachment filenames"
},
"prompt": {

View File

@@ -159,5 +159,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -92,7 +92,7 @@
},
"filename": {
"description": "Path to the image or video to upload.",
"example": "{example_image_path}",
"example": "/config/www/image.jpg",
"name": "Filename"
}
},

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/google_translate",
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["gtts"],
"requirements": ["gTTS==2.5.3"]

View File

@@ -18,12 +18,10 @@ from homeassistant.components.notify import (
SERVICE_SEND_MESSAGE,
BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_ACTION,
CONF_ENTITIES,
CONF_SERVICE,
@@ -175,23 +173,14 @@ class NotifyGroup(GroupEntity, NotifyEntity):
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to all members of the group."""
data = {
ATTR_MESSAGE: message,
ATTR_ENTITY_ID: self._entity_ids,
}
# add title only if supported and provided
if (
title is not None
and self._attr_supported_features & NotifyEntityFeature.TITLE
):
data[ATTR_TITLE] = title
await self.hass.services.async_call(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
data,
{
ATTR_MESSAGE: message,
ATTR_TITLE: title,
ATTR_ENTITY_ID: self._entity_ids,
},
blocking=True,
context=self._context,
)
@@ -205,15 +194,3 @@ class NotifyGroup(GroupEntity, NotifyEntity):
for entity_id in self._entity_ids
if (state := self.hass.states.get(entity_id)) is not None
)
# Support title if all members support it
self._attr_supported_features |= NotifyEntityFeature.TITLE
for entity_id in self._entity_ids:
state = self.hass.states.get(entity_id)
if (
state is None
or not state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& NotifyEntityFeature.TITLE
):
self._attr_supported_features &= ~NotifyEntityFeature.TITLE
break

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