mirror of
				https://github.com/home-assistant/core.git
				synced 2025-10-31 14:39:27 +00:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			epenet-pat
			...
			block_pyse
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3a79fb273e | ||
|   | 162c27b92c | ||
|   | e7e42dc318 | ||
|   | 9aa288ed44 | ||
|   | 5aacb6e1b8 | ||
|   | 1428ce4084 | ||
|   | dba07ac90d | ||
|   | 264df97069 | ||
|   | c3f493394a | ||
|   | 7e3e82746f | ||
|   | 33724240d7 | ||
|   | 998a4eab9e | ||
|   | 9985262a53 | 
							
								
								
									
										2
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							| @@ -326,7 +326,7 @@ jobs: | |||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|  |  | ||||||
|       - name: Install Cosign |       - name: Install Cosign | ||||||
|         uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 |         uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 | ||||||
|         with: |         with: | ||||||
|           cosign-release: "v2.2.3" |           cosign-release: "v2.2.3" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								CODEOWNERS
									
									
									
										generated
									
									
									
								
							| @@ -619,8 +619,6 @@ build.json @home-assistant/supervisor | |||||||
| /tests/components/greeneye_monitor/ @jkeljo | /tests/components/greeneye_monitor/ @jkeljo | ||||||
| /homeassistant/components/group/ @home-assistant/core | /homeassistant/components/group/ @home-assistant/core | ||||||
| /tests/components/group/ @home-assistant/core | /tests/components/group/ @home-assistant/core | ||||||
| /homeassistant/components/growatt_server/ @johanzander |  | ||||||
| /tests/components/growatt_server/ @johanzander |  | ||||||
| /homeassistant/components/guardian/ @bachya | /homeassistant/components/guardian/ @bachya | ||||||
| /tests/components/guardian/ @bachya | /tests/components/guardian/ @bachya | ||||||
| /homeassistant/components/habitica/ @tr4nt0r | /homeassistant/components/habitica/ @tr4nt0r | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								build.yaml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								build.yaml
									
									
									
									
									
								
							| @@ -1,10 +1,10 @@ | |||||||
| image: ghcr.io/home-assistant/{arch}-homeassistant | image: ghcr.io/home-assistant/{arch}-homeassistant | ||||||
| build_from: | build_from: | ||||||
|   aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1 |   aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0 | ||||||
|   armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1 |   armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0 | ||||||
|   armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1 |   armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0 | ||||||
|   amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1 |   amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0 | ||||||
|   i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1 |   i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0 | ||||||
| codenotary: | codenotary: | ||||||
|   signer: notary@home-assistant.io |   signer: notary@home-assistant.io | ||||||
|   base_image: notary@home-assistant.io |   base_image: notary@home-assistant.io | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ | |||||||
|         }, |         }, | ||||||
|         "data_description": { |         "data_description": { | ||||||
|           "return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)", |           "return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)", | ||||||
|           "clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0" |           "clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -41,8 +41,6 @@ from .pipeline import ( | |||||||
|     async_setup_pipeline_store, |     async_setup_pipeline_store, | ||||||
|     async_update_pipeline, |     async_update_pipeline, | ||||||
| ) | ) | ||||||
| from .select import AssistPipelineSelect, VadSensitivitySelect |  | ||||||
| from .vad import VadSensitivity |  | ||||||
| from .websocket_api import async_register_websocket_api | from .websocket_api import async_register_websocket_api | ||||||
|  |  | ||||||
| __all__ = ( | __all__ = ( | ||||||
| @@ -53,14 +51,11 @@ __all__ = ( | |||||||
|     "SAMPLE_CHANNELS", |     "SAMPLE_CHANNELS", | ||||||
|     "SAMPLE_RATE", |     "SAMPLE_RATE", | ||||||
|     "SAMPLE_WIDTH", |     "SAMPLE_WIDTH", | ||||||
|     "AssistPipelineSelect", |  | ||||||
|     "AudioSettings", |     "AudioSettings", | ||||||
|     "Pipeline", |     "Pipeline", | ||||||
|     "PipelineEvent", |     "PipelineEvent", | ||||||
|     "PipelineEventType", |     "PipelineEventType", | ||||||
|     "PipelineNotFound", |     "PipelineNotFound", | ||||||
|     "VadSensitivity", |  | ||||||
|     "VadSensitivitySelect", |  | ||||||
|     "WakeWordSettings", |     "WakeWordSettings", | ||||||
|     "async_create_default_pipeline", |     "async_create_default_pipeline", | ||||||
|     "async_get_pipelines", |     "async_get_pipelines", | ||||||
|   | |||||||
| @@ -146,7 +146,7 @@ | |||||||
|         }, |         }, | ||||||
|         "state": { |         "state": { | ||||||
|           "title": "Add a Bayesian sensor", |           "title": "Add a Bayesian sensor", | ||||||
|           "description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.", |           "description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.", | ||||||
|  |  | ||||||
|           "data": { |           "data": { | ||||||
|             "name": "[%key:common::config_flow::data::name%]", |             "name": "[%key:common::config_flow::data::name%]", | ||||||
|   | |||||||
| @@ -113,6 +113,7 @@ __all__ = [ | |||||||
|     "BluetoothServiceInfo", |     "BluetoothServiceInfo", | ||||||
|     "BluetoothServiceInfoBleak", |     "BluetoothServiceInfoBleak", | ||||||
|     "HaBluetoothConnector", |     "HaBluetoothConnector", | ||||||
|  |     "HomeAssistantRemoteScanner", | ||||||
|     "async_address_present", |     "async_address_present", | ||||||
|     "async_ble_device_from_address", |     "async_ble_device_from_address", | ||||||
|     "async_clear_address_from_match_history", |     "async_clear_address_from_match_history", | ||||||
|   | |||||||
| @@ -74,10 +74,7 @@ from .const import ( | |||||||
|     StreamType, |     StreamType, | ||||||
| ) | ) | ||||||
| from .helper import get_camera_from_entity_id | from .helper import get_camera_from_entity_id | ||||||
| from .img_util import ( | from .img_util import scale_jpeg_camera_image | ||||||
|     TurboJPEGSingleton,  # noqa: F401 |  | ||||||
|     scale_jpeg_camera_image, |  | ||||||
| ) |  | ||||||
| from .prefs import ( | from .prefs import ( | ||||||
|     CameraPreferences, |     CameraPreferences, | ||||||
|     DynamicStreamSettings,  # noqa: F401 |     DynamicStreamSettings,  # noqa: F401 | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ from homeassistant.components.alexa import ( | |||||||
|     errors as alexa_errors, |     errors as alexa_errors, | ||||||
|     smart_home as alexa_smart_home, |     smart_home as alexa_smart_home, | ||||||
| ) | ) | ||||||
| from homeassistant.components.camera import async_register_ice_servers | from homeassistant.components.camera.webrtc import async_register_ice_servers | ||||||
| from homeassistant.components.google_assistant import smart_home as ga | from homeassistant.components.google_assistant import smart_home as ga | ||||||
| from homeassistant.const import __version__ as HA_VERSION | from homeassistant.const import __version__ as HA_VERSION | ||||||
| from homeassistant.core import Context, HassJob, HomeAssistant, callback | from homeassistant.core import Context, HassJob, HomeAssistant, callback | ||||||
|   | |||||||
| @@ -12,9 +12,7 @@ from hass_nabucasa.google_report_state import ErrorResponse | |||||||
|  |  | ||||||
| from homeassistant.components.binary_sensor import BinarySensorDeviceClass | from homeassistant.components.binary_sensor import BinarySensorDeviceClass | ||||||
| from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN | from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN | ||||||
| from homeassistant.components.google_assistant.helpers import (  # pylint: disable=hass-component-root-import | from homeassistant.components.google_assistant.helpers import AbstractConfig | ||||||
|     AbstractConfig, |  | ||||||
| ) |  | ||||||
| from homeassistant.components.homeassistant.exposed_entities import ( | from homeassistant.components.homeassistant.exposed_entities import ( | ||||||
|     async_expose_entity, |     async_expose_entity, | ||||||
|     async_get_assistant_settings, |     async_get_assistant_settings, | ||||||
|   | |||||||
| @@ -13,6 +13,6 @@ | |||||||
|   "integration_type": "system", |   "integration_type": "system", | ||||||
|   "iot_class": "cloud_push", |   "iot_class": "cloud_push", | ||||||
|   "loggers": ["acme", "hass_nabucasa", "snitun"], |   "loggers": ["acme", "hass_nabucasa", "snitun"], | ||||||
|   "requirements": ["hass-nabucasa==1.4.0"], |   "requirements": ["hass-nabucasa==1.3.0"], | ||||||
|   "single_config_entry": true |   "single_config_entry": true | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender | |||||||
| from homeassistant.auth.const import GROUP_ID_ADMIN | from homeassistant.auth.const import GROUP_ID_ADMIN | ||||||
| from homeassistant.auth.models import User | from homeassistant.auth.models import User | ||||||
| from homeassistant.components import webhook | from homeassistant.components import webhook | ||||||
| from homeassistant.components.google_assistant.http import (  # pylint: disable=hass-component-root-import | from homeassistant.components.google_assistant.http import ( | ||||||
|     async_get_users as async_get_google_assistant_users, |     async_get_users as async_get_google_assistant_users, | ||||||
| ) | ) | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
|   | |||||||
| @@ -6,9 +6,7 @@ from typing import Any | |||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN | from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN | ||||||
| from homeassistant.components.automation.config import (  # pylint: disable=hass-component-root-import | from homeassistant.components.automation.config import async_validate_config_item | ||||||
|     async_validate_config_item, |  | ||||||
| ) |  | ||||||
| from homeassistant.config import AUTOMATION_CONFIG_PATH | from homeassistant.config import AUTOMATION_CONFIG_PATH | ||||||
| from homeassistant.const import CONF_ID, SERVICE_RELOAD | from homeassistant.const import CONF_ID, SERVICE_RELOAD | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
|   | |||||||
| @@ -5,9 +5,7 @@ from __future__ import annotations | |||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN | from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN | ||||||
| from homeassistant.components.script.config import (  # pylint: disable=hass-component-root-import | from homeassistant.components.script.config import async_validate_config_item | ||||||
|     async_validate_config_item, |  | ||||||
| ) |  | ||||||
| from homeassistant.config import SCRIPT_CONFIG_PATH | from homeassistant.config import SCRIPT_CONFIG_PATH | ||||||
| from homeassistant.const import SERVICE_RELOAD | from homeassistant.const import SERVICE_RELOAD | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
|   | |||||||
| @@ -569,17 +569,14 @@ class ChatLog: | |||||||
|         if llm_api: |         if llm_api: | ||||||
|             prompt_parts.append(llm_api.api_prompt) |             prompt_parts.append(llm_api.api_prompt) | ||||||
|  |  | ||||||
|         # Append current date and time to the prompt if the corresponding tool is not provided |         prompt_parts.append( | ||||||
|         llm_tools: list[llm.Tool] = llm_api.tools if llm_api else [] |             await self._async_expand_prompt_template( | ||||||
|         if not any(tool.name.endswith("GetDateTime") for tool in llm_tools): |                 llm_context, | ||||||
|             prompt_parts.append( |                 llm.BASE_PROMPT, | ||||||
|                 await self._async_expand_prompt_template( |                 llm_context.language, | ||||||
|                     llm_context, |                 user_name, | ||||||
|                     llm.DATE_TIME_PROMPT, |  | ||||||
|                     llm_context.language, |  | ||||||
|                     user_name, |  | ||||||
|                 ) |  | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         if extra_system_prompt := ( |         if extra_system_prompt := ( | ||||||
|             # Take new system prompt if one was given |             # Take new system prompt if one was given | ||||||
|   | |||||||
| @@ -5,9 +5,7 @@ from __future__ import annotations | |||||||
| import datetime | import datetime | ||||||
|  |  | ||||||
| from homeassistant.components.alarm_control_panel import AlarmControlPanelState | from homeassistant.components.alarm_control_panel import AlarmControlPanelState | ||||||
| from homeassistant.components.manual.alarm_control_panel import (  # pylint: disable=hass-component-root-import | from homeassistant.components.manual.alarm_control_panel import ManualAlarm | ||||||
|     ManualAlarm, |  | ||||||
| ) |  | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME | from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
|   | |||||||
| @@ -139,7 +139,6 @@ class DemoCover(CoverEntity): | |||||||
|             self.async_write_ha_state() |             self.async_write_ha_state() | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         self._is_opening = False |  | ||||||
|         self._is_closing = True |         self._is_closing = True | ||||||
|         self._listen_cover() |         self._listen_cover() | ||||||
|         self._requested_closing = True |         self._requested_closing = True | ||||||
| @@ -163,7 +162,6 @@ class DemoCover(CoverEntity): | |||||||
|             return |             return | ||||||
|  |  | ||||||
|         self._is_opening = True |         self._is_opening = True | ||||||
|         self._is_closing = False |  | ||||||
|         self._listen_cover() |         self._listen_cover() | ||||||
|         self._requested_closing = False |         self._requested_closing = False | ||||||
|         self.async_write_ha_state() |         self.async_write_ha_state() | ||||||
| @@ -183,14 +181,10 @@ class DemoCover(CoverEntity): | |||||||
|         if self._position == position: |         if self._position == position: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         self._is_closing = position < (self._position or 0) |  | ||||||
|         self._is_opening = not self._is_closing |  | ||||||
|  |  | ||||||
|         self._listen_cover() |         self._listen_cover() | ||||||
|         self._requested_closing = ( |         self._requested_closing = ( | ||||||
|             self._position is not None and position < self._position |             self._position is not None and position < self._position | ||||||
|         ) |         ) | ||||||
|         self.async_write_ha_state() |  | ||||||
|  |  | ||||||
|     async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: |     async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: | ||||||
|         """Move the cover til to a specific position.""" |         """Move the cover til to a specific position.""" | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ | |||||||
|   "iot_class": "local_polling", |   "iot_class": "local_polling", | ||||||
|   "loggers": ["pydoods"], |   "loggers": ["pydoods"], | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["pydoods==1.0.2", "Pillow==12.0.0"] |   "requirements": ["pydoods==1.0.2", "Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,11 +8,8 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario | |||||||
| from eheimdigital.device import EheimDigitalDevice | from eheimdigital.device import EheimDigitalDevice | ||||||
| from eheimdigital.types import FilterErrorCode | from eheimdigital.types import FilterErrorCode | ||||||
|  |  | ||||||
| from homeassistant.components.sensor import ( | from homeassistant.components.sensor import SensorEntity, SensorEntityDescription | ||||||
|     SensorDeviceClass, | from homeassistant.components.sensor.const import SensorDeviceClass | ||||||
|     SensorEntity, |  | ||||||
|     SensorEntityDescription, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime | from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|   | |||||||
| @@ -16,9 +16,7 @@ from homeassistant.components.sensor import ( | |||||||
|     SensorEntity, |     SensorEntity, | ||||||
|     SensorStateClass, |     SensorStateClass, | ||||||
| ) | ) | ||||||
| from homeassistant.components.sensor.recorder import (  # pylint: disable=hass-component-root-import | from homeassistant.components.sensor.recorder import reset_detected | ||||||
|     reset_detected, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume | from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume | ||||||
| from homeassistant.core import ( | from homeassistant.core import ( | ||||||
|     HomeAssistant, |     HomeAssistant, | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ from homeassistant.components.sensor import ( | |||||||
|     SensorDeviceClass, |     SensorDeviceClass, | ||||||
|     SensorEntity, |     SensorEntity, | ||||||
|     SensorEntityDescription, |     SensorEntityDescription, | ||||||
|     SensorStateClass, |  | ||||||
| ) | ) | ||||||
|  | from homeassistant.components.sensor.const import SensorStateClass | ||||||
| from homeassistant.const import PERCENTAGE | from homeassistant.const import PERCENTAGE | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ from dataclasses import replace | |||||||
|  |  | ||||||
| from aioesphomeapi import EntityInfo, SelectInfo, SelectState | from aioesphomeapi import EntityInfo, SelectInfo, SelectState | ||||||
|  |  | ||||||
| from homeassistant.components.assist_pipeline import ( | from homeassistant.components.assist_pipeline.select import ( | ||||||
|     AssistPipelineSelect, |     AssistPipelineSelect, | ||||||
|     VadSensitivitySelect, |     VadSensitivitySelect, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -19,9 +19,7 @@ from homeassistant.components.ffmpeg import ( | |||||||
|     FFmpegManager, |     FFmpegManager, | ||||||
|     get_ffmpeg_manager, |     get_ffmpeg_manager, | ||||||
| ) | ) | ||||||
| from homeassistant.components.ffmpeg_motion.binary_sensor import (  # pylint: disable=hass-component-root-import | from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor | ||||||
|     FFmpegBinarySensor, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import CONF_NAME | from homeassistant.const import CONF_NAME | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers import config_validation as cv | from homeassistant.helpers import config_validation as cv | ||||||
|   | |||||||
| @@ -4,12 +4,8 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from pyfirefly.models import Account, Category | from pyfirefly.models import Account, Category | ||||||
|  |  | ||||||
| from homeassistant.components.sensor import ( | from homeassistant.components.sensor import SensorEntity, SensorStateClass, StateType | ||||||
|     SensorDeviceClass, | from homeassistant.components.sensor.const import SensorDeviceClass | ||||||
|     SensorEntity, |  | ||||||
|     SensorStateClass, |  | ||||||
|     StateType, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import EntityCategory | from homeassistant.const import EntityCategory | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|   | |||||||
| @@ -6,8 +6,9 @@ import logging | |||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from homeassistant.components.camera import CameraEntityFeature | from homeassistant.components.camera import CameraEntityFeature | ||||||
| from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT | from homeassistant.components.ffmpeg.camera import ( | ||||||
| from homeassistant.components.ffmpeg.camera import (  # pylint: disable=hass-component-root-import |     CONF_EXTRA_ARGUMENTS, | ||||||
|  |     CONF_INPUT, | ||||||
|     DEFAULT_ARGUMENTS, |     DEFAULT_ARGUMENTS, | ||||||
|     FFmpegCamera, |     FFmpegCamera, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -7,5 +7,5 @@ | |||||||
|   "documentation": "https://www.home-assistant.io/integrations/generic", |   "documentation": "https://www.home-assistant.io/integrations/generic", | ||||||
|   "integration_type": "device", |   "integration_type": "device", | ||||||
|   "iot_class": "local_push", |   "iot_class": "local_push", | ||||||
|   "requirements": ["av==13.1.0", "Pillow==12.0.0"] |   "requirements": ["av==13.1.0", "Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,8 +30,8 @@ from homeassistant.components.camera import ( | |||||||
|     WebRTCMessage, |     WebRTCMessage, | ||||||
|     WebRTCSendMessage, |     WebRTCSendMessage, | ||||||
|     async_register_webrtc_provider, |     async_register_webrtc_provider, | ||||||
|     get_dynamic_camera_stream_settings, |  | ||||||
| ) | ) | ||||||
|  | from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings | ||||||
| from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN | from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN | ||||||
| from homeassistant.components.stream import Orientation | from homeassistant.components.stream import Orientation | ||||||
| from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry | from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "domain": "growatt_server", |   "domain": "growatt_server", | ||||||
|   "name": "Growatt", |   "name": "Growatt", | ||||||
|   "codeowners": ["@johanzander"], |   "codeowners": [], | ||||||
|   "config_flow": true, |   "config_flow": true, | ||||||
|   "documentation": "https://www.home-assistant.io/integrations/growatt_server", |   "documentation": "https://www.home-assistant.io/integrations/growatt_server", | ||||||
|   "iot_class": "cloud_polling", |   "iot_class": "cloud_polling", | ||||||
|   | |||||||
| @@ -7,5 +7,5 @@ | |||||||
|   "iot_class": "cloud_polling", |   "iot_class": "cloud_polling", | ||||||
|   "loggers": ["habiticalib"], |   "loggers": ["habiticalib"], | ||||||
|   "quality_scale": "platinum", |   "quality_scale": "platinum", | ||||||
|   "requirements": ["habiticalib==0.4.6"] |   "requirements": ["habiticalib==0.4.5"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ from homeassistant.components.binary_sensor import ( | |||||||
|     BinarySensorDeviceClass, |     BinarySensorDeviceClass, | ||||||
| ) | ) | ||||||
| from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN | from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN | ||||||
| from homeassistant.components.device_automation.trigger import (  # pylint: disable=hass-component-root-import | from homeassistant.components.device_automation.trigger import ( | ||||||
|     async_validate_trigger_config, |     async_validate_trigger_config, | ||||||
| ) | ) | ||||||
| from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass | from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ | |||||||
|   "config_flow": true, |   "config_flow": true, | ||||||
|   "dependencies": ["application_credentials"], |   "dependencies": ["application_credentials"], | ||||||
|   "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", |   "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", | ||||||
|   "integration_type": "hub", |  | ||||||
|   "iot_class": "cloud_push", |   "iot_class": "cloud_push", | ||||||
|   "loggers": ["aioautomower"], |   "loggers": ["aioautomower"], | ||||||
|   "quality_scale": "silver", |   "quality_scale": "silver", | ||||||
|   | |||||||
| @@ -7,5 +7,5 @@ | |||||||
|   "documentation": "https://www.home-assistant.io/integrations/image_upload", |   "documentation": "https://www.home-assistant.io/integrations/image_upload", | ||||||
|   "integration_type": "system", |   "integration_type": "system", | ||||||
|   "quality_scale": "internal", |   "quality_scale": "internal", | ||||||
|   "requirements": ["Pillow==12.0.0"] |   "requirements": ["Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,12 +2,7 @@ | |||||||
|  |  | ||||||
| from typing import Any, Final | from typing import Any, Final | ||||||
|  |  | ||||||
| from iometer import ( | from iometer import IOmeterClient, IOmeterConnectionError | ||||||
|     IOmeterClient, |  | ||||||
|     IOmeterConnectionError, |  | ||||||
|     IOmeterNoReadingsError, |  | ||||||
|     IOmeterNoStatusError, |  | ||||||
| ) |  | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||||||
| @@ -39,11 +34,6 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN): | |||||||
|         client = IOmeterClient(host=host, session=session) |         client = IOmeterClient(host=host, session=session) | ||||||
|         try: |         try: | ||||||
|             status = await client.get_current_status() |             status = await client.get_current_status() | ||||||
|             _ = await client.get_current_reading() |  | ||||||
|         except IOmeterNoStatusError: |  | ||||||
|             return self.async_abort(reason="no_status") |  | ||||||
|         except IOmeterNoReadingsError: |  | ||||||
|             return self.async_abort(reason="no_readings") |  | ||||||
|         except IOmeterConnectionError: |         except IOmeterConnectionError: | ||||||
|             return self.async_abort(reason="cannot_connect") |             return self.async_abort(reason="cannot_connect") | ||||||
|  |  | ||||||
| @@ -80,11 +70,6 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN): | |||||||
|             client = IOmeterClient(host=self._host, session=session) |             client = IOmeterClient(host=self._host, session=session) | ||||||
|             try: |             try: | ||||||
|                 status = await client.get_current_status() |                 status = await client.get_current_status() | ||||||
|                 _ = await client.get_current_reading() |  | ||||||
|             except IOmeterNoStatusError: |  | ||||||
|                 errors["base"] = "no_status" |  | ||||||
|             except IOmeterNoReadingsError: |  | ||||||
|                 errors["base"] = "no_readings" |  | ||||||
|             except IOmeterConnectionError: |             except IOmeterConnectionError: | ||||||
|                 errors["base"] = "cannot_connect" |                 errors["base"] = "cannot_connect" | ||||||
|             else: |             else: | ||||||
|   | |||||||
| @@ -20,8 +20,6 @@ | |||||||
|       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" |       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" | ||||||
|     }, |     }, | ||||||
|     "error": { |     "error": { | ||||||
|       "no_status": "No status received from the IOmeter. Check your device status in the IOmeter app", |  | ||||||
|       "no_readings": "No readings received from the IOmeter. Please attach the IOmeter Core to the electricity meter and wait for the first reading.", |  | ||||||
|       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", |       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" |       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]): | |||||||
|             manufacturer=self.coordinator.data.info.manufacturer, |             manufacturer=self.coordinator.data.info.manufacturer, | ||||||
|             model=self.coordinator.data.info.model, |             model=self.coordinator.data.info.model, | ||||||
|             name=self.coordinator.data.info.name, |             name=self.coordinator.data.info.name, | ||||||
|             serial_number=self.coordinator.data.info.serial, |  | ||||||
|             sw_version=self.coordinator.data.info.version, |             sw_version=self.coordinator.data.info.version, | ||||||
|             configuration_url=self.coordinator.data.info.more_info, |             configuration_url=self.coordinator.data.info.more_info, | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -358,7 +358,7 @@ | |||||||
|             "entity_label": "Entity name", |             "entity_label": "Entity name", | ||||||
|             "entity_description": "Optional if a device is selected, otherwise required. If the entity is assigned to a device, the device name is used as prefix.", |             "entity_description": "Optional if a device is selected, otherwise required. If the entity is assigned to a device, the device name is used as prefix.", | ||||||
|             "entity_category_title": "Entity category", |             "entity_category_title": "Entity category", | ||||||
|             "entity_category_description": "Classification of a non-primary entity. Leave empty for standard behavior." |             "entity_category_description": "Classification of a non-primary entity. Leave empty for standard behaviour." | ||||||
|           }, |           }, | ||||||
|           "knx": { |           "knx": { | ||||||
|             "title": "KNX configuration", |             "title": "KNX configuration", | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ from pychromecast import Chromecast | |||||||
| from pychromecast.const import CAST_TYPE_CHROMECAST | from pychromecast.const import CAST_TYPE_CHROMECAST | ||||||
|  |  | ||||||
| from homeassistant.components.cast import DOMAIN as CAST_DOMAIN | from homeassistant.components.cast import DOMAIN as CAST_DOMAIN | ||||||
| from homeassistant.components.cast.home_assistant_cast import (  # pylint: disable=hass-component-root-import | from homeassistant.components.cast.home_assistant_cast import ( | ||||||
|     ATTR_URL_PATH, |     ATTR_URL_PATH, | ||||||
|     ATTR_VIEW_PATH, |     ATTR_VIEW_PATH, | ||||||
|     NO_URL_AVAILABLE_ERROR, |     NO_URL_AVAILABLE_ERROR, | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ | |||||||
|   "iot_class": "cloud_push", |   "iot_class": "cloud_push", | ||||||
|   "loggers": ["matrix_client"], |   "loggers": ["matrix_client"], | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0"] |   "requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,5 +7,5 @@ | |||||||
|   "integration_type": "service", |   "integration_type": "service", | ||||||
|   "iot_class": "local_polling", |   "iot_class": "local_polling", | ||||||
|   "quality_scale": "platinum", |   "quality_scale": "platinum", | ||||||
|   "requirements": ["aiomealie==1.0.1"] |   "requirements": ["aiomealie==1.0.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,8 +5,9 @@ from __future__ import annotations | |||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| from datapoint.Forecast import Forecast | import datapoint | ||||||
| from datapoint.Manager import Manager | import datapoint.Forecast | ||||||
|  | import datapoint.Manager | ||||||
|  |  | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
| @@ -47,19 +48,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | |||||||
|  |  | ||||||
|     coordinates = f"{latitude}_{longitude}" |     coordinates = f"{latitude}_{longitude}" | ||||||
|  |  | ||||||
|     connection = Manager(api_key=api_key) |     connection = datapoint.Manager.Manager(api_key=api_key) | ||||||
|  |  | ||||||
|     async def async_update_hourly() -> Forecast: |     async def async_update_hourly() -> datapoint.Forecast: | ||||||
|         return await hass.async_add_executor_job( |         return await hass.async_add_executor_job( | ||||||
|             fetch_data, connection, latitude, longitude, "hourly" |             fetch_data, connection, latitude, longitude, "hourly" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     async def async_update_daily() -> Forecast: |     async def async_update_daily() -> datapoint.Forecast: | ||||||
|         return await hass.async_add_executor_job( |         return await hass.async_add_executor_job( | ||||||
|             fetch_data, connection, latitude, longitude, "daily" |             fetch_data, connection, latitude, longitude, "daily" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     async def async_update_twice_daily() -> Forecast: |     async def async_update_twice_daily() -> datapoint.Forecast: | ||||||
|         return await hass.async_add_executor_job( |         return await hass.async_add_executor_job( | ||||||
|             fetch_data, connection, latitude, longitude, "twice-daily" |             fetch_data, connection, latitude, longitude, "twice-daily" | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -6,8 +6,9 @@ from collections.abc import Mapping | |||||||
| import logging | import logging | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
|  | import datapoint | ||||||
| from datapoint.exceptions import APIException | from datapoint.exceptions import APIException | ||||||
| from datapoint.Manager import Manager | import datapoint.Manager | ||||||
| from requests import HTTPError | from requests import HTTPError | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ async def validate_input( | |||||||
|     Data has the keys from DATA_SCHEMA with values provided by the user. |     Data has the keys from DATA_SCHEMA with values provided by the user. | ||||||
|     """ |     """ | ||||||
|     errors = {} |     errors = {} | ||||||
|     connection = Manager(api_key=api_key) |     connection = datapoint.Manager.Manager(api_key=api_key) | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         forecast = await hass.async_add_executor_job( |         forecast = await hass.async_add_executor_job( | ||||||
|   | |||||||
| @@ -5,9 +5,8 @@ from __future__ import annotations | |||||||
| import logging | import logging | ||||||
| from typing import Any, Literal | from typing import Any, Literal | ||||||
|  |  | ||||||
| from datapoint.exceptions import APIException | import datapoint | ||||||
| from datapoint.Forecast import Forecast | from datapoint.Forecast import Forecast | ||||||
| from datapoint.Manager import Manager |  | ||||||
| from requests import HTTPError | from requests import HTTPError | ||||||
|  |  | ||||||
| from homeassistant.exceptions import ConfigEntryAuthFailed | from homeassistant.exceptions import ConfigEntryAuthFailed | ||||||
| @@ -17,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) | |||||||
|  |  | ||||||
|  |  | ||||||
| def fetch_data( | def fetch_data( | ||||||
|     connection: Manager, |     connection: datapoint.Manager, | ||||||
|     latitude: float, |     latitude: float, | ||||||
|     longitude: float, |     longitude: float, | ||||||
|     frequency: Literal["daily", "twice-daily", "hourly"], |     frequency: Literal["daily", "twice-daily", "hourly"], | ||||||
| @@ -27,7 +26,7 @@ def fetch_data( | |||||||
|         return connection.get_forecast( |         return connection.get_forecast( | ||||||
|             latitude, longitude, frequency, convert_weather_code=False |             latitude, longitude, frequency, convert_weather_code=False | ||||||
|         ) |         ) | ||||||
|     except (ValueError, APIException) as err: |     except (ValueError, datapoint.exceptions.APIException) as err: | ||||||
|         _LOGGER.error("Check Met Office connection: %s", err.args) |         _LOGGER.error("Check Met Office connection: %s", err.args) | ||||||
|         raise UpdateFailed from err |         raise UpdateFailed from err | ||||||
|     except HTTPError as err: |     except HTTPError as err: | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from __future__ import annotations | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from typing import Any, cast | from typing import Any, cast | ||||||
|  |  | ||||||
| from datapoint.Forecast import Forecast | from datapoint.Forecast import Forecast as ForecastData | ||||||
|  |  | ||||||
| from homeassistant.components.weather import ( | from homeassistant.components.weather import ( | ||||||
|     ATTR_FORECAST_CONDITION, |     ATTR_FORECAST_CONDITION, | ||||||
| @@ -22,7 +22,7 @@ from homeassistant.components.weather import ( | |||||||
|     ATTR_FORECAST_WIND_BEARING, |     ATTR_FORECAST_WIND_BEARING, | ||||||
|     DOMAIN as WEATHER_DOMAIN, |     DOMAIN as WEATHER_DOMAIN, | ||||||
|     CoordinatorWeatherEntity, |     CoordinatorWeatherEntity, | ||||||
|     Forecast as WeatherForecast, |     Forecast, | ||||||
|     WeatherEntityFeature, |     WeatherEntityFeature, | ||||||
| ) | ) | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| @@ -85,20 +85,20 @@ async def async_setup_entry( | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _build_hourly_forecast_data(timestep: dict[str, Any]) -> WeatherForecast: | def _build_hourly_forecast_data(timestep: dict[str, Any]) -> Forecast: | ||||||
|     data = WeatherForecast(datetime=timestep["time"].isoformat()) |     data = Forecast(datetime=timestep["time"].isoformat()) | ||||||
|     _populate_forecast_data(data, timestep, HOURLY_FORECAST_ATTRIBUTE_MAP) |     _populate_forecast_data(data, timestep, HOURLY_FORECAST_ATTRIBUTE_MAP) | ||||||
|     return data |     return data | ||||||
|  |  | ||||||
|  |  | ||||||
| def _build_daily_forecast_data(timestep: dict[str, Any]) -> WeatherForecast: | def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast: | ||||||
|     data = WeatherForecast(datetime=timestep["time"].isoformat()) |     data = Forecast(datetime=timestep["time"].isoformat()) | ||||||
|     _populate_forecast_data(data, timestep, DAILY_FORECAST_ATTRIBUTE_MAP) |     _populate_forecast_data(data, timestep, DAILY_FORECAST_ATTRIBUTE_MAP) | ||||||
|     return data |     return data | ||||||
|  |  | ||||||
|  |  | ||||||
| def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> WeatherForecast: | def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> Forecast: | ||||||
|     data = WeatherForecast(datetime=timestep["time"].isoformat()) |     data = Forecast(datetime=timestep["time"].isoformat()) | ||||||
|  |  | ||||||
|     # day and night forecasts have slightly different format |     # day and night forecasts have slightly different format | ||||||
|     if "daySignificantWeatherCode" in timestep: |     if "daySignificantWeatherCode" in timestep: | ||||||
| @@ -111,7 +111,7 @@ def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> WeatherForecas | |||||||
|  |  | ||||||
|  |  | ||||||
| def _populate_forecast_data( | def _populate_forecast_data( | ||||||
|     forecast: WeatherForecast, timestep: dict[str, Any], mapping: dict[str, str] |     forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     def get_mapped_attribute(attr: str) -> Any: |     def get_mapped_attribute(attr: str) -> Any: | ||||||
|         if attr not in mapping: |         if attr not in mapping: | ||||||
| @@ -153,9 +153,9 @@ def _populate_forecast_data( | |||||||
|  |  | ||||||
| class MetOfficeWeather( | class MetOfficeWeather( | ||||||
|     CoordinatorWeatherEntity[ |     CoordinatorWeatherEntity[ | ||||||
|         TimestampDataUpdateCoordinator[Forecast], |         TimestampDataUpdateCoordinator[ForecastData], | ||||||
|         TimestampDataUpdateCoordinator[Forecast], |         TimestampDataUpdateCoordinator[ForecastData], | ||||||
|         TimestampDataUpdateCoordinator[Forecast], |         TimestampDataUpdateCoordinator[ForecastData], | ||||||
|     ] |     ] | ||||||
| ): | ): | ||||||
|     """Implementation of a Met Office weather condition.""" |     """Implementation of a Met Office weather condition.""" | ||||||
| @@ -177,9 +177,9 @@ class MetOfficeWeather( | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         coordinator_daily: TimestampDataUpdateCoordinator[Forecast], |         coordinator_daily: TimestampDataUpdateCoordinator[ForecastData], | ||||||
|         coordinator_hourly: TimestampDataUpdateCoordinator[Forecast], |         coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData], | ||||||
|         coordinator_twice_daily: TimestampDataUpdateCoordinator[Forecast], |         coordinator_twice_daily: TimestampDataUpdateCoordinator[ForecastData], | ||||||
|         hass_data: dict[str, Any], |         hass_data: dict[str, Any], | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Initialise the platform with a data instance.""" |         """Initialise the platform with a data instance.""" | ||||||
| @@ -263,10 +263,10 @@ class MetOfficeWeather( | |||||||
|         return float(value) if value is not None else None |         return float(value) if value is not None else None | ||||||
|  |  | ||||||
|     @callback |     @callback | ||||||
|     def _async_forecast_daily(self) -> list[WeatherForecast] | None: |     def _async_forecast_daily(self) -> list[Forecast] | None: | ||||||
|         """Return the daily forecast in native units.""" |         """Return the daily forecast in native units.""" | ||||||
|         coordinator = cast( |         coordinator = cast( | ||||||
|             TimestampDataUpdateCoordinator[Forecast], |             TimestampDataUpdateCoordinator[ForecastData], | ||||||
|             self.forecast_coordinators["daily"], |             self.forecast_coordinators["daily"], | ||||||
|         ) |         ) | ||||||
|         timesteps = coordinator.data.timesteps |         timesteps = coordinator.data.timesteps | ||||||
| @@ -277,10 +277,10 @@ class MetOfficeWeather( | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     @callback |     @callback | ||||||
|     def _async_forecast_hourly(self) -> list[WeatherForecast] | None: |     def _async_forecast_hourly(self) -> list[Forecast] | None: | ||||||
|         """Return the hourly forecast in native units.""" |         """Return the hourly forecast in native units.""" | ||||||
|         coordinator = cast( |         coordinator = cast( | ||||||
|             TimestampDataUpdateCoordinator[Forecast], |             TimestampDataUpdateCoordinator[ForecastData], | ||||||
|             self.forecast_coordinators["hourly"], |             self.forecast_coordinators["hourly"], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -292,10 +292,10 @@ class MetOfficeWeather( | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     @callback |     @callback | ||||||
|     def _async_forecast_twice_daily(self) -> list[WeatherForecast] | None: |     def _async_forecast_twice_daily(self) -> list[Forecast] | None: | ||||||
|         """Return the twice daily forecast in native units.""" |         """Return the twice daily forecast in native units.""" | ||||||
|         coordinator = cast( |         coordinator = cast( | ||||||
|             TimestampDataUpdateCoordinator[Forecast], |             TimestampDataUpdateCoordinator[ForecastData], | ||||||
|             self.forecast_coordinators["twice_daily"], |             self.forecast_coordinators["twice_daily"], | ||||||
|         ) |         ) | ||||||
|         timesteps = coordinator.data.timesteps |         timesteps = coordinator.data.timesteps | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ from typing import Any | |||||||
| from homeassistant.components.diagnostics import async_redact_data | from homeassistant.components.diagnostics import async_redact_data | ||||||
| from homeassistant.const import CONF_HOST | from homeassistant.const import CONF_HOST | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers import device_registry as dr |  | ||||||
|  |  | ||||||
| from .onewirehub import OneWireConfigEntry | from .onewirehub import OneWireConfigEntry | ||||||
|  |  | ||||||
| @@ -27,28 +26,7 @@ async def async_get_config_entry_diagnostics( | |||||||
|             "data": async_redact_data(entry.data, TO_REDACT), |             "data": async_redact_data(entry.data, TO_REDACT), | ||||||
|             "options": {**entry.options}, |             "options": {**entry.options}, | ||||||
|         }, |         }, | ||||||
|         "devices": [asdict(device_details) for device_details in onewire_hub.devices], |         "devices": [asdict(device_details) for device_details in onewire_hub.devices] | ||||||
|     } |         if onewire_hub.devices | ||||||
|  |         else [], | ||||||
|  |  | ||||||
| async def async_get_device_diagnostics( |  | ||||||
|     hass: HomeAssistant, entry: OneWireConfigEntry, device_entry: dr.DeviceEntry |  | ||||||
| ) -> dict[str, Any]: |  | ||||||
|     """Return diagnostics for a device.""" |  | ||||||
|  |  | ||||||
|     onewire_hub = entry.runtime_data |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         "entry": { |  | ||||||
|             "title": entry.title, |  | ||||||
|             "data": async_redact_data(entry.data, TO_REDACT), |  | ||||||
|             "options": {**entry.options}, |  | ||||||
|         }, |  | ||||||
|         "device": asdict( |  | ||||||
|             next( |  | ||||||
|                 device_details |  | ||||||
|                 for device_details in onewire_hub.devices |  | ||||||
|                 if device_details.id[3:] == device_entry.serial_number |  | ||||||
|             ) |  | ||||||
|         ), |  | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ | |||||||
|   "integration_type": "hub", |   "integration_type": "hub", | ||||||
|   "iot_class": "local_polling", |   "iot_class": "local_polling", | ||||||
|   "loggers": ["aio_ownet"], |   "loggers": ["aio_ownet"], | ||||||
|   "quality_scale": "silver", |  | ||||||
|   "requirements": ["aio-ownet==0.0.4"], |   "requirements": ["aio-ownet==0.0.4"], | ||||||
|   "zeroconf": ["_owserver._tcp.local."] |   "zeroconf": ["_owserver._tcp.local."] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| rules: | rules: | ||||||
|   ## Bronze |   ## Bronze | ||||||
|   config-flow: done |   config-flow: | ||||||
|  |     status: todo | ||||||
|  |     comment: missing data_description on options flow | ||||||
|   test-before-configure: done |   test-before-configure: done | ||||||
|   unique-config-entry: |   unique-config-entry: | ||||||
|     status: done |     status: done | ||||||
| @@ -14,19 +16,27 @@ rules: | |||||||
|   entity-event-setup: |   entity-event-setup: | ||||||
|     status: exempt |     status: exempt | ||||||
|     comment: entities do not subscribe to events |     comment: entities do not subscribe to events | ||||||
|   dependency-transparency: done |   dependency-transparency: | ||||||
|  |     status: todo | ||||||
|  |     comment: The package is not built and published inside a CI pipeline | ||||||
|   action-setup: |   action-setup: | ||||||
|     status: exempt |     status: exempt | ||||||
|     comment: No service actions currently available |     comment: No service actions currently available | ||||||
|   common-modules: |   common-modules: | ||||||
|     status: done |     status: done | ||||||
|     comment: base entity available, but no coordinator |     comment: base entity available, but no coordinator | ||||||
|   docs-high-level-description: done |   docs-high-level-description: | ||||||
|   docs-installation-instructions: done |     status: todo | ||||||
|   docs-removal-instructions: done |     comment: Under review | ||||||
|  |   docs-installation-instructions: | ||||||
|  |     status: todo | ||||||
|  |     comment: Under review | ||||||
|  |   docs-removal-instructions: | ||||||
|  |     status: todo | ||||||
|  |     comment: Under review | ||||||
|   docs-actions: |   docs-actions: | ||||||
|     status: exempt |     status: todo | ||||||
|     comment: No service actions currently available |     comment: Under review | ||||||
|   brands: done |   brands: done | ||||||
|  |  | ||||||
|   ## Silver |   ## Silver | ||||||
| @@ -42,8 +52,12 @@ rules: | |||||||
|   parallel-updates: done |   parallel-updates: done | ||||||
|   test-coverage: done |   test-coverage: done | ||||||
|   integration-owner: done |   integration-owner: done | ||||||
|   docs-installation-parameters: done |   docs-installation-parameters: | ||||||
|   docs-configuration-parameters: done |     status: todo | ||||||
|  |     comment: Under review | ||||||
|  |   docs-configuration-parameters: | ||||||
|  |     status: todo | ||||||
|  |     comment: Under review | ||||||
|  |  | ||||||
|   ## Gold |   ## Gold | ||||||
|   entity-translations: done |   entity-translations: done | ||||||
| @@ -59,7 +73,9 @@ rules: | |||||||
|     comment: > |     comment: > | ||||||
|       Manual removal, as it is not possible to distinguish |       Manual removal, as it is not possible to distinguish | ||||||
|       between a flaky device and a device that has been removed |       between a flaky device and a device that has been removed | ||||||
|   diagnostics: done |   diagnostics: | ||||||
|  |     status: todo | ||||||
|  |     comment: config-entry diagnostics level available, might be nice to have device-level diagnostics | ||||||
|   exception-translations: |   exception-translations: | ||||||
|     status: todo |     status: todo | ||||||
|     comment: Under review |     comment: Under review | ||||||
|   | |||||||
| @@ -139,12 +139,8 @@ | |||||||
|     "step": { |     "step": { | ||||||
|       "device_selection": { |       "device_selection": { | ||||||
|         "data": { |         "data": { | ||||||
|           "clear_device_options": "Reset all device customizations", |           "clear_device_options": "Clear all device configurations", | ||||||
|           "device_selection": "Customize specific devices" |           "device_selection": "[%key:component::onewire::options::error::device_not_selected%]" | ||||||
|         }, |  | ||||||
|         "data_description": { |  | ||||||
|           "clear_device_options": "Use this to reset all device specific options to default values.", |  | ||||||
|           "device_selection": "Customize behavior of individual devices." |  | ||||||
|         }, |         }, | ||||||
|         "description": "Select what configuration steps to process", |         "description": "Select what configuration steps to process", | ||||||
|         "title": "1-Wire device options" |         "title": "1-Wire device options" | ||||||
| @@ -153,9 +149,6 @@ | |||||||
|         "data": { |         "data": { | ||||||
|           "precision": "Sensor precision" |           "precision": "Sensor precision" | ||||||
|         }, |         }, | ||||||
|         "data_description": { |  | ||||||
|           "precision": "The lower the precision, the faster the sensor will respond, but with less accuracy." |  | ||||||
|         }, |  | ||||||
|         "description": "Select sensor precision for {sensor_id}", |         "description": "Select sensor precision for {sensor_id}", | ||||||
|         "title": "1-Wire sensor precision" |         "title": "1-Wire sensor precision" | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ async def async_setup_entry( | |||||||
|             vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), |             vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), | ||||||
|             vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), |             vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), | ||||||
|             vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, |             vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, | ||||||
|             vol.Optional(ATTR_SPEED): cv.small_float, |             vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, | ||||||
|             vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In( |             vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In( | ||||||
|                 [ |                 [ | ||||||
|                     CONTINUOUS_MOVE, |                     CONTINUOUS_MOVE, | ||||||
| @@ -210,10 +210,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): | |||||||
|     async def async_perform_ptz( |     async def async_perform_ptz( | ||||||
|         self, |         self, | ||||||
|         distance, |         distance, | ||||||
|  |         speed, | ||||||
|         move_mode, |         move_mode, | ||||||
|         continuous_duration, |         continuous_duration, | ||||||
|         preset, |         preset, | ||||||
|         speed=None, |  | ||||||
|         pan=None, |         pan=None, | ||||||
|         tilt=None, |         tilt=None, | ||||||
|         zoom=None, |         zoom=None, | ||||||
|   | |||||||
| @@ -602,11 +602,10 @@ class ONVIFDevice: | |||||||
|                     return |                     return | ||||||
|  |  | ||||||
|                 req.PresetToken = preset_val |                 req.PresetToken = preset_val | ||||||
|                 if speed_val is not None: |                 req.Speed = { | ||||||
|                     req.Speed = { |                     "PanTilt": {"x": speed_val, "y": speed_val}, | ||||||
|                         "PanTilt": {"x": speed_val, "y": speed_val}, |                     "Zoom": {"x": speed_val}, | ||||||
|                         "Zoom": {"x": speed_val}, |                 } | ||||||
|                     } |  | ||||||
|                 await ptz_service.GotoPreset(req) |                 await ptz_service.GotoPreset(req) | ||||||
|             elif move_mode == STOP_MOVE: |             elif move_mode == STOP_MOVE: | ||||||
|                 await ptz_service.Stop(req) |                 await ptz_service.Stop(req) | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ ptz: | |||||||
|           max: 1 |           max: 1 | ||||||
|           step: 0.01 |           step: 0.01 | ||||||
|     speed: |     speed: | ||||||
|  |       default: 0.5 | ||||||
|       selector: |       selector: | ||||||
|         number: |         number: | ||||||
|           min: 0 |           min: 0 | ||||||
|   | |||||||
| @@ -25,13 +25,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema( |  | ||||||
|     { |  | ||||||
|         vol.Required(CONF_HOST): str, |  | ||||||
|         vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def validate_input(hass: HomeAssistant, host: str, port: int) -> None: | async def validate_input(hass: HomeAssistant, host: str, port: int) -> None: | ||||||
|     """Validate the user input allows us to connect.""" |     """Validate the user input allows us to connect.""" | ||||||
| @@ -46,48 +39,6 @@ async def validate_input(hass: HomeAssistant, host: str, port: int) -> None: | |||||||
| class OpenRGBConfigFlow(ConfigFlow, domain=DOMAIN): | class OpenRGBConfigFlow(ConfigFlow, domain=DOMAIN): | ||||||
|     """Handle a config flow for OpenRGB.""" |     """Handle a config flow for OpenRGB.""" | ||||||
|  |  | ||||||
|     async def async_step_reconfigure( |  | ||||||
|         self, user_input: dict[str, Any] | None = None |  | ||||||
|     ) -> ConfigFlowResult: |  | ||||||
|         """Handle reconfiguration of the OpenRGB SDK Server.""" |  | ||||||
|         reconfigure_entry = self._get_reconfigure_entry() |  | ||||||
|  |  | ||||||
|         errors: dict[str, str] = {} |  | ||||||
|         if user_input is not None: |  | ||||||
|             host = user_input[CONF_HOST] |  | ||||||
|             port = user_input[CONF_PORT] |  | ||||||
|  |  | ||||||
|             # Prevent duplicate entries |  | ||||||
|             self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) |  | ||||||
|  |  | ||||||
|             try: |  | ||||||
|                 await validate_input(self.hass, host, port) |  | ||||||
|             except CONNECTION_ERRORS: |  | ||||||
|                 errors["base"] = "cannot_connect" |  | ||||||
|             except Exception: |  | ||||||
|                 _LOGGER.exception( |  | ||||||
|                     "Unknown error while connecting to OpenRGB SDK server at %s", |  | ||||||
|                     f"{host}:{port}", |  | ||||||
|                 ) |  | ||||||
|                 errors["base"] = "unknown" |  | ||||||
|             else: |  | ||||||
|                 return self.async_update_reload_and_abort( |  | ||||||
|                     reconfigure_entry, |  | ||||||
|                     data_updates={CONF_HOST: host, CONF_PORT: port}, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         return self.async_show_form( |  | ||||||
|             step_id="reconfigure", |  | ||||||
|             data_schema=self.add_suggested_values_to_schema( |  | ||||||
|                 data_schema=STEP_RECONFIGURE_DATA_SCHEMA, |  | ||||||
|                 suggested_values={ |  | ||||||
|                     CONF_HOST: reconfigure_entry.data[CONF_HOST], |  | ||||||
|                     CONF_PORT: reconfigure_entry.data[CONF_PORT], |  | ||||||
|                 }, |  | ||||||
|             ), |  | ||||||
|             errors=errors, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     async def async_step_user( |     async def async_step_user( | ||||||
|         self, user_input: dict[str, Any] | None = None |         self, user_input: dict[str, Any] | None = None | ||||||
|     ) -> ConfigFlowResult: |     ) -> ConfigFlowResult: | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ rules: | |||||||
|   entity-translations: todo |   entity-translations: todo | ||||||
|   exception-translations: done |   exception-translations: done | ||||||
|   icon-translations: todo |   icon-translations: todo | ||||||
|   reconfiguration-flow: done |   reconfiguration-flow: todo | ||||||
|   repair-issues: todo |   repair-issues: todo | ||||||
|   stale-devices: todo |   stale-devices: todo | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ | |||||||
|   "config": { |   "config": { | ||||||
|     "step": { |     "step": { | ||||||
|       "user": { |       "user": { | ||||||
|         "title": "Set up OpenRGB SDK server", |  | ||||||
|         "description": "Set up your OpenRGB SDK server to allow control from within Home Assistant.", |         "description": "Set up your OpenRGB SDK server to allow control from within Home Assistant.", | ||||||
|         "data": { |         "data": { | ||||||
|           "name": "[%key:common::config_flow::data::name%]", |           "name": "[%key:common::config_flow::data::name%]", | ||||||
| @@ -14,18 +13,6 @@ | |||||||
|           "host": "The IP address or hostname of the computer running the OpenRGB SDK server.", |           "host": "The IP address or hostname of the computer running the OpenRGB SDK server.", | ||||||
|           "port": "The port number that the OpenRGB SDK server is running on." |           "port": "The port number that the OpenRGB SDK server is running on." | ||||||
|         } |         } | ||||||
|       }, |  | ||||||
|       "reconfigure": { |  | ||||||
|         "title": "Reconfigure OpenRGB SDK server", |  | ||||||
|         "description": "Update the connection settings for your OpenRGB SDK server.", |  | ||||||
|         "data": { |  | ||||||
|           "host": "[%key:common::config_flow::data::host%]", |  | ||||||
|           "port": "[%key:common::config_flow::data::port%]" |  | ||||||
|         }, |  | ||||||
|         "data_description": { |  | ||||||
|           "host": "[%key:component::openrgb::config::step::user::data_description::host%]", |  | ||||||
|           "port": "[%key:component::openrgb::config::step::user::data_description::port%]" |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "error": { |     "error": { | ||||||
| @@ -34,7 +21,6 @@ | |||||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" |       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||||
|     }, |     }, | ||||||
|     "abort": { |     "abort": { | ||||||
|       "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", |  | ||||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" |       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -8,6 +8,6 @@ | |||||||
|   "iot_class": "local_polling", |   "iot_class": "local_polling", | ||||||
|   "loggers": ["plugwise"], |   "loggers": ["plugwise"], | ||||||
|   "quality_scale": "platinum", |   "quality_scale": "platinum", | ||||||
|   "requirements": ["plugwise==1.8.1"], |   "requirements": ["plugwise==1.8.0"], | ||||||
|   "zeroconf": ["_plugwise._tcp.local."] |   "zeroconf": ["_plugwise._tcp.local."] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,5 +4,5 @@ | |||||||
|   "codeowners": [], |   "codeowners": [], | ||||||
|   "documentation": "https://www.home-assistant.io/integrations/proxy", |   "documentation": "https://www.home-assistant.io/integrations/proxy", | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["Pillow==12.0.0"] |   "requirements": ["Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ | |||||||
|   "iot_class": "calculated", |   "iot_class": "calculated", | ||||||
|   "loggers": ["pyzbar"], |   "loggers": ["pyzbar"], | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["Pillow==12.0.0", "pyzbar==0.1.7"] |   "requirements": ["Pillow==11.3.0", "pyzbar==0.1.7"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN): | |||||||
|                 title=self._discovered_devices[address], data={} |                 title=self._discovered_devices[address], data={} | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         current_addresses = self._async_current_ids(include_ignore=False) |         current_addresses = self._async_current_ids() | ||||||
|         for discovery_info in async_discovered_service_info(self.hass, False): |         for discovery_info in async_discovered_service_info(self.hass, False): | ||||||
|             address = discovery_info.address |             address = discovery_info.address | ||||||
|             if address in current_addresses or address in self._discovered_devices: |             if address in current_addresses or address in self._discovered_devices: | ||||||
|   | |||||||
| @@ -5,5 +5,5 @@ | |||||||
|   "documentation": "https://www.home-assistant.io/integrations/seven_segments", |   "documentation": "https://www.home-assistant.io/integrations/seven_segments", | ||||||
|   "iot_class": "local_polling", |   "iot_class": "local_polling", | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["Pillow==12.0.0"] |   "requirements": ["Pillow==11.3.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -59,8 +59,8 @@ from .coordinator import ( | |||||||
| ) | ) | ||||||
| from .repairs import ( | from .repairs import ( | ||||||
|     async_manage_ble_scanner_firmware_unsupported_issue, |     async_manage_ble_scanner_firmware_unsupported_issue, | ||||||
|     async_manage_deprecated_firmware_issue, |  | ||||||
|     async_manage_outbound_websocket_incorrectly_enabled_issue, |     async_manage_outbound_websocket_incorrectly_enabled_issue, | ||||||
|  |     async_manage_wall_display_firmware_unsupported_issue, | ||||||
| ) | ) | ||||||
| from .utils import ( | from .utils import ( | ||||||
|     async_create_issue_unsupported_firmware, |     async_create_issue_unsupported_firmware, | ||||||
| @@ -337,7 +337,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) | |||||||
|         await hass.config_entries.async_forward_entry_setups( |         await hass.config_entries.async_forward_entry_setups( | ||||||
|             entry, runtime_data.platforms |             entry, runtime_data.platforms | ||||||
|         ) |         ) | ||||||
|         async_manage_deprecated_firmware_issue(hass, entry) |         async_manage_wall_display_firmware_unsupported_issue(hass, entry) | ||||||
|         async_manage_ble_scanner_firmware_unsupported_issue( |         async_manage_ble_scanner_firmware_unsupported_issue( | ||||||
|             hass, |             hass, | ||||||
|             entry, |             entry, | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from __future__ import annotations | |||||||
| from enum import StrEnum | from enum import StrEnum | ||||||
| from logging import Logger, getLogger | from logging import Logger, getLogger | ||||||
| import re | import re | ||||||
| from typing import Final, TypedDict | from typing import Final | ||||||
|  |  | ||||||
| from aioshelly.const import ( | from aioshelly.const import ( | ||||||
|     MODEL_BULB, |     MODEL_BULB, | ||||||
| @@ -232,6 +232,7 @@ class BLEScannerMode(StrEnum): | |||||||
|  |  | ||||||
|  |  | ||||||
| BLE_SCANNER_MIN_FIRMWARE = "1.5.1" | BLE_SCANNER_MIN_FIRMWARE = "1.5.1" | ||||||
|  | WALL_DISPLAY_MIN_FIRMWARE = "2.3.0" | ||||||
|  |  | ||||||
| MAX_PUSH_UPDATE_FAILURES = 5 | MAX_PUSH_UPDATE_FAILURES = 5 | ||||||
| PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" | PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" | ||||||
| @@ -244,28 +245,9 @@ BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID = "ble_scanner_firmware_unsupported_{u | |||||||
| OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID = ( | OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID = ( | ||||||
|     "outbound_websocket_incorrectly_enabled_{unique}" |     "outbound_websocket_incorrectly_enabled_{unique}" | ||||||
| ) | ) | ||||||
| DEPRECATED_FIRMWARE_ISSUE_ID = "deprecated_firmware_{unique}" | WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID = ( | ||||||
|  |     "wall_display_firmware_unsupported_{unique}" | ||||||
|  | ) | ||||||
| class DeprecatedFirmwareInfo(TypedDict): |  | ||||||
|     """TypedDict for Deprecated Firmware Info.""" |  | ||||||
|  |  | ||||||
|     min_firmware: str |  | ||||||
|     ha_version: str |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Provide firmware deprecation data: |  | ||||||
| # key: device model |  | ||||||
| # value: dict with: |  | ||||||
| #   min_firmware: minimum supported firmware version |  | ||||||
| #   ha_version: Home Assistant version when older firmware will be deprecated |  | ||||||
| # Example: |  | ||||||
| # DEPRECATED_FIRMWARES: dict[str, DeprecatedFirmwareInfo] = { |  | ||||||
| #   MODEL_WALL_DISPLAY: DeprecatedFirmwareInfo( |  | ||||||
| #     {"min_firmware": "2.3.0", "ha_version": "2025.10.0"} |  | ||||||
| #   ), |  | ||||||
| # } |  | ||||||
| DEPRECATED_FIRMWARES: dict[str, DeprecatedFirmwareInfo] = {} |  | ||||||
|  |  | ||||||
| GAS_VALVE_OPEN_STATES = ("opening", "opened") | GAS_VALVE_OPEN_STATES = ("opening", "opened") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
| from aioshelly.const import MODEL_OUT_PLUG_S_G3, MODEL_PLUG_S_G3 | from aioshelly.const import MODEL_OUT_PLUG_S_G3, MODEL_PLUG_S_G3, MODEL_WALL_DISPLAY | ||||||
| from aioshelly.exceptions import DeviceConnectionError, RpcCallError | from aioshelly.exceptions import DeviceConnectionError, RpcCallError | ||||||
| from aioshelly.rpc_device import RpcDevice | from aioshelly.rpc_device import RpcDevice | ||||||
| from awesomeversion import AwesomeVersion | from awesomeversion import AwesomeVersion | ||||||
| @@ -19,10 +19,10 @@ from .const import ( | |||||||
|     BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID, |     BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID, | ||||||
|     BLE_SCANNER_MIN_FIRMWARE, |     BLE_SCANNER_MIN_FIRMWARE, | ||||||
|     CONF_BLE_SCANNER_MODE, |     CONF_BLE_SCANNER_MODE, | ||||||
|     DEPRECATED_FIRMWARE_ISSUE_ID, |  | ||||||
|     DEPRECATED_FIRMWARES, |  | ||||||
|     DOMAIN, |     DOMAIN, | ||||||
|     OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID, |     OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID, | ||||||
|  |     WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID, | ||||||
|  |     WALL_DISPLAY_MIN_FIRMWARE, | ||||||
|     BLEScannerMode, |     BLEScannerMode, | ||||||
| ) | ) | ||||||
| from .coordinator import ShellyConfigEntry | from .coordinator import ShellyConfigEntry | ||||||
| @@ -70,25 +70,21 @@ def async_manage_ble_scanner_firmware_unsupported_issue( | |||||||
|  |  | ||||||
|  |  | ||||||
| @callback | @callback | ||||||
| def async_manage_deprecated_firmware_issue( | def async_manage_wall_display_firmware_unsupported_issue( | ||||||
|     hass: HomeAssistant, |     hass: HomeAssistant, | ||||||
|     entry: ShellyConfigEntry, |     entry: ShellyConfigEntry, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Manage deprecated firmware issue.""" |     """Manage the Wall Display firmware unsupported issue.""" | ||||||
|     issue_id = DEPRECATED_FIRMWARE_ISSUE_ID.format(unique=entry.unique_id) |     issue_id = WALL_DISPLAY_FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) | ||||||
|  |  | ||||||
|     if TYPE_CHECKING: |     if TYPE_CHECKING: | ||||||
|         assert entry.runtime_data.rpc is not None |         assert entry.runtime_data.rpc is not None | ||||||
|  |  | ||||||
|     device = entry.runtime_data.rpc.device |     device = entry.runtime_data.rpc.device | ||||||
|     model = entry.data["model"] |  | ||||||
|  |  | ||||||
|     if model in DEPRECATED_FIRMWARES: |  | ||||||
|         min_firmware = DEPRECATED_FIRMWARES[model]["min_firmware"] |  | ||||||
|         ha_version = DEPRECATED_FIRMWARES[model]["ha_version"] |  | ||||||
|  |  | ||||||
|  |     if entry.data["model"] == MODEL_WALL_DISPLAY: | ||||||
|         firmware = AwesomeVersion(device.shelly["ver"]) |         firmware = AwesomeVersion(device.shelly["ver"]) | ||||||
|         if firmware < min_firmware: |         if firmware < WALL_DISPLAY_MIN_FIRMWARE: | ||||||
|             ir.async_create_issue( |             ir.async_create_issue( | ||||||
|                 hass, |                 hass, | ||||||
|                 DOMAIN, |                 DOMAIN, | ||||||
| @@ -96,12 +92,11 @@ def async_manage_deprecated_firmware_issue( | |||||||
|                 is_fixable=True, |                 is_fixable=True, | ||||||
|                 is_persistent=True, |                 is_persistent=True, | ||||||
|                 severity=ir.IssueSeverity.WARNING, |                 severity=ir.IssueSeverity.WARNING, | ||||||
|                 translation_key="deprecated_firmware", |                 translation_key="wall_display_firmware_unsupported", | ||||||
|                 translation_placeholders={ |                 translation_placeholders={ | ||||||
|                     "device_name": device.name, |                     "device_name": device.name, | ||||||
|                     "ip_address": device.ip_address, |                     "ip_address": device.ip_address, | ||||||
|                     "firmware": firmware, |                     "firmware": firmware, | ||||||
|                     "ha_version": ha_version, |  | ||||||
|                 }, |                 }, | ||||||
|                 data={"entry_id": entry.entry_id}, |                 data={"entry_id": entry.entry_id}, | ||||||
|             ) |             ) | ||||||
| @@ -246,7 +241,7 @@ async def async_create_fix_flow( | |||||||
|  |  | ||||||
|     if ( |     if ( | ||||||
|         "ble_scanner_firmware_unsupported" in issue_id |         "ble_scanner_firmware_unsupported" in issue_id | ||||||
|         or "deprecated_firmware" in issue_id |         or "wall_display_firmware_unsupported" in issue_id | ||||||
|     ): |     ): | ||||||
|         return FirmwareUpdateFlow(device) |         return FirmwareUpdateFlow(device) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -312,13 +312,13 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "deprecated_firmware": { |     "wall_display_firmware_unsupported": { | ||||||
|       "title": "{device_name} is running outdated firmware", |       "title": "{device_name} is running outdated firmware", | ||||||
|       "fix_flow": { |       "fix_flow": { | ||||||
|         "step": { |         "step": { | ||||||
|           "confirm": { |           "confirm": { | ||||||
|             "title": "{device_name} is running outdated firmware", |             "title": "{device_name} is running outdated firmware", | ||||||
|             "description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware}. This firmware version will not be supported by Shelly integration starting from Home Assistant {ha_version}.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version." |             "description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware}. This firmware version will not be supported by Shelly integration starting from Home Assistant 2025.11.0.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version." | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "abort": { |         "abort": { | ||||||
|   | |||||||
| @@ -6,5 +6,5 @@ | |||||||
|   "iot_class": "cloud_polling", |   "iot_class": "cloud_polling", | ||||||
|   "loggers": ["simplehound"], |   "loggers": ["simplehound"], | ||||||
|   "quality_scale": "legacy", |   "quality_scale": "legacy", | ||||||
|   "requirements": ["Pillow==12.0.0", "simplehound==0.3"] |   "requirements": ["Pillow==11.3.0", "simplehound==0.3"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -39,9 +39,7 @@ from homeassistant.components.media_player import ( | |||||||
|     async_process_play_media_url, |     async_process_play_media_url, | ||||||
| ) | ) | ||||||
| from homeassistant.components.plex import PLEX_URI_SCHEME | from homeassistant.components.plex import PLEX_URI_SCHEME | ||||||
| from homeassistant.components.plex.services import (  # pylint: disable=hass-component-root-import | from homeassistant.components.plex.services import process_plex_payload | ||||||
|     process_plex_payload, |  | ||||||
| ) |  | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
| from homeassistant.exceptions import HomeAssistantError, ServiceValidationError | from homeassistant.exceptions import HomeAssistantError, ServiceValidationError | ||||||
| from homeassistant.helpers import entity_registry as er | from homeassistant.helpers import entity_registry as er | ||||||
|   | |||||||
| @@ -441,7 +441,9 @@ class KeyFrameConverter: | |||||||
|  |  | ||||||
|         # Keep import here so that we can import stream integration |         # Keep import here so that we can import stream integration | ||||||
|         # without installing reqs |         # without installing reqs | ||||||
|         from homeassistant.components.camera import TurboJPEGSingleton  # noqa: PLC0415 |         from homeassistant.components.camera.img_util import (  # noqa: PLC0415 | ||||||
|  |             TurboJPEGSingleton, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         self._packet: Packet | None = None |         self._packet: Packet | None = None | ||||||
|         self._event: asyncio.Event = asyncio.Event() |         self._event: asyncio.Event = asyncio.Event() | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import io | |||||||
| import logging | import logging | ||||||
| from ssl import SSLContext | from ssl import SSLContext | ||||||
| from types import MappingProxyType | from types import MappingProxyType | ||||||
| from typing import Any, cast | from typing import Any | ||||||
|  |  | ||||||
| import httpx | import httpx | ||||||
| from telegram import ( | from telegram import ( | ||||||
| @@ -23,7 +23,6 @@ from telegram import ( | |||||||
|     InputMediaVideo, |     InputMediaVideo, | ||||||
|     InputPollOption, |     InputPollOption, | ||||||
|     Message, |     Message, | ||||||
|     PhotoSize, |  | ||||||
|     ReplyKeyboardMarkup, |     ReplyKeyboardMarkup, | ||||||
|     ReplyKeyboardRemove, |     ReplyKeyboardRemove, | ||||||
|     Update, |     Update, | ||||||
| @@ -57,10 +56,6 @@ from .const import ( | |||||||
|     ATTR_DISABLE_NOTIF, |     ATTR_DISABLE_NOTIF, | ||||||
|     ATTR_DISABLE_WEB_PREV, |     ATTR_DISABLE_WEB_PREV, | ||||||
|     ATTR_FILE, |     ATTR_FILE, | ||||||
|     ATTR_FILE_ID, |  | ||||||
|     ATTR_FILE_MIME_TYPE, |  | ||||||
|     ATTR_FILE_NAME, |  | ||||||
|     ATTR_FILE_SIZE, |  | ||||||
|     ATTR_FROM_FIRST, |     ATTR_FROM_FIRST, | ||||||
|     ATTR_FROM_LAST, |     ATTR_FROM_LAST, | ||||||
|     ATTR_INLINE_MESSAGE_ID, |     ATTR_INLINE_MESSAGE_ID, | ||||||
| @@ -91,7 +86,6 @@ from .const import ( | |||||||
|     CONF_CHAT_ID, |     CONF_CHAT_ID, | ||||||
|     CONF_PROXY_URL, |     CONF_PROXY_URL, | ||||||
|     DOMAIN, |     DOMAIN, | ||||||
|     EVENT_TELEGRAM_ATTACHMENT, |  | ||||||
|     EVENT_TELEGRAM_CALLBACK, |     EVENT_TELEGRAM_CALLBACK, | ||||||
|     EVENT_TELEGRAM_COMMAND, |     EVENT_TELEGRAM_COMMAND, | ||||||
|     EVENT_TELEGRAM_SENT, |     EVENT_TELEGRAM_SENT, | ||||||
| @@ -189,10 +183,6 @@ class BaseTelegramBot: | |||||||
|             # This is a command message - set event type to command and split data into command and args |             # This is a command message - set event type to command and split data into command and args | ||||||
|             event_type = EVENT_TELEGRAM_COMMAND |             event_type = EVENT_TELEGRAM_COMMAND | ||||||
|             event_data.update(self._get_command_event_data(message.text)) |             event_data.update(self._get_command_event_data(message.text)) | ||||||
|         elif filters.ATTACHMENT.filter(message): |  | ||||||
|             event_type = EVENT_TELEGRAM_ATTACHMENT |  | ||||||
|             event_data[ATTR_TEXT] = message.caption |  | ||||||
|             event_data.update(self._get_file_id_event_data(message)) |  | ||||||
|         else: |         else: | ||||||
|             event_type = EVENT_TELEGRAM_TEXT |             event_type = EVENT_TELEGRAM_TEXT | ||||||
|             event_data[ATTR_TEXT] = message.text |             event_data[ATTR_TEXT] = message.text | ||||||
| @@ -202,26 +192,6 @@ class BaseTelegramBot: | |||||||
|  |  | ||||||
|         return event_type, event_data |         return event_type, event_data | ||||||
|  |  | ||||||
|     def _get_file_id_event_data(self, message: Message) -> dict[str, Any]: |  | ||||||
|         """Extract file_id from a message attachment, if any.""" |  | ||||||
|         if filters.PHOTO.filter(message): |  | ||||||
|             photos = cast(Sequence[PhotoSize], message.effective_attachment) |  | ||||||
|             return { |  | ||||||
|                 ATTR_FILE_ID: photos[-1].file_id, |  | ||||||
|                 ATTR_FILE_MIME_TYPE: "image/jpeg",  # telegram always uses jpeg for photos |  | ||||||
|                 ATTR_FILE_SIZE: photos[-1].file_size, |  | ||||||
|             } |  | ||||||
|         return { |  | ||||||
|             k: getattr(message.effective_attachment, v) |  | ||||||
|             for k, v in ( |  | ||||||
|                 (ATTR_FILE_ID, "file_id"), |  | ||||||
|                 (ATTR_FILE_NAME, "file_name"), |  | ||||||
|                 (ATTR_FILE_MIME_TYPE, "mime_type"), |  | ||||||
|                 (ATTR_FILE_SIZE, "file_size"), |  | ||||||
|             ) |  | ||||||
|             if hasattr(message.effective_attachment, v) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def _get_user_event_data(self, user: User) -> dict[str, Any]: |     def _get_user_event_data(self, user: User) -> dict[str, Any]: | ||||||
|         return { |         return { | ||||||
|             ATTR_USER_ID: user.id, |             ATTR_USER_ID: user.id, | ||||||
| @@ -578,7 +548,6 @@ class TelegramNotificationService: | |||||||
|             "Error sending message", |             "Error sending message", | ||||||
|             params[ATTR_MESSAGE_TAG], |             params[ATTR_MESSAGE_TAG], | ||||||
|             text, |             text, | ||||||
|             target=target, |  | ||||||
|             parse_mode=params[ATTR_PARSER], |             parse_mode=params[ATTR_PARSER], | ||||||
|             disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], |             disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], | ||||||
|             disable_notification=params[ATTR_DISABLE_NOTIF], |             disable_notification=params[ATTR_DISABLE_NOTIF], | ||||||
|   | |||||||
| @@ -54,7 +54,6 @@ SERVICE_LEAVE_CHAT = "leave_chat" | |||||||
| EVENT_TELEGRAM_CALLBACK = "telegram_callback" | EVENT_TELEGRAM_CALLBACK = "telegram_callback" | ||||||
| EVENT_TELEGRAM_COMMAND = "telegram_command" | EVENT_TELEGRAM_COMMAND = "telegram_command" | ||||||
| EVENT_TELEGRAM_TEXT = "telegram_text" | EVENT_TELEGRAM_TEXT = "telegram_text" | ||||||
| EVENT_TELEGRAM_ATTACHMENT = "telegram_attachment" |  | ||||||
| EVENT_TELEGRAM_SENT = "telegram_sent" | EVENT_TELEGRAM_SENT = "telegram_sent" | ||||||
|  |  | ||||||
| PARSER_HTML = "html" | PARSER_HTML = "html" | ||||||
| @@ -91,10 +90,6 @@ ATTR_DISABLE_NOTIF = "disable_notification" | |||||||
| ATTR_DISABLE_WEB_PREV = "disable_web_page_preview" | ATTR_DISABLE_WEB_PREV = "disable_web_page_preview" | ||||||
| ATTR_EDITED_MSG = "edited_message" | ATTR_EDITED_MSG = "edited_message" | ||||||
| ATTR_FILE = "file" | ATTR_FILE = "file" | ||||||
| ATTR_FILE_ID = "file_id" |  | ||||||
| ATTR_FILE_MIME_TYPE = "file_mime_type" |  | ||||||
| ATTR_FILE_NAME = "file_name" |  | ||||||
| ATTR_FILE_SIZE = "file_size" |  | ||||||
| ATTR_FROM_FIRST = "from_first" | ATTR_FROM_FIRST = "from_first" | ||||||
| ATTR_FROM_LAST = "from_last" | ATTR_FROM_LAST = "from_last" | ||||||
| ATTR_KEYBOARD = "keyboard" | ATTR_KEYBOARD = "keyboard" | ||||||
|   | |||||||
| @@ -21,9 +21,7 @@ from homeassistant.components.sensor import ( | |||||||
|     SensorEntity, |     SensorEntity, | ||||||
|     SensorStateClass, |     SensorStateClass, | ||||||
| ) | ) | ||||||
| from homeassistant.components.sensor.helpers import (  # pylint: disable=hass-component-root-import | from homeassistant.components.sensor.helpers import async_parse_date_datetime | ||||||
|     async_parse_date_datetime, |  | ||||||
| ) |  | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
|     ATTR_ENTITY_ID, |     ATTR_ENTITY_ID, | ||||||
|   | |||||||
| @@ -11,6 +11,6 @@ | |||||||
|     "tf-models-official==2.5.0", |     "tf-models-official==2.5.0", | ||||||
|     "pycocotools==2.0.6", |     "pycocotools==2.0.6", | ||||||
|     "numpy==2.3.2", |     "numpy==2.3.2", | ||||||
|     "Pillow==12.0.0" |     "Pillow==11.3.0" | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -254,11 +254,10 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): | |||||||
|     def _is_position_reversed(self) -> bool: |     def _is_position_reversed(self) -> bool: | ||||||
|         """Check if the cover position and direction should be reversed.""" |         """Check if the cover position and direction should be reversed.""" | ||||||
|         # The default is True |         # The default is True | ||||||
|         # Having motor_reverse_mode == "forward" cancels the inversion |         # Having motor_reverse_mode == "back" cancels the inversion | ||||||
|         return not ( |         return not ( | ||||||
|             self._motor_reverse_mode_enum |             self._motor_reverse_mode_enum | ||||||
|             and self.device.status.get(self._motor_reverse_mode_enum.dpcode) |             and self.device.status.get(self._motor_reverse_mode_enum.dpcode) == "back" | ||||||
|             == "forward" |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|   | |||||||
| @@ -20,9 +20,7 @@ from homeassistant.components.sensor import ( | |||||||
|     SensorExtraStoredData, |     SensorExtraStoredData, | ||||||
|     SensorStateClass, |     SensorStateClass, | ||||||
| ) | ) | ||||||
| from homeassistant.components.sensor.recorder import (  # pylint: disable=hass-component-root-import | from homeassistant.components.sensor.recorder import _suggest_report_issue | ||||||
|     _suggest_report_issue, |  | ||||||
| ) |  | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
|     ATTR_DEVICE_CLASS, |     ATTR_DEVICE_CLASS, | ||||||
|   | |||||||
| @@ -13,5 +13,5 @@ | |||||||
|   "documentation": "https://www.home-assistant.io/integrations/vesync", |   "documentation": "https://www.home-assistant.io/integrations/vesync", | ||||||
|   "iot_class": "cloud_polling", |   "iot_class": "cloud_polling", | ||||||
|   "loggers": ["pyvesync"], |   "loggers": ["pyvesync"], | ||||||
|   "requirements": ["pyvesync==3.1.2"] |   "requirements": ["pyvesync==3.1.0"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ from homeassistant.components.binary_sensor import ( | |||||||
|     BinarySensorEntity, |     BinarySensorEntity, | ||||||
|     BinarySensorEntityDescription, |     BinarySensorEntityDescription, | ||||||
| ) | ) | ||||||
| from homeassistant.const import EntityCategory |  | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|  |  | ||||||
| @@ -118,32 +117,6 @@ GLOBAL_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( | |||||||
|         device_class=BinarySensorDeviceClass.PROBLEM, |         device_class=BinarySensorDeviceClass.PROBLEM, | ||||||
|         value_getter=lambda api: len(api.getDeviceErrors()) > 0, |         value_getter=lambda api: len(api.getDeviceErrors()) > 0, | ||||||
|     ), |     ), | ||||||
|     ViCareBinarySensorEntityDescription( |  | ||||||
|         key="identification_mode", |  | ||||||
|         translation_key="identification_mode", |  | ||||||
|         entity_category=EntityCategory.DIAGNOSTIC, |  | ||||||
|         value_getter=lambda api: api.getIdentification(), |  | ||||||
|         entity_registry_enabled_default=False, |  | ||||||
|     ), |  | ||||||
|     ViCareBinarySensorEntityDescription( |  | ||||||
|         key="mounting_mode", |  | ||||||
|         translation_key="mounting_mode", |  | ||||||
|         entity_category=EntityCategory.DIAGNOSTIC, |  | ||||||
|         value_getter=lambda api: api.getMountingMode(), |  | ||||||
|         entity_registry_enabled_default=False, |  | ||||||
|     ), |  | ||||||
|     ViCareBinarySensorEntityDescription( |  | ||||||
|         key="child_safety_lock_mode", |  | ||||||
|         translation_key="child_safety_lock_mode", |  | ||||||
|         value_getter=lambda api: api.getChildLock() == "active", |  | ||||||
|         entity_registry_enabled_default=False, |  | ||||||
|     ), |  | ||||||
|     ViCareBinarySensorEntityDescription( |  | ||||||
|         key="valve", |  | ||||||
|         translation_key="valve", |  | ||||||
|         device_class=BinarySensorDeviceClass.DOOR, |  | ||||||
|         value_getter=lambda api: api.isValveOpen(), |  | ||||||
|     ), |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,15 +21,6 @@ | |||||||
|       }, |       }, | ||||||
|       "one_time_charge": { |       "one_time_charge": { | ||||||
|         "default": "mdi:shower-head" |         "default": "mdi:shower-head" | ||||||
|       }, |  | ||||||
|       "mounting_mode": { |  | ||||||
|         "default": "mdi:wrench" |  | ||||||
|       }, |  | ||||||
|       "child_safety_lock_mode": { |  | ||||||
|         "default": "mdi:lock" |  | ||||||
|       }, |  | ||||||
|       "valve": { |  | ||||||
|         "default": "mdi:pipe-valve" |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "button": { |     "button": { | ||||||
| @@ -99,12 +90,6 @@ | |||||||
|       }, |       }, | ||||||
|       "ventilation_level": { |       "ventilation_level": { | ||||||
|         "default": "mdi:fan" |         "default": "mdi:fan" | ||||||
|       }, |  | ||||||
|       "zigbee_signal_strength": { |  | ||||||
|         "default": "mdi:wifi" |  | ||||||
|       }, |  | ||||||
|       "valve_position": { |  | ||||||
|         "default": "mdi:pipe-valve" |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -943,23 +943,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( | |||||||
|         entity_category=EntityCategory.DIAGNOSTIC, |         entity_category=EntityCategory.DIAGNOSTIC, | ||||||
|         value_getter=lambda api: api.getBatteryLevel(), |         value_getter=lambda api: api.getBatteryLevel(), | ||||||
|     ), |     ), | ||||||
|     ViCareSensorEntityDescription( |  | ||||||
|         key="zigbee_signal_strength", |  | ||||||
|         translation_key="zigbee_signal_strength", |  | ||||||
|         state_class=SensorStateClass.MEASUREMENT, |  | ||||||
|         entity_category=EntityCategory.DIAGNOSTIC, |  | ||||||
|         native_unit_of_measurement=PERCENTAGE, |  | ||||||
|         value_getter=lambda api: api.getZigbeeSignalStrength(), |  | ||||||
|         entity_registry_enabled_default=False, |  | ||||||
|     ), |  | ||||||
|     ViCareSensorEntityDescription( |  | ||||||
|         key="valve_position", |  | ||||||
|         translation_key="valve_position", |  | ||||||
|         state_class=SensorStateClass.MEASUREMENT, |  | ||||||
|         native_unit_of_measurement=PERCENTAGE, |  | ||||||
|         value_getter=lambda api: api.getValvePosition(), |  | ||||||
|         entity_registry_enabled_default=False, |  | ||||||
|     ), |  | ||||||
|     ViCareSensorEntityDescription( |     ViCareSensorEntityDescription( | ||||||
|         key="fuel_need", |         key="fuel_need", | ||||||
|         translation_key="fuel_need", |         translation_key="fuel_need", | ||||||
|   | |||||||
| @@ -66,18 +66,6 @@ | |||||||
|       }, |       }, | ||||||
|       "one_time_charge": { |       "one_time_charge": { | ||||||
|         "name": "One-time charge" |         "name": "One-time charge" | ||||||
|       }, |  | ||||||
|       "identification_mode": { |  | ||||||
|         "name": "Identification mode" |  | ||||||
|       }, |  | ||||||
|       "mounting_mode": { |  | ||||||
|         "name": "Mounting mode" |  | ||||||
|       }, |  | ||||||
|       "child_safety_lock_mode": { |  | ||||||
|         "name": "Child safety lock" |  | ||||||
|       }, |  | ||||||
|       "valve": { |  | ||||||
|         "name": "Valve" |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "button": { |     "button": { | ||||||
| @@ -514,12 +502,6 @@ | |||||||
|       }, |       }, | ||||||
|       "fuel_need": { |       "fuel_need": { | ||||||
|         "name": "Fuel need" |         "name": "Fuel need" | ||||||
|       }, |  | ||||||
|       "zigbee_signal_strength": { |  | ||||||
|         "name": "[%key:component::sensor::entity_component::signal_strength::name%]" |  | ||||||
|       }, |  | ||||||
|       "valve_position": { |  | ||||||
|         "name": "Valve position" |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "water_heater": { |     "water_heater": { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from homeassistant.components.assist_pipeline.repair_flows import (  # pylint: disable=hass-component-root-import | from homeassistant.components.assist_pipeline.repair_flows import ( | ||||||
|     AssistInProgressDeprecatedRepairFlow, |     AssistInProgressDeprecatedRepairFlow, | ||||||
| ) | ) | ||||||
| from homeassistant.components.repairs import RepairsFlow | from homeassistant.components.repairs import RepairsFlow | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
| from homeassistant.components.assist_pipeline import ( | from homeassistant.components.assist_pipeline.select import ( | ||||||
|     AssistPipelineSelect, |     AssistPipelineSelect, | ||||||
|     VadSensitivitySelect, |     VadSensitivitySelect, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -348,12 +348,6 @@ | |||||||
|       "odometer": { |       "odometer": { | ||||||
|         "default": "mdi:counter" |         "default": "mdi:counter" | ||||||
|       }, |       }, | ||||||
|       "service_warning": { |  | ||||||
|         "default": "mdi:wrench-clock", |  | ||||||
|         "state": { |  | ||||||
|           "no_warning": "mdi:car-wrench" |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       "target_battery_charge_level": { |       "target_battery_charge_level": { | ||||||
|         "default": "mdi:battery-medium" |         "default": "mdi:battery-medium" | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -8,6 +8,6 @@ | |||||||
|   "integration_type": "device", |   "integration_type": "device", | ||||||
|   "iot_class": "cloud_polling", |   "iot_class": "cloud_polling", | ||||||
|   "loggers": ["volvocarsapi"], |   "loggers": ["volvocarsapi"], | ||||||
|   "quality_scale": "platinum", |   "quality_scale": "silver", | ||||||
|   "requirements": ["volvocarsapi==0.4.3"] |   "requirements": ["volvocarsapi==0.4.3"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ rules: | |||||||
|  |  | ||||||
|   # Gold |   # Gold | ||||||
|   devices: done |   devices: done | ||||||
|   diagnostics: done |   diagnostics: todo | ||||||
|   discovery-update-info: |   discovery-update-info: | ||||||
|     status: exempt |     status: exempt | ||||||
|     comment: | |     comment: | | ||||||
| @@ -52,12 +52,12 @@ rules: | |||||||
|     comment: | |     comment: | | ||||||
|       No discovery possible. |       No discovery possible. | ||||||
|   docs-data-update: done |   docs-data-update: done | ||||||
|   docs-examples: done |   docs-examples: todo | ||||||
|   docs-known-limitations: done |   docs-known-limitations: done | ||||||
|   docs-supported-devices: done |   docs-supported-devices: done | ||||||
|   docs-supported-functions: done |   docs-supported-functions: done | ||||||
|   docs-troubleshooting: done |   docs-troubleshooting: done | ||||||
|   docs-use-cases: done |   docs-use-cases: todo | ||||||
|   dynamic-devices: |   dynamic-devices: | ||||||
|     status: exempt |     status: exempt | ||||||
|     comment: | |     comment: | | ||||||
| @@ -70,10 +70,7 @@ rules: | |||||||
|   exception-translations: done |   exception-translations: done | ||||||
|   icon-translations: done |   icon-translations: done | ||||||
|   reconfiguration-flow: done |   reconfiguration-flow: done | ||||||
|   repair-issues: |   repair-issues: todo | ||||||
|     status: exempt |  | ||||||
|     comment: | |  | ||||||
|       This integration doesn't have any cases where raising an issue is needed. |  | ||||||
|   stale-devices: |   stale-devices: | ||||||
|     status: exempt |     status: exempt | ||||||
|     comment: | |     comment: | | ||||||
|   | |||||||
| @@ -332,25 +332,6 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( | |||||||
|         state_class=SensorStateClass.TOTAL_INCREASING, |         state_class=SensorStateClass.TOTAL_INCREASING, | ||||||
|         suggested_display_precision=1, |         suggested_display_precision=1, | ||||||
|     ), |     ), | ||||||
|     # diagnostics endpoint |  | ||||||
|     VolvoSensorDescription( |  | ||||||
|         key="service_warning", |  | ||||||
|         api_field="serviceWarning", |  | ||||||
|         device_class=SensorDeviceClass.ENUM, |  | ||||||
|         options=[ |  | ||||||
|             "distance_driven_almost_time_for_service", |  | ||||||
|             "distance_driven_overdue_for_service", |  | ||||||
|             "distance_driven_time_for_service", |  | ||||||
|             "engine_hours_almost_time_for_service", |  | ||||||
|             "engine_hours_overdue_for_service", |  | ||||||
|             "engine_hours_time_for_service", |  | ||||||
|             "no_warning", |  | ||||||
|             "regular_maintenance_almost_time_for_service", |  | ||||||
|             "regular_maintenance_overdue_for_service", |  | ||||||
|             "regular_maintenance_time_for_service", |  | ||||||
|             "unknown_warning", |  | ||||||
|         ], |  | ||||||
|     ), |  | ||||||
|     # energy state endpoint |     # energy state endpoint | ||||||
|     VolvoSensorDescription( |     VolvoSensorDescription( | ||||||
|         key="target_battery_charge_level", |         key="target_battery_charge_level", | ||||||
|   | |||||||
| @@ -309,22 +309,6 @@ | |||||||
|       "odometer": { |       "odometer": { | ||||||
|         "name": "Odometer" |         "name": "Odometer" | ||||||
|       }, |       }, | ||||||
|       "service_warning": { |  | ||||||
|         "name": "Service", |  | ||||||
|         "state": { |  | ||||||
|           "distance_driven_almost_time_for_service": "Almost time for distance service", |  | ||||||
|           "distance_driven_overdue_for_service": "Distance service overdue", |  | ||||||
|           "distance_driven_time_for_service": "Time for distance service", |  | ||||||
|           "engine_hours_almost_time_for_service": "Almost time for engine service", |  | ||||||
|           "engine_hours_overdue_for_service": "Engine service overdue", |  | ||||||
|           "engine_hours_time_for_service": "Time for engine service", |  | ||||||
|           "no_warning": "No warning", |  | ||||||
|           "regular_maintenance_almost_time_for_service": "Almost time for service", |  | ||||||
|           "regular_maintenance_overdue_for_service": "Service overdue", |  | ||||||
|           "regular_maintenance_time_for_service": "Time for service", |  | ||||||
|           "unknown_warning": "Unknown warning" |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       "target_battery_charge_level": { |       "target_battery_charge_level": { | ||||||
|         "name": "Target battery charge level" |         "name": "Target battery charge level" | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -2,169 +2,32 @@ | |||||||
|  |  | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from types import MappingProxyType |  | ||||||
| from typing import TYPE_CHECKING |  | ||||||
|  |  | ||||||
| from aiowaqi import WAQIClient | from aiowaqi import WAQIClient | ||||||
|  |  | ||||||
| from homeassistant.config_entries import ConfigEntry, ConfigSubentry |  | ||||||
| from homeassistant.const import CONF_API_KEY, Platform | from homeassistant.const import CONF_API_KEY, Platform | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers import ( |  | ||||||
|     config_validation as cv, |  | ||||||
|     device_registry as dr, |  | ||||||
|     entity_registry as er, |  | ||||||
| ) |  | ||||||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||||||
| from homeassistant.helpers.typing import ConfigType |  | ||||||
|  |  | ||||||
| from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION |  | ||||||
| from .coordinator import WAQIConfigEntry, WAQIDataUpdateCoordinator | from .coordinator import WAQIConfigEntry, WAQIDataUpdateCoordinator | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) |  | ||||||
| PLATFORMS: list[Platform] = [Platform.SENSOR] | PLATFORMS: list[Platform] = [Platform.SENSOR] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: |  | ||||||
|     """Set up WAQI.""" |  | ||||||
|  |  | ||||||
|     await async_migrate_integration(hass) |  | ||||||
|     return True |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_setup_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool: | async def async_setup_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool: | ||||||
|     """Set up World Air Quality Index (WAQI) from a config entry.""" |     """Set up World Air Quality Index (WAQI) from a config entry.""" | ||||||
|  |  | ||||||
|     client = WAQIClient(session=async_get_clientsession(hass)) |     client = WAQIClient(session=async_get_clientsession(hass)) | ||||||
|     client.authenticate(entry.data[CONF_API_KEY]) |     client.authenticate(entry.data[CONF_API_KEY]) | ||||||
|  |  | ||||||
|     entry.runtime_data = {} |     waqi_coordinator = WAQIDataUpdateCoordinator(hass, entry, client) | ||||||
|  |     await waqi_coordinator.async_config_entry_first_refresh() | ||||||
|     for subentry in entry.subentries.values(): |     entry.runtime_data = waqi_coordinator | ||||||
|         if subentry.subentry_type != SUBENTRY_TYPE_STATION: |  | ||||||
|             continue |  | ||||||
|  |  | ||||||
|         # Create a coordinator for each station subentry |  | ||||||
|         coordinator = WAQIDataUpdateCoordinator(hass, entry, subentry, client) |  | ||||||
|         await coordinator.async_config_entry_first_refresh() |  | ||||||
|         entry.runtime_data[subentry.subentry_id] = coordinator |  | ||||||
|  |  | ||||||
|     entry.async_on_unload(entry.add_update_listener(async_update_entry)) |  | ||||||
|  |  | ||||||
|     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) |     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||||||
|  |  | ||||||
|     return True |     return True | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_update_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> None: |  | ||||||
|     """Update entry.""" |  | ||||||
|     await hass.config_entries.async_reload(entry.entry_id) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_unload_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool: | async def async_unload_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool: | ||||||
|     """Unload a config entry.""" |     """Unload a config entry.""" | ||||||
|     return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |     return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_migrate_integration(hass: HomeAssistant) -> None: |  | ||||||
|     """Migrate integration entry structure to subentries.""" |  | ||||||
|  |  | ||||||
|     # Make sure we get enabled config entries first |  | ||||||
|     entries = sorted( |  | ||||||
|         hass.config_entries.async_entries(DOMAIN), |  | ||||||
|         key=lambda e: e.disabled_by is not None, |  | ||||||
|     ) |  | ||||||
|     if not any(entry.version == 1 for entry in entries): |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {} |  | ||||||
|     entity_registry = er.async_get(hass) |  | ||||||
|     device_registry = dr.async_get(hass) |  | ||||||
|  |  | ||||||
|     for entry in entries: |  | ||||||
|         subentry = ConfigSubentry( |  | ||||||
|             data=MappingProxyType( |  | ||||||
|                 {CONF_STATION_NUMBER: entry.data[CONF_STATION_NUMBER]} |  | ||||||
|             ), |  | ||||||
|             subentry_type="station", |  | ||||||
|             title=entry.title, |  | ||||||
|             unique_id=entry.unique_id, |  | ||||||
|         ) |  | ||||||
|         if entry.data[CONF_API_KEY] not in api_keys_entries: |  | ||||||
|             all_disabled = all( |  | ||||||
|                 e.disabled_by is not None |  | ||||||
|                 for e in entries |  | ||||||
|                 if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY] |  | ||||||
|             ) |  | ||||||
|             api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled) |  | ||||||
|  |  | ||||||
|         parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]] |  | ||||||
|  |  | ||||||
|         hass.config_entries.async_add_subentry(parent_entry, subentry) |  | ||||||
|  |  | ||||||
|         entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id) |  | ||||||
|         if TYPE_CHECKING: |  | ||||||
|             assert entry.unique_id is not None |  | ||||||
|         device = device_registry.async_get_device( |  | ||||||
|             identifiers={(DOMAIN, entry.unique_id)} |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         for entity_entry in entities: |  | ||||||
|             entity_disabled_by = entity_entry.disabled_by |  | ||||||
|             if ( |  | ||||||
|                 entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY |  | ||||||
|                 and not all_disabled |  | ||||||
|             ): |  | ||||||
|                 # Device and entity registries don't update the disabled_by flag |  | ||||||
|                 # when moving a device or entity from one config entry to another, |  | ||||||
|                 # so we need to do it manually. |  | ||||||
|                 entity_disabled_by = ( |  | ||||||
|                     er.RegistryEntryDisabler.DEVICE |  | ||||||
|                     if device |  | ||||||
|                     else er.RegistryEntryDisabler.USER |  | ||||||
|                 ) |  | ||||||
|             entity_registry.async_update_entity( |  | ||||||
|                 entity_entry.entity_id, |  | ||||||
|                 config_entry_id=parent_entry.entry_id, |  | ||||||
|                 config_subentry_id=subentry.subentry_id, |  | ||||||
|                 disabled_by=entity_disabled_by, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if device is not None: |  | ||||||
|             # Device and entity registries don't update the disabled_by flag when |  | ||||||
|             # moving a device or entity from one config entry to another, so we |  | ||||||
|             # need to do it manually. |  | ||||||
|             device_disabled_by = device.disabled_by |  | ||||||
|             if ( |  | ||||||
|                 device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY |  | ||||||
|                 and not all_disabled |  | ||||||
|             ): |  | ||||||
|                 device_disabled_by = dr.DeviceEntryDisabler.USER |  | ||||||
|             device_registry.async_update_device( |  | ||||||
|                 device.id, |  | ||||||
|                 disabled_by=device_disabled_by, |  | ||||||
|                 add_config_subentry_id=subentry.subentry_id, |  | ||||||
|                 add_config_entry_id=parent_entry.entry_id, |  | ||||||
|             ) |  | ||||||
|             if parent_entry.entry_id != entry.entry_id: |  | ||||||
|                 device_registry.async_update_device( |  | ||||||
|                     device.id, |  | ||||||
|                     remove_config_entry_id=entry.entry_id, |  | ||||||
|                 ) |  | ||||||
|             else: |  | ||||||
|                 device_registry.async_update_device( |  | ||||||
|                     device.id, |  | ||||||
|                     remove_config_entry_id=entry.entry_id, |  | ||||||
|                     remove_config_subentry_id=None, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         if parent_entry.entry_id != entry.entry_id: |  | ||||||
|             await hass.config_entries.async_remove(entry.entry_id) |  | ||||||
|         else: |  | ||||||
|             hass.config_entries.async_update_entry( |  | ||||||
|                 entry, |  | ||||||
|                 title="WAQI", |  | ||||||
|                 version=2, |  | ||||||
|                 data={CONF_API_KEY: entry.data[CONF_API_KEY]}, |  | ||||||
|                 unique_id=None, |  | ||||||
|             ) |  | ||||||
|   | |||||||
| @@ -13,24 +13,22 @@ from aiowaqi import ( | |||||||
| ) | ) | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
| from homeassistant.config_entries import ( | from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||||||
|     ConfigEntry, |  | ||||||
|     ConfigFlow, |  | ||||||
|     ConfigFlowResult, |  | ||||||
|     ConfigSubentryFlow, |  | ||||||
|     SubentryFlowResult, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
|     CONF_API_KEY, |     CONF_API_KEY, | ||||||
|     CONF_LATITUDE, |     CONF_LATITUDE, | ||||||
|     CONF_LOCATION, |     CONF_LOCATION, | ||||||
|     CONF_LONGITUDE, |     CONF_LONGITUDE, | ||||||
|  |     CONF_METHOD, | ||||||
| ) | ) | ||||||
| from homeassistant.core import callback |  | ||||||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||||||
| from homeassistant.helpers.selector import LocationSelector | from homeassistant.helpers.selector import ( | ||||||
|  |     LocationSelector, | ||||||
|  |     SelectSelector, | ||||||
|  |     SelectSelectorConfig, | ||||||
|  | ) | ||||||
|  |  | ||||||
| from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION | from .const import CONF_STATION_NUMBER, DOMAIN | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -56,15 +54,11 @@ async def get_by_station_number( | |||||||
| class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): | class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): | ||||||
|     """Handle a config flow for World Air Quality Index (WAQI).""" |     """Handle a config flow for World Air Quality Index (WAQI).""" | ||||||
|  |  | ||||||
|     VERSION = 2 |     VERSION = 1 | ||||||
|  |  | ||||||
|     @classmethod |     def __init__(self) -> None: | ||||||
|     @callback |         """Initialize config flow.""" | ||||||
|     def async_get_supported_subentry_types( |         self.data: dict[str, Any] = {} | ||||||
|         cls, config_entry: ConfigEntry |  | ||||||
|     ) -> dict[str, type[ConfigSubentryFlow]]: |  | ||||||
|         """Return subentries supported by this handler.""" |  | ||||||
|         return {SUBENTRY_TYPE_STATION: StationFlowHandler} |  | ||||||
|  |  | ||||||
|     async def async_step_user( |     async def async_step_user( | ||||||
|         self, user_input: dict[str, Any] | None = None |         self, user_input: dict[str, Any] | None = None | ||||||
| @@ -72,7 +66,6 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): | |||||||
|         """Handle the initial step.""" |         """Handle the initial step.""" | ||||||
|         errors: dict[str, str] = {} |         errors: dict[str, str] = {} | ||||||
|         if user_input is not None: |         if user_input is not None: | ||||||
|             self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]}) |  | ||||||
|             client = WAQIClient(session=async_get_clientsession(self.hass)) |             client = WAQIClient(session=async_get_clientsession(self.hass)) | ||||||
|             client.authenticate(user_input[CONF_API_KEY]) |             client.authenticate(user_input[CONF_API_KEY]) | ||||||
|             try: |             try: | ||||||
| @@ -85,40 +78,35 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): | |||||||
|                 _LOGGER.exception("Unexpected exception") |                 _LOGGER.exception("Unexpected exception") | ||||||
|                 errors["base"] = "unknown" |                 errors["base"] = "unknown" | ||||||
|             else: |             else: | ||||||
|                 return self.async_create_entry( |                 self.data = user_input | ||||||
|                     title="World Air Quality Index", |                 if user_input[CONF_METHOD] == CONF_MAP: | ||||||
|                     data={ |                     return await self.async_step_map() | ||||||
|                         CONF_API_KEY: user_input[CONF_API_KEY], |                 return await self.async_step_station_number() | ||||||
|                     }, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         return self.async_show_form( |         return self.async_show_form( | ||||||
|             step_id="user", |             step_id="user", | ||||||
|             data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), |             data_schema=vol.Schema( | ||||||
|  |                 { | ||||||
|  |                     vol.Required(CONF_API_KEY): str, | ||||||
|  |                     vol.Required(CONF_METHOD): SelectSelector( | ||||||
|  |                         SelectSelectorConfig( | ||||||
|  |                             options=[CONF_MAP, CONF_STATION_NUMBER], | ||||||
|  |                             translation_key="method", | ||||||
|  |                         ) | ||||||
|  |                     ), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|             errors=errors, |             errors=errors, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class StationFlowHandler(ConfigSubentryFlow): |  | ||||||
|     """Handle subentry flow.""" |  | ||||||
|  |  | ||||||
|     async def async_step_user( |  | ||||||
|         self, user_input: dict[str, Any] | None = None |  | ||||||
|     ) -> SubentryFlowResult: |  | ||||||
|         """User flow to create a sensor subentry.""" |  | ||||||
|         return self.async_show_menu( |  | ||||||
|             step_id="user", |  | ||||||
|             menu_options=["map", "station_number"], |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     async def async_step_map( |     async def async_step_map( | ||||||
|         self, user_input: dict[str, Any] | None = None |         self, user_input: dict[str, Any] | None = None | ||||||
|     ) -> SubentryFlowResult: |     ) -> ConfigFlowResult: | ||||||
|         """Add measuring station via map.""" |         """Add measuring station via map.""" | ||||||
|         errors: dict[str, str] = {} |         errors: dict[str, str] = {} | ||||||
|         if user_input is not None: |         if user_input is not None: | ||||||
|             client = WAQIClient(session=async_get_clientsession(self.hass)) |             client = WAQIClient(session=async_get_clientsession(self.hass)) | ||||||
|             client.authenticate(self._get_entry().data[CONF_API_KEY]) |             client.authenticate(self.data[CONF_API_KEY]) | ||||||
|             try: |             try: | ||||||
|                 measuring_station = await client.get_by_coordinates( |                 measuring_station = await client.get_by_coordinates( | ||||||
|                     user_input[CONF_LOCATION][CONF_LATITUDE], |                     user_input[CONF_LOCATION][CONF_LATITUDE], | ||||||
| @@ -136,7 +124,9 @@ class StationFlowHandler(ConfigSubentryFlow): | |||||||
|             data_schema=self.add_suggested_values_to_schema( |             data_schema=self.add_suggested_values_to_schema( | ||||||
|                 vol.Schema( |                 vol.Schema( | ||||||
|                     { |                     { | ||||||
|                         vol.Required(CONF_LOCATION): LocationSelector(), |                         vol.Required( | ||||||
|  |                             CONF_LOCATION, | ||||||
|  |                         ): LocationSelector(), | ||||||
|                     } |                     } | ||||||
|                 ), |                 ), | ||||||
|                 { |                 { | ||||||
| @@ -151,12 +141,12 @@ class StationFlowHandler(ConfigSubentryFlow): | |||||||
|  |  | ||||||
|     async def async_step_station_number( |     async def async_step_station_number( | ||||||
|         self, user_input: dict[str, Any] | None = None |         self, user_input: dict[str, Any] | None = None | ||||||
|     ) -> SubentryFlowResult: |     ) -> ConfigFlowResult: | ||||||
|         """Add measuring station via station number.""" |         """Add measuring station via station number.""" | ||||||
|         errors: dict[str, str] = {} |         errors: dict[str, str] = {} | ||||||
|         if user_input is not None: |         if user_input is not None: | ||||||
|             client = WAQIClient(session=async_get_clientsession(self.hass)) |             client = WAQIClient(session=async_get_clientsession(self.hass)) | ||||||
|             client.authenticate(self._get_entry().data[CONF_API_KEY]) |             client.authenticate(self.data[CONF_API_KEY]) | ||||||
|             station_number = user_input[CONF_STATION_NUMBER] |             station_number = user_input[CONF_STATION_NUMBER] | ||||||
|             measuring_station, errors = await get_by_station_number( |             measuring_station, errors = await get_by_station_number( | ||||||
|                 client, abs(station_number) |                 client, abs(station_number) | ||||||
| @@ -170,22 +160,25 @@ class StationFlowHandler(ConfigSubentryFlow): | |||||||
|                 return await self._async_create_entry(measuring_station) |                 return await self._async_create_entry(measuring_station) | ||||||
|         return self.async_show_form( |         return self.async_show_form( | ||||||
|             step_id=CONF_STATION_NUMBER, |             step_id=CONF_STATION_NUMBER, | ||||||
|             data_schema=vol.Schema({vol.Required(CONF_STATION_NUMBER): int}), |             data_schema=vol.Schema( | ||||||
|  |                 { | ||||||
|  |                     vol.Required( | ||||||
|  |                         CONF_STATION_NUMBER, | ||||||
|  |                     ): int, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|             errors=errors, |             errors=errors, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     async def _async_create_entry( |     async def _async_create_entry( | ||||||
|         self, measuring_station: WAQIAirQuality |         self, measuring_station: WAQIAirQuality | ||||||
|     ) -> SubentryFlowResult: |     ) -> ConfigFlowResult: | ||||||
|         station_id = str(measuring_station.station_id) |         await self.async_set_unique_id(str(measuring_station.station_id)) | ||||||
|         for entry in self.hass.config_entries.async_entries(DOMAIN): |         self._abort_if_unique_id_configured() | ||||||
|             for subentry in entry.subentries.values(): |  | ||||||
|                 if subentry.unique_id == station_id: |  | ||||||
|                     return self.async_abort(reason="already_configured") |  | ||||||
|         return self.async_create_entry( |         return self.async_create_entry( | ||||||
|             title=measuring_station.city.name, |             title=measuring_station.city.name, | ||||||
|             data={ |             data={ | ||||||
|  |                 CONF_API_KEY: self.data[CONF_API_KEY], | ||||||
|                 CONF_STATION_NUMBER: measuring_station.station_id, |                 CONF_STATION_NUMBER: measuring_station.station_id, | ||||||
|             }, |             }, | ||||||
|             unique_id=station_id, |  | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -8,4 +8,4 @@ LOGGER = logging.getLogger(__package__) | |||||||
|  |  | ||||||
| CONF_STATION_NUMBER = "station_number" | CONF_STATION_NUMBER = "station_number" | ||||||
|  |  | ||||||
| SUBENTRY_TYPE_STATION = "station" | ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=waqi"} | ||||||
|   | |||||||
| @@ -6,13 +6,13 @@ from datetime import timedelta | |||||||
|  |  | ||||||
| from aiowaqi import WAQIAirQuality, WAQIClient, WAQIError | from aiowaqi import WAQIAirQuality, WAQIClient, WAQIError | ||||||
|  |  | ||||||
| from homeassistant.config_entries import ConfigEntry, ConfigSubentry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||||||
|  |  | ||||||
| from .const import CONF_STATION_NUMBER, LOGGER | from .const import CONF_STATION_NUMBER, DOMAIN, LOGGER | ||||||
|  |  | ||||||
| type WAQIConfigEntry = ConfigEntry[dict[str, WAQIDataUpdateCoordinator]] | type WAQIConfigEntry = ConfigEntry[WAQIDataUpdateCoordinator] | ||||||
|  |  | ||||||
|  |  | ||||||
| class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]): | class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]): | ||||||
| @@ -21,27 +21,22 @@ class WAQIDataUpdateCoordinator(DataUpdateCoordinator[WAQIAirQuality]): | |||||||
|     config_entry: WAQIConfigEntry |     config_entry: WAQIConfigEntry | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, hass: HomeAssistant, config_entry: WAQIConfigEntry, client: WAQIClient | ||||||
|         hass: HomeAssistant, |  | ||||||
|         config_entry: WAQIConfigEntry, |  | ||||||
|         subentry: ConfigSubentry, |  | ||||||
|         client: WAQIClient, |  | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Initialize the WAQI data coordinator.""" |         """Initialize the WAQI data coordinator.""" | ||||||
|         super().__init__( |         super().__init__( | ||||||
|             hass, |             hass, | ||||||
|             LOGGER, |             LOGGER, | ||||||
|             config_entry=config_entry, |             config_entry=config_entry, | ||||||
|             name=subentry.title, |             name=DOMAIN, | ||||||
|             update_interval=timedelta(minutes=5), |             update_interval=timedelta(minutes=5), | ||||||
|         ) |         ) | ||||||
|         self._client = client |         self._client = client | ||||||
|         self.subentry = subentry |  | ||||||
|  |  | ||||||
|     async def _async_update_data(self) -> WAQIAirQuality: |     async def _async_update_data(self) -> WAQIAirQuality: | ||||||
|         try: |         try: | ||||||
|             return await self._client.get_by_station_number( |             return await self._client.get_by_station_number( | ||||||
|                 self.subentry.data[CONF_STATION_NUMBER] |                 self.config_entry.data[CONF_STATION_NUMBER] | ||||||
|             ) |             ) | ||||||
|         except WAQIError as exc: |         except WAQIError as exc: | ||||||
|             raise UpdateFailed from exc |             raise UpdateFailed from exc | ||||||
|   | |||||||
| @@ -130,15 +130,12 @@ async def async_setup_entry( | |||||||
|     async_add_entities: AddConfigEntryEntitiesCallback, |     async_add_entities: AddConfigEntryEntitiesCallback, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Set up the WAQI sensor.""" |     """Set up the WAQI sensor.""" | ||||||
|     for subentry_id, coordinator in entry.runtime_data.items(): |     coordinator = entry.runtime_data | ||||||
|         async_add_entities( |     async_add_entities( | ||||||
|             ( |         WaqiSensor(coordinator, sensor) | ||||||
|                 WaqiSensor(coordinator, sensor) |         for sensor in SENSORS | ||||||
|                 for sensor in SENSORS |         if sensor.available_fn(coordinator.data) | ||||||
|                 if sensor.available_fn(coordinator.data) |     ) | ||||||
|             ), |  | ||||||
|             config_subentry_id=subentry_id, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity): | class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity): | ||||||
|   | |||||||
| @@ -3,10 +3,19 @@ | |||||||
|     "step": { |     "step": { | ||||||
|       "user": { |       "user": { | ||||||
|         "data": { |         "data": { | ||||||
|           "api_key": "[%key:common::config_flow::data::api_key%]" |           "api_key": "[%key:common::config_flow::data::api_key%]", | ||||||
|         }, |           "method": "How do you want to select a measuring station?" | ||||||
|         "data_description": { |         } | ||||||
|           "api_key": "API key for the World Air Quality Index" |       }, | ||||||
|  |       "map": { | ||||||
|  |         "description": "Select a location to get the closest measuring station.", | ||||||
|  |         "data": { | ||||||
|  |           "location": "[%key:common::config_flow::data::location%]" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "station_number": { | ||||||
|  |         "data": { | ||||||
|  |           "station_number": "Measuring station number" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
| @@ -16,44 +25,15 @@ | |||||||
|       "unknown": "[%key:common::config_flow::error::unknown%]" |       "unknown": "[%key:common::config_flow::error::unknown%]" | ||||||
|     }, |     }, | ||||||
|     "abort": { |     "abort": { | ||||||
|       "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" |       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "config_subentries": { |   "selector": { | ||||||
|     "station": { |     "method": { | ||||||
|       "step": { |       "options": { | ||||||
|         "user": { |         "map": "Select nearest from point on the map", | ||||||
|           "title": "Add measuring station", |         "station_number": "Enter a station number" | ||||||
|           "description": "How do you want to select a measuring station?", |       } | ||||||
|           "menu_options": { |  | ||||||
|             "map": "[%key:common::config_flow::data::location%]", |  | ||||||
|             "station_number": "Measuring station number" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         "map": { |  | ||||||
|           "data": { |  | ||||||
|             "location": "[%key:common::config_flow::data::location%]" |  | ||||||
|           }, |  | ||||||
|           "data_description": { |  | ||||||
|             "location": "The location to get the nearest measuring station from" |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         "station_number": { |  | ||||||
|           "data": { |  | ||||||
|             "station_number": "[%key:component::waqi::config_subentries::station::step::user::menu_options::station_number%]" |  | ||||||
|           }, |  | ||||||
|           "data_description": { |  | ||||||
|             "station_number": "The number of the measuring station" |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       "abort": { |  | ||||||
|         "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" |  | ||||||
|       }, |  | ||||||
|       "initiate_flow": { |  | ||||||
|         "user": "Add measuring station" |  | ||||||
|       }, |  | ||||||
|       "entry_type": "Measuring station" |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "entity": { |   "entity": { | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from __future__ import annotations | |||||||
| from collections.abc import Callable | from collections.abc import Callable | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
|  |  | ||||||
| from homeassistant.components.assist_pipeline import VadSensitivity | from homeassistant.components.assist_pipeline.vad import VadSensitivity | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
| from homeassistant.helpers import entity_registry as er | from homeassistant.helpers import entity_registry as er | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,11 +4,11 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from typing import TYPE_CHECKING, Final | from typing import TYPE_CHECKING, Final | ||||||
|  |  | ||||||
| from homeassistant.components.assist_pipeline import ( | from homeassistant.components.assist_pipeline.select import ( | ||||||
|     AssistPipelineSelect, |     AssistPipelineSelect, | ||||||
|     VadSensitivity, |  | ||||||
|     VadSensitivitySelect, |     VadSensitivitySelect, | ||||||
| ) | ) | ||||||
|  | from homeassistant.components.assist_pipeline.vad import VadSensitivity | ||||||
| from homeassistant.components.select import SelectEntity, SelectEntityDescription | from homeassistant.components.select import SelectEntity, SelectEntityDescription | ||||||
| from homeassistant.config_entries import ConfigEntry | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import EntityCategory | from homeassistant.const import EntityCategory | ||||||
|   | |||||||
| @@ -8,13 +8,14 @@ from xbox.webapi.api.client import XboxLiveClient | |||||||
| from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList | from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList | ||||||
| from xbox.webapi.common.signed_session import SignedSession | from xbox.webapi.common.signed_session import SignedSession | ||||||
|  |  | ||||||
|  | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.const import Platform | from homeassistant.const import Platform | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv | from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv | ||||||
|  |  | ||||||
| from . import api | from . import api | ||||||
| from .const import DOMAIN | from .const import DOMAIN | ||||||
| from .coordinator import XboxConfigEntry, XboxUpdateCoordinator | from .coordinator import XboxUpdateCoordinator | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -28,7 +29,7 @@ PLATFORMS = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool: | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||
|     """Set up xbox from a config entry.""" |     """Set up xbox from a config entry.""" | ||||||
|     implementation = ( |     implementation = ( | ||||||
|         await config_entry_oauth2_flow.async_get_config_entry_implementation( |         await config_entry_oauth2_flow.async_get_config_entry_implementation( | ||||||
| @@ -44,20 +45,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool | |||||||
|     _LOGGER.debug( |     _LOGGER.debug( | ||||||
|         "Found %d consoles: %s", |         "Found %d consoles: %s", | ||||||
|         len(consoles.result), |         len(consoles.result), | ||||||
|         consoles.model_dump(), |         consoles.dict(), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     coordinator = XboxUpdateCoordinator(hass, entry, client, consoles) |     coordinator = XboxUpdateCoordinator(hass, entry, client, consoles) | ||||||
|     await coordinator.async_config_entry_first_refresh() |     await coordinator.async_config_entry_first_refresh() | ||||||
|  |  | ||||||
|     entry.runtime_data = coordinator |     hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { | ||||||
|  |         "client": XboxLiveClient(auth), | ||||||
|  |         "consoles": consoles, | ||||||
|  |         "coordinator": coordinator, | ||||||
|  |     } | ||||||
|  |  | ||||||
|     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) |     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||||||
|  |  | ||||||
|     return True |     return True | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_unload_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool: | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||||||
|     """Unload a config entry.""" |     """Unload a config entry.""" | ||||||
|  |     unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||||||
|  |     if unload_ok: | ||||||
|  |         # Unsub from coordinator updates | ||||||
|  |         hass.data[DOMAIN][entry.entry_id]["sensor_unsub"]() | ||||||
|  |         hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"]() | ||||||
|  |         hass.data[DOMAIN].pop(entry.entry_id) | ||||||
|  |  | ||||||
|     return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |     return unload_ok | ||||||
|   | |||||||
| @@ -5,11 +5,13 @@ from __future__ import annotations | |||||||
| from functools import partial | from functools import partial | ||||||
|  |  | ||||||
| from homeassistant.components.binary_sensor import BinarySensorEntity | from homeassistant.components.binary_sensor import BinarySensorEntity | ||||||
|  | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
| from homeassistant.helpers import entity_registry as er | from homeassistant.helpers import entity_registry as er | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|  |  | ||||||
| from .coordinator import XboxConfigEntry, XboxUpdateCoordinator | from .const import DOMAIN | ||||||
|  | from .coordinator import XboxUpdateCoordinator | ||||||
| from .entity import XboxBaseEntity | from .entity import XboxBaseEntity | ||||||
|  |  | ||||||
| PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"] | PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"] | ||||||
| @@ -17,16 +19,18 @@ PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"] | |||||||
|  |  | ||||||
| async def async_setup_entry( | async def async_setup_entry( | ||||||
|     hass: HomeAssistant, |     hass: HomeAssistant, | ||||||
|     entry: XboxConfigEntry, |     entry: ConfigEntry, | ||||||
|     async_add_entities: AddConfigEntryEntitiesCallback, |     async_add_entities: AddConfigEntryEntitiesCallback, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Set up Xbox Live friends.""" |     """Set up Xbox Live friends.""" | ||||||
|     coordinator = entry.runtime_data |     coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ | ||||||
|  |         "coordinator" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     update_friends = partial(async_update_friends, coordinator, {}, async_add_entities) |     update_friends = partial(async_update_friends, coordinator, {}, async_add_entities) | ||||||
|  |  | ||||||
|     entry.async_on_unload(coordinator.async_add_listener(update_friends)) |     unsub = coordinator.async_add_listener(update_friends) | ||||||
|  |     hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"] = unsub | ||||||
|     update_friends() |     update_friends() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,8 +28,6 @@ from .const import DOMAIN | |||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| type XboxConfigEntry = ConfigEntry[XboxUpdateCoordinator] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class ConsoleData: | class ConsoleData: | ||||||
|   | |||||||
| @@ -5,11 +5,13 @@ from __future__ import annotations | |||||||
| import re | import re | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
|  | from xbox.webapi.api.client import XboxLiveClient | ||||||
| from xbox.webapi.api.provider.catalog.models import Image | from xbox.webapi.api.provider.catalog.models import Image | ||||||
| from xbox.webapi.api.provider.smartglass.models import ( | from xbox.webapi.api.provider.smartglass.models import ( | ||||||
|     PlaybackState, |     PlaybackState, | ||||||
|     PowerState, |     PowerState, | ||||||
|     SmartglassConsole, |     SmartglassConsole, | ||||||
|  |     SmartglassConsoleList, | ||||||
|     VolumeDirection, |     VolumeDirection, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -19,6 +21,7 @@ from homeassistant.components.media_player import ( | |||||||
|     MediaPlayerState, |     MediaPlayerState, | ||||||
|     MediaType, |     MediaType, | ||||||
| ) | ) | ||||||
|  | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.device_registry import DeviceInfo | from homeassistant.helpers.device_registry import DeviceInfo | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
| @@ -26,7 +29,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity | |||||||
|  |  | ||||||
| from .browse_media import build_item_response | from .browse_media import build_item_response | ||||||
| from .const import DOMAIN | from .const import DOMAIN | ||||||
| from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator | from .coordinator import ConsoleData, XboxUpdateCoordinator | ||||||
|  |  | ||||||
| SUPPORT_XBOX = ( | SUPPORT_XBOX = ( | ||||||
|     MediaPlayerEntityFeature.TURN_ON |     MediaPlayerEntityFeature.TURN_ON | ||||||
| @@ -54,18 +57,18 @@ XBOX_STATE_MAP: dict[PlaybackState | PowerState, MediaPlayerState | None] = { | |||||||
|  |  | ||||||
| async def async_setup_entry( | async def async_setup_entry( | ||||||
|     hass: HomeAssistant, |     hass: HomeAssistant, | ||||||
|     entry: XboxConfigEntry, |     entry: ConfigEntry, | ||||||
|     async_add_entities: AddConfigEntryEntitiesCallback, |     async_add_entities: AddConfigEntryEntitiesCallback, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Set up Xbox media_player from a config entry.""" |     """Set up Xbox media_player from a config entry.""" | ||||||
|  |     client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"] | ||||||
|     coordinator = entry.runtime_data |     consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"] | ||||||
|  |     coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ | ||||||
|  |         "coordinator" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     async_add_entities( |     async_add_entities( | ||||||
|         [ |         [XboxMediaPlayer(client, console, coordinator) for console in consoles.result] | ||||||
|             XboxMediaPlayer(console, coordinator) |  | ||||||
|             for console in coordinator.consoles.result |  | ||||||
|         ] |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -74,13 +77,14 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|  |         client: XboxLiveClient, | ||||||
|         console: SmartglassConsole, |         console: SmartglassConsole, | ||||||
|         coordinator: XboxUpdateCoordinator, |         coordinator: XboxUpdateCoordinator, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Initialize the Xbox Media Player.""" |         """Initialize the Xbox Media Player.""" | ||||||
|         super().__init__(coordinator) |         super().__init__(coordinator) | ||||||
|         self.client = coordinator.client |         self.client: XboxLiveClient = client | ||||||
|         self._console = console |         self._console: SmartglassConsole = console | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def name(self): |     def name(self): | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ from homeassistant.util import dt as dt_util | |||||||
|  |  | ||||||
| from .browse_media import _find_media_image | from .browse_media import _find_media_image | ||||||
| from .const import DOMAIN | from .const import DOMAIN | ||||||
| from .coordinator import XboxConfigEntry |  | ||||||
|  |  | ||||||
| MIME_TYPE_MAP = { | MIME_TYPE_MAP = { | ||||||
|     "gameclips": "video/mp4", |     "gameclips": "video/mp4", | ||||||
| @@ -39,8 +38,8 @@ MEDIA_CLASS_MAP = { | |||||||
|  |  | ||||||
| async def async_get_media_source(hass: HomeAssistant): | async def async_get_media_source(hass: HomeAssistant): | ||||||
|     """Set up Xbox media source.""" |     """Set up Xbox media source.""" | ||||||
|     entry: XboxConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] |     entry = hass.config_entries.async_entries(DOMAIN)[0] | ||||||
|     client = entry.runtime_data.client |     client = hass.data[DOMAIN][entry.entry_id]["client"] | ||||||
|     return XboxSource(hass, client) |     return XboxSource(hass, client) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,10 +7,12 @@ from collections.abc import Iterable | |||||||
| import re | import re | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
|  | from xbox.webapi.api.client import XboxLiveClient | ||||||
| from xbox.webapi.api.provider.smartglass.models import ( | from xbox.webapi.api.provider.smartglass.models import ( | ||||||
|     InputKeyType, |     InputKeyType, | ||||||
|     PowerState, |     PowerState, | ||||||
|     SmartglassConsole, |     SmartglassConsole, | ||||||
|  |     SmartglassConsoleList, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from homeassistant.components.remote import ( | from homeassistant.components.remote import ( | ||||||
| @@ -19,25 +21,30 @@ from homeassistant.components.remote import ( | |||||||
|     DEFAULT_DELAY_SECS, |     DEFAULT_DELAY_SECS, | ||||||
|     RemoteEntity, |     RemoteEntity, | ||||||
| ) | ) | ||||||
|  | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant | from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.device_registry import DeviceInfo | from homeassistant.helpers.device_registry import DeviceInfo | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||||||
|  |  | ||||||
| from .const import DOMAIN | from .const import DOMAIN | ||||||
| from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator | from .coordinator import ConsoleData, XboxUpdateCoordinator | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_setup_entry( | async def async_setup_entry( | ||||||
|     hass: HomeAssistant, |     hass: HomeAssistant, | ||||||
|     entry: XboxConfigEntry, |     entry: ConfigEntry, | ||||||
|     async_add_entities: AddConfigEntryEntitiesCallback, |     async_add_entities: AddConfigEntryEntitiesCallback, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Set up Xbox media_player from a config entry.""" |     """Set up Xbox media_player from a config entry.""" | ||||||
|     coordinator = entry.runtime_data |     client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"] | ||||||
|  |     consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"] | ||||||
|  |     coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ | ||||||
|  |         "coordinator" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     async_add_entities( |     async_add_entities( | ||||||
|         [XboxRemote(console, coordinator) for console in coordinator.consoles.result] |         [XboxRemote(client, console, coordinator) for console in consoles.result] | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -46,13 +53,14 @@ class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity): | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|  |         client: XboxLiveClient, | ||||||
|         console: SmartglassConsole, |         console: SmartglassConsole, | ||||||
|         coordinator: XboxUpdateCoordinator, |         coordinator: XboxUpdateCoordinator, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Initialize the Xbox Media Player.""" |         """Initialize the Xbox Media Player.""" | ||||||
|         super().__init__(coordinator) |         super().__init__(coordinator) | ||||||
|         self.client = coordinator.client |         self.client: XboxLiveClient = client | ||||||
|         self._console = console |         self._console: SmartglassConsole = console | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def name(self): |     def name(self): | ||||||
|   | |||||||
| @@ -5,11 +5,13 @@ from __future__ import annotations | |||||||
| from functools import partial | from functools import partial | ||||||
|  |  | ||||||
| from homeassistant.components.sensor import SensorEntity | from homeassistant.components.sensor import SensorEntity | ||||||
|  | from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant, callback | from homeassistant.core import HomeAssistant, callback | ||||||
| from homeassistant.helpers import entity_registry as er | from homeassistant.helpers import entity_registry as er | ||||||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||||||
|  |  | ||||||
| from .coordinator import XboxConfigEntry, XboxUpdateCoordinator | from .const import DOMAIN | ||||||
|  | from .coordinator import XboxUpdateCoordinator | ||||||
| from .entity import XboxBaseEntity | from .entity import XboxBaseEntity | ||||||
|  |  | ||||||
| SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"] | SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"] | ||||||
| @@ -17,15 +19,18 @@ SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"] | |||||||
|  |  | ||||||
| async def async_setup_entry( | async def async_setup_entry( | ||||||
|     hass: HomeAssistant, |     hass: HomeAssistant, | ||||||
|     config_entry: XboxConfigEntry, |     config_entry: ConfigEntry, | ||||||
|     async_add_entities: AddConfigEntryEntitiesCallback, |     async_add_entities: AddConfigEntryEntitiesCallback, | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Set up Xbox Live friends.""" |     """Set up Xbox Live friends.""" | ||||||
|     coordinator = config_entry.runtime_data |     coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ | ||||||
|  |         "coordinator" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     update_friends = partial(async_update_friends, coordinator, {}, async_add_entities) |     update_friends = partial(async_update_friends, coordinator, {}, async_add_entities) | ||||||
|  |  | ||||||
|     config_entry.async_on_unload(coordinator.async_add_listener(update_friends)) |     unsub = coordinator.async_add_listener(update_friends) | ||||||
|  |     hass.data[DOMAIN][config_entry.entry_id]["sensor_unsub"] = unsub | ||||||
|     update_friends() |     update_friends() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ ACTION_PARAMETERS_CACHE: HassKey[ | |||||||
|  |  | ||||||
| LLM_API_ASSIST = "assist" | LLM_API_ASSIST = "assist" | ||||||
|  |  | ||||||
| DATE_TIME_PROMPT = ( | BASE_PROMPT = ( | ||||||
|     'Current time is {{ now().strftime("%H:%M:%S") }}. ' |     'Current time is {{ now().strftime("%H:%M:%S") }}. ' | ||||||
|     'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n' |     'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n' | ||||||
| ) | ) | ||||||
| @@ -592,8 +592,6 @@ class AssistAPI(API): | |||||||
|             for intent_handler in intent_handlers |             for intent_handler in intent_handlers | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         tools.append(GetDateTimeTool()) |  | ||||||
|  |  | ||||||
|         if exposed_entities: |         if exposed_entities: | ||||||
|             if exposed_entities[CALENDAR_DOMAIN]: |             if exposed_entities[CALENDAR_DOMAIN]: | ||||||
|                 names = [] |                 names = [] | ||||||
| @@ -1183,29 +1181,3 @@ class GetLiveContextTool(Tool): | |||||||
|             "success": True, |             "success": True, | ||||||
|             "result": "\n".join(prompt), |             "result": "\n".join(prompt), | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class GetDateTimeTool(Tool): |  | ||||||
|     """Tool for getting the current date and time.""" |  | ||||||
|  |  | ||||||
|     name = "GetDateTime" |  | ||||||
|     description = "Provides the current date and time." |  | ||||||
|  |  | ||||||
|     async def async_call( |  | ||||||
|         self, |  | ||||||
|         hass: HomeAssistant, |  | ||||||
|         tool_input: ToolInput, |  | ||||||
|         llm_context: LLMContext, |  | ||||||
|     ) -> JsonObjectType: |  | ||||||
|         """Get the current date and time.""" |  | ||||||
|         now = dt_util.now() |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|             "success": True, |  | ||||||
|             "result": { |  | ||||||
|                 "date": now.strftime("%Y-%m-%d"), |  | ||||||
|                 "time": now.strftime("%H:%M:%S"), |  | ||||||
|                 "timezone": now.strftime("%Z"), |  | ||||||
|                 "weekday": now.strftime("%A"), |  | ||||||
|             }, |  | ||||||
|         } |  | ||||||
|   | |||||||
| @@ -16,9 +16,7 @@ from homeassistant.components.sensor import ( | |||||||
|     SensorDeviceClass, |     SensorDeviceClass, | ||||||
|     SensorEntity, |     SensorEntity, | ||||||
| ) | ) | ||||||
| from homeassistant.components.sensor.helpers import (  # pylint: disable=hass-component-root-import | from homeassistant.components.sensor.helpers import async_parse_date_datetime | ||||||
|     async_parse_date_datetime, |  | ||||||
| ) |  | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
|     ATTR_ENTITY_PICTURE, |     ATTR_ENTITY_PICTURE, | ||||||
|     ATTR_FRIENDLY_NAME, |     ATTR_FRIENDLY_NAME, | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user