mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +00:00
2024.3.1 (#113249)
This commit is contained in:
commit
f7972ce9b2
@ -639,6 +639,12 @@ omit =
|
||||
homeassistant/components/izone/climate.py
|
||||
homeassistant/components/izone/discovery.py
|
||||
homeassistant/components/joaoapps_join/*
|
||||
homeassistant/components/juicenet/__init__.py
|
||||
homeassistant/components/juicenet/device.py
|
||||
homeassistant/components/juicenet/entity.py
|
||||
homeassistant/components/juicenet/number.py
|
||||
homeassistant/components/juicenet/sensor.py
|
||||
homeassistant/components/juicenet/switch.py
|
||||
homeassistant/components/justnimbus/coordinator.py
|
||||
homeassistant/components/justnimbus/entity.py
|
||||
homeassistant/components/justnimbus/sensor.py
|
||||
|
@ -669,6 +669,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/jellyfin/ @j-stienstra @ctalkington
|
||||
/homeassistant/components/jewish_calendar/ @tsvi
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/juicenet/ @jesserockz
|
||||
/tests/components/juicenet/ @jesserockz
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
|
@ -27,7 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
address = entry.unique_id
|
||||
|
||||
elevation = hass.config.elevation
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
assert address is not None
|
||||
|
||||
@ -40,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Could not find Airthings device with address {address}"
|
||||
)
|
||||
|
||||
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
|
||||
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
|
||||
|
||||
async def _async_update_method() -> AirthingsDevice:
|
||||
"""Get data from Airthings BLE."""
|
||||
|
@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.6.1"]
|
||||
"requirements": ["airthings-ble==0.7.1"]
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
@ -106,8 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
),
|
||||
"illuminance": SensorEntityDescription(
|
||||
key="illuminance",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
@ -222,7 +220,7 @@ class AirthingsSensor(
|
||||
manufacturer=airthings_device.manufacturer,
|
||||
hw_version=airthings_device.hw_version,
|
||||
sw_version=airthings_device.sw_version,
|
||||
model=airthings_device.model,
|
||||
model=airthings_device.model.name,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -31,9 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
# Ignore services that don't support usage data
|
||||
ignore_types = FETCH_TYPES + ["Hardware"]
|
||||
|
||||
try:
|
||||
await client.login()
|
||||
services = await client.get_services(drop_types=FETCH_TYPES)
|
||||
services = await client.get_services(drop_types=ignore_types)
|
||||
except AuthenticationException as exc:
|
||||
raise ConfigEntryAuthFailed() from exc
|
||||
except ClientError as exc:
|
||||
|
@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==50"],
|
||||
"requirements": ["axis==54"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["boschshcpy"],
|
||||
"requirements": ["boschshcpy==0.2.75"],
|
||||
"requirements": ["boschshcpy==0.2.82"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["bring-api==0.5.5"]
|
||||
"requirements": ["bring-api==0.5.6"]
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
brother = await Brother.create(
|
||||
host, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
)
|
||||
except (ConnectionError, SnmpError) as error:
|
||||
except (ConnectionError, SnmpError, TimeoutError) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
coordinator = BrotherDataUpdateCoordinator(hass, brother)
|
||||
|
@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["brother==4.0.0"],
|
||||
"requirements": ["brother==4.0.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_printer._tcp.local.",
|
||||
|
@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.6.0"]
|
||||
"requirements": ["bthome-ble==3.8.0"]
|
||||
}
|
||||
|
@ -189,6 +189,11 @@ def _validate_rrule(value: Any) -> str:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _empty_as_none(value: str | None) -> str | None:
|
||||
"""Convert any empty string values to None."""
|
||||
return value or None
|
||||
|
||||
|
||||
CREATE_EVENT_SERVICE = "create_event"
|
||||
CREATE_EVENT_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
|
||||
@ -733,7 +738,9 @@ async def handle_calendar_event_create(
|
||||
vol.Required("type"): "calendar/event/delete",
|
||||
vol.Required("entity_id"): cv.entity_id,
|
||||
vol.Required(EVENT_UID): cv.string,
|
||||
vol.Optional(EVENT_RECURRENCE_ID): cv.string,
|
||||
vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
|
||||
vol.All(cv.string, _empty_as_none), None
|
||||
),
|
||||
vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
|
||||
}
|
||||
)
|
||||
@ -777,7 +784,9 @@ async def handle_calendar_event_delete(
|
||||
vol.Required("type"): "calendar/event/update",
|
||||
vol.Required("entity_id"): cv.entity_id,
|
||||
vol.Required(EVENT_UID): cv.string,
|
||||
vol.Optional(EVENT_RECURRENCE_ID): cv.string,
|
||||
vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
|
||||
vol.All(cv.string, _empty_as_none), None
|
||||
),
|
||||
vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
|
||||
vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA,
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ from homeassistant.components.media_source.models import (
|
||||
PlayMedia,
|
||||
)
|
||||
from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@ -25,13 +26,20 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource:
|
||||
return CameraMediaSource(hass)
|
||||
|
||||
|
||||
def _media_source_for_camera(camera: Camera, content_type: str) -> BrowseMediaSource:
|
||||
def _media_source_for_camera(
|
||||
hass: HomeAssistant, camera: Camera, content_type: str
|
||||
) -> BrowseMediaSource:
|
||||
camera_state = hass.states.get(camera.entity_id)
|
||||
title = camera.name
|
||||
if camera_state:
|
||||
title = camera_state.attributes.get(ATTR_FRIENDLY_NAME, camera.name)
|
||||
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=camera.entity_id,
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=content_type,
|
||||
title=camera.name,
|
||||
title=title,
|
||||
thumbnail=f"/api/camera_proxy/{camera.entity_id}",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
@ -89,7 +97,7 @@ class CameraMediaSource(MediaSource):
|
||||
async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None:
|
||||
stream_type = camera.frontend_stream_type
|
||||
if stream_type is None:
|
||||
return _media_source_for_camera(camera, camera.content_type)
|
||||
return _media_source_for_camera(self.hass, camera, camera.content_type)
|
||||
if not can_stream_hls:
|
||||
return None
|
||||
|
||||
@ -97,7 +105,7 @@ class CameraMediaSource(MediaSource):
|
||||
if stream_type != StreamType.HLS and not (await camera.stream_source()):
|
||||
return None
|
||||
|
||||
return _media_source_for_camera(camera, content_type)
|
||||
return _media_source_for_camera(self.hass, camera, content_type)
|
||||
|
||||
component: EntityComponent[Camera] = self.hass.data[DOMAIN]
|
||||
results = await asyncio.gather(
|
||||
|
@ -6,6 +6,7 @@
|
||||
"fan_mode": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"auto": "mdi:fan-auto",
|
||||
"diffuse": "mdi:weather-windy",
|
||||
"focus": "mdi:target",
|
||||
"high": "mdi:speedometer",
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.28"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.12"]
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==0.8.0",
|
||||
"aiodhcpwatcher==0.8.1",
|
||||
"aiodiscover==1.6.1",
|
||||
"cached_ipaddress==0.3.0"
|
||||
]
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.19.1"],
|
||||
"requirements": ["pyenphase==1.19.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240306.0"]
|
||||
"requirements": ["home-assistant-frontend==20240307.0"]
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ from gardena_bluetooth.exceptions import (
|
||||
)
|
||||
from gardena_bluetooth.parse import Characteristic, CharacteristicType
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@ -117,13 +116,7 @@ class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.coordinator.last_update_success
|
||||
and bluetooth.async_address_present(
|
||||
self.hass, self.coordinator.address, True
|
||||
)
|
||||
and self._attr_available
|
||||
)
|
||||
return self.coordinator.last_update_success and self._attr_available
|
||||
|
||||
|
||||
class GardenaBluetoothDescriptorEntity(GardenaBluetoothEntity):
|
||||
|
@ -229,11 +229,11 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
available = super().available
|
||||
sensor_data = getattr(self.coordinator.data, self.entity_description.key)
|
||||
available = super().available and bool(sensor_data)
|
||||
|
||||
# Sometimes the API returns sensor data without indexes
|
||||
if self.entity_description.subkey:
|
||||
if self.entity_description.subkey and available:
|
||||
return available and bool(sensor_data.index)
|
||||
|
||||
return available and bool(sensor_data)
|
||||
return available
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.0"]
|
||||
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"]
|
||||
}
|
||||
|
@ -2706,10 +2706,9 @@ class SensorStateTrait(_Trait):
|
||||
name = TRAIT_SENSOR_STATE
|
||||
commands: list[str] = []
|
||||
|
||||
def _air_quality_description_for_aqi(self, aqi):
|
||||
if aqi is None or aqi.isnumeric() is False:
|
||||
def _air_quality_description_for_aqi(self, aqi: float | None) -> str:
|
||||
if aqi is None or aqi < 0:
|
||||
return "unknown"
|
||||
aqi = int(aqi)
|
||||
if aqi <= 50:
|
||||
return "healthy"
|
||||
if aqi <= 100:
|
||||
@ -2764,11 +2763,17 @@ class SensorStateTrait(_Trait):
|
||||
if device_class is None or data is None:
|
||||
return {}
|
||||
|
||||
sensor_data = {"name": data[0], "rawValue": self.state.state}
|
||||
try:
|
||||
value = float(self.state.state)
|
||||
except ValueError:
|
||||
value = None
|
||||
if self.state.state == STATE_UNKNOWN:
|
||||
value = None
|
||||
sensor_data = {"name": data[0], "rawValue": value}
|
||||
|
||||
if device_class == sensor.SensorDeviceClass.AQI:
|
||||
sensor_data["currentSensorState"] = self._air_quality_description_for_aqi(
|
||||
self.state.state
|
||||
value
|
||||
)
|
||||
|
||||
return {"currentSensorStateData": [sensor_data]}
|
||||
|
@ -19,6 +19,7 @@ ATTR_HOMEASSISTANT = "homeassistant"
|
||||
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE = "homeassistant_exclude_database"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_ISSUES = "issues"
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_PANELS = "panels"
|
||||
ATTR_PASSWORD = "password"
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.const import SERVER_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import ATTR_DISCOVERY, DOMAIN, X_HASS_SOURCE
|
||||
from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
@ -262,10 +262,7 @@ async def async_update_core(
|
||||
@bind_hass
|
||||
@_api_bool
|
||||
async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict:
|
||||
"""Apply a suggestion from supervisor's resolution center.
|
||||
|
||||
The caller of the function should handle HassioAPIError.
|
||||
"""
|
||||
"""Apply a suggestion from supervisor's resolution center."""
|
||||
hassio: HassIO = hass.data[DOMAIN]
|
||||
command = f"/resolution/suggestion/{suggestion_uuid}"
|
||||
return await hassio.send_command(command, timeout=None)
|
||||
@ -576,7 +573,7 @@ class HassIO:
|
||||
raise HassioAPIError()
|
||||
|
||||
try:
|
||||
request = await self.websession.request(
|
||||
response = await self.websession.request(
|
||||
method,
|
||||
joined_url,
|
||||
json=payload,
|
||||
@ -589,14 +586,23 @@ class HassIO:
|
||||
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||
)
|
||||
|
||||
if request.status != HTTPStatus.OK:
|
||||
_LOGGER.error("%s return code %d", command, request.status)
|
||||
if response.status != HTTPStatus.OK:
|
||||
error = await response.json(encoding="utf-8")
|
||||
if error.get(ATTR_RESULT) == "error":
|
||||
raise HassioAPIError(error.get(ATTR_MESSAGE))
|
||||
|
||||
_LOGGER.error(
|
||||
"Request to %s method %s returned with code %d",
|
||||
command,
|
||||
method,
|
||||
response.status,
|
||||
)
|
||||
raise HassioAPIError()
|
||||
|
||||
if return_text:
|
||||
return await request.text(encoding="utf-8")
|
||||
return await response.text(encoding="utf-8")
|
||||
|
||||
return await request.json(encoding="utf-8")
|
||||
return await response.json(encoding="utf-8")
|
||||
|
||||
except TimeoutError:
|
||||
_LOGGER.error("Timeout on %s request", command)
|
||||
|
@ -3,11 +3,13 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
@ -35,6 +37,7 @@ from .const import (
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
@ -302,12 +305,17 @@ class SupervisorIssues:
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues
|
||||
)
|
||||
|
||||
async def update(self) -> None:
|
||||
async def update(self, _: datetime | None = None) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
try:
|
||||
data = await self._client.get_resolution_info()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.error("Failed to update supervisor issues: %r", err)
|
||||
async_call_later(
|
||||
self._hass,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
HassJob(self.update, cancel_on_shutdown=True),
|
||||
)
|
||||
return
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
@ -18,7 +18,7 @@ from .const import (
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .handler import HassioAPIError, async_apply_suggestion
|
||||
from .handler import async_apply_suggestion
|
||||
from .issues import Issue, Suggestion
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"}
|
||||
@ -109,12 +109,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
|
||||
return self._async_form_for_suggestion(suggestion)
|
||||
|
||||
try:
|
||||
await async_apply_suggestion(self.hass, suggestion.uuid)
|
||||
except HassioAPIError:
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
|
||||
return self.async_create_entry(data={})
|
||||
if await async_apply_suggestion(self.hass, suggestion.uuid):
|
||||
return self.async_create_entry(data={})
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
|
||||
@staticmethod
|
||||
def _async_step(
|
||||
|
@ -21,7 +21,6 @@ from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_ENDPOINT,
|
||||
ATTR_METHOD,
|
||||
ATTR_RESULT,
|
||||
ATTR_SESSION_DATA_USER_ID,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_WS_EVENT,
|
||||
@ -131,9 +130,6 @@ async def websocket_supervisor_api(
|
||||
payload=payload,
|
||||
source="core.websocket_api",
|
||||
)
|
||||
|
||||
if result.get(ATTR_RESULT) == "error":
|
||||
raise HassioAPIError(result.get("message"))
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err)
|
||||
connection.send_error(
|
||||
|
@ -57,7 +57,7 @@ async def async_setup_entry(
|
||||
event_type: EventType, resource: BehaviorInstance | LightLevel | Motion
|
||||
) -> None:
|
||||
"""Add entity from Hue resource."""
|
||||
async_add_entities([switch_class(bridge, api.sensors.motion, resource)])
|
||||
async_add_entities([switch_class(bridge, controller, resource)])
|
||||
|
||||
# add all current items in controller
|
||||
for item in controller:
|
||||
|
@ -269,10 +269,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
self._dynamic_mode_active = lights_in_dynamic_mode > 0
|
||||
self._attr_supported_color_modes = supported_color_modes
|
||||
# pick a winner for the current colormode
|
||||
if (
|
||||
lights_with_color_temp_support > 0
|
||||
and lights_in_colortemp_mode == lights_with_color_temp_support
|
||||
):
|
||||
if lights_with_color_temp_support > 0 and lights_in_colortemp_mode > 0:
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif lights_with_color_support > 0:
|
||||
self._attr_color_mode = ColorMode.XY
|
||||
|
@ -2,7 +2,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioautomower.utils import async_structure_token
|
||||
from aioautomower.utils import structure_token
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
@ -27,7 +27,7 @@ class HusqvarnaConfigFlowHandler(
|
||||
"""Create an entry for the flow."""
|
||||
token = data[CONF_TOKEN]
|
||||
user_id = token[CONF_USER_ID]
|
||||
structured_token = await async_structure_token(token[CONF_ACCESS_TOKEN])
|
||||
structured_token = structure_token(token[CONF_ACCESS_TOKEN])
|
||||
first_name = structured_token.user.first_name
|
||||
last_name = structured_token.user.last_name
|
||||
await self.async_set_unique_id(user_id)
|
||||
|
@ -6,5 +6,6 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["aioautomower==2024.2.10"]
|
||||
"loggers": ["aioautomower"],
|
||||
"requirements": ["aioautomower==2024.3.0"]
|
||||
}
|
||||
|
@ -52,7 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="deprecated_yaml_import_issue",
|
||||
translation_placeholders={"error_type": error_type},
|
||||
translation_placeholders={
|
||||
"error_type": error_type,
|
||||
"url": "/config/integrations/dashboard/add?domain=hydrawise",
|
||||
},
|
||||
)
|
||||
return self.async_abort(reason=error_type)
|
||||
|
||||
|
28
homeassistant/components/ipp/diagnostics.py
Normal file
28
homeassistant/components/ipp/diagnostics.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Diagnostics support for Internet Printing Protocol (IPP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import IPPDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
return {
|
||||
"entry": {
|
||||
"data": {
|
||||
**config_entry.data,
|
||||
},
|
||||
"unique_id": config_entry.unique_id,
|
||||
},
|
||||
"data": coordinator.data.as_dict(),
|
||||
}
|
@ -8,6 +8,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["deepmerge", "pyipp"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyipp==0.14.5"],
|
||||
"requirements": ["pyipp==0.15.0"],
|
||||
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
|
||||
}
|
||||
|
@ -149,7 +149,9 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity):
|
||||
media_content_type = CONTENT_TYPE_MAP.get(self.now_playing["Type"], None)
|
||||
media_content_id = self.now_playing["Id"]
|
||||
media_title = self.now_playing["Name"]
|
||||
media_duration = int(self.now_playing["RunTimeTicks"] / 10000000)
|
||||
|
||||
if "RunTimeTicks" in self.now_playing:
|
||||
media_duration = int(self.now_playing["RunTimeTicks"] / 10000000)
|
||||
|
||||
if media_content_type == MediaType.EPISODE:
|
||||
media_content_type = MediaType.TVSHOW
|
||||
|
@ -1,37 +1,107 @@
|
||||
"""The JuiceNet integration."""
|
||||
from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
import aiohttp
|
||||
from pyjuicenet import Api, TokenError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
DOMAIN = "juicenet"
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .device import JuiceNetApi
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the JuiceNet component."""
|
||||
conf = config.get(DOMAIN)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if not conf:
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up JuiceNet from a config entry."""
|
||||
ir.async_create_issue(
|
||||
|
||||
config = entry.data
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
access_token = config[CONF_ACCESS_TOKEN]
|
||||
api = Api(access_token, session)
|
||||
|
||||
juicenet = JuiceNetApi(api)
|
||||
|
||||
try:
|
||||
await juicenet.setup()
|
||||
except TokenError as error:
|
||||
_LOGGER.error("JuiceNet Error %s", error)
|
||||
return False
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Could not reach the JuiceNet API %s", error)
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
if not juicenet.devices:
|
||||
_LOGGER.error("No JuiceNet devices found for this account")
|
||||
return False
|
||||
_LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices))
|
||||
|
||||
async def async_update_data():
|
||||
"""Update all device states from the JuiceNet API."""
|
||||
for device in juicenet.devices:
|
||||
await device.update_state(True)
|
||||
return True
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/juicenet",
|
||||
},
|
||||
_LOGGER,
|
||||
name="JuiceNet",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
JUICENET_API: juicenet,
|
||||
JUICENET_COORDINATOR: coordinator,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
return True
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
@ -1,11 +1,77 @@
|
||||
"""Config flow for JuiceNet integration."""
|
||||
import logging
|
||||
|
||||
from homeassistant import config_entries
|
||||
import aiohttp
|
||||
from pyjuicenet import Api, TokenError
|
||||
import voluptuous as vol
|
||||
|
||||
from . import DOMAIN
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
juicenet = Api(data[CONF_ACCESS_TOKEN], session)
|
||||
|
||||
try:
|
||||
await juicenet.get_devices()
|
||||
except TokenError as error:
|
||||
_LOGGER.error("Token Error %s", error)
|
||||
raise InvalidAuth from error
|
||||
except aiohttp.ClientError as error:
|
||||
_LOGGER.error("Error connecting %s", error)
|
||||
raise CannotConnect from error
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": "JuiceNet"}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for JuiceNet."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Handle import."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
6
homeassistant/components/juicenet/const.py
Normal file
6
homeassistant/components/juicenet/const.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Constants used by the JuiceNet component."""
|
||||
|
||||
DOMAIN = "juicenet"
|
||||
|
||||
JUICENET_API = "juicenet_api"
|
||||
JUICENET_COORDINATOR = "juicenet_coordinator"
|
19
homeassistant/components/juicenet/device.py
Normal file
19
homeassistant/components/juicenet/device.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||
|
||||
|
||||
class JuiceNetApi:
|
||||
"""Represent a connection to JuiceNet."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Create an object from the provided API instance."""
|
||||
self.api = api
|
||||
self._devices = []
|
||||
|
||||
async def setup(self):
|
||||
"""JuiceNet device setup.""" # noqa: D403
|
||||
self._devices = await self.api.get_devices()
|
||||
|
||||
@property
|
||||
def devices(self) -> list:
|
||||
"""Get a list of devices managed by this account."""
|
||||
return self._devices
|
34
homeassistant/components/juicenet/entity.py
Normal file
34
homeassistant/components/juicenet/entity.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Adapter to wrap the pyjuicenet api for home assistant."""
|
||||
|
||||
from pyjuicenet import Charger
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class JuiceNetDevice(CoordinatorEntity):
|
||||
"""Represent a base JuiceNet device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.device = device
|
||||
self.key = key
|
||||
self._attr_unique_id = f"{device.id}-{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=(
|
||||
f"https://home.juice.net/Portal/Details?unitID={device.id}"
|
||||
),
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
manufacturer="JuiceNet",
|
||||
name=device.name,
|
||||
)
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "juicenet",
|
||||
"name": "JuiceNet",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@jesserockz"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/juicenet",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": []
|
||||
"loggers": ["pyjuicenet"],
|
||||
"requirements": ["python-juicenet==1.1.0"]
|
||||
}
|
||||
|
99
homeassistant/components/juicenet/number.py
Normal file
99
homeassistant/components/juicenet/number.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyjuicenet import Api, Charger
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JuiceNetNumberEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
setter_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JuiceNetNumberEntityDescription(
|
||||
NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin
|
||||
):
|
||||
"""An entity description for a JuiceNetNumber."""
|
||||
|
||||
native_max_value_key: str | None = None
|
||||
|
||||
|
||||
NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = (
|
||||
JuiceNetNumberEntityDescription(
|
||||
translation_key="amperage_limit",
|
||||
key="current_charging_amperage_limit",
|
||||
native_min_value=6,
|
||||
native_max_value_key="max_charging_amperage",
|
||||
native_step=1,
|
||||
setter_key="set_charging_amperage_limit",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JuiceNet Numbers."""
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api: Api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
entities = [
|
||||
JuiceNetNumber(device, description, coordinator)
|
||||
for device in api.devices
|
||||
for description in NUMBER_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetNumber(JuiceNetDevice, NumberEntity):
|
||||
"""Implementation of a JuiceNet number."""
|
||||
|
||||
entity_description: JuiceNetNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Charger,
|
||||
description: JuiceNetNumberEntityDescription,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialise the number."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the value of the entity."""
|
||||
return getattr(self.device, self.entity_description.key, None)
|
||||
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
if self.entity_description.native_max_value_key is not None:
|
||||
return getattr(self.device, self.entity_description.native_max_value_key)
|
||||
if self.entity_description.native_max_value is not None:
|
||||
return self.entity_description.native_max_value
|
||||
return DEFAULT_MAX_VALUE
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
await getattr(self.device, self.entity_description.setter_key)(value)
|
116
homeassistant/components/juicenet/sensor.py
Normal file
116
homeassistant/components/juicenet/sensor.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="status",
|
||||
name="Charging Status",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="amps",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="watts",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="charge_time",
|
||||
translation_key="charge_time",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_added",
|
||||
translation_key="energy_added",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JuiceNet Sensors."""
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
entities = [
|
||||
JuiceNetSensorDevice(device, coordinator, description)
|
||||
for device in api.devices
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity):
|
||||
"""Implementation of a JuiceNet sensor."""
|
||||
|
||||
def __init__(
|
||||
self, device, coordinator, description: SensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialise the sensor."""
|
||||
super().__init__(device, description.key, coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the sensor."""
|
||||
icon = None
|
||||
if self.entity_description.key == "status":
|
||||
status = self.device.status
|
||||
if status == "standby":
|
||||
icon = "mdi:power-plug-off"
|
||||
elif status == "plugged":
|
||||
icon = "mdi:power-plug"
|
||||
elif status == "charging":
|
||||
icon = "mdi:battery-positive"
|
||||
else:
|
||||
icon = self.entity_description.icon
|
||||
return icon
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state."""
|
||||
return getattr(self.device, self.entity_description.key, None)
|
@ -1,8 +1,41 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The JuiceNet integration has been removed",
|
||||
"description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})."
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"description": "You will need the API Token from https://home.juice.net/Manage.",
|
||||
"title": "Connect to JuiceNet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"amperage_limit": {
|
||||
"name": "Amperage limit"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"charge_time": {
|
||||
"name": "Charge time"
|
||||
},
|
||||
"energy_added": {
|
||||
"name": "Energy added"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"charge_now": {
|
||||
"name": "Charge now"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
49
homeassistant/components/juicenet/switch.py
Normal file
49
homeassistant/components/juicenet/switch.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches."""
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
|
||||
from .entity import JuiceNetDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JuiceNet switches."""
|
||||
entities = []
|
||||
juicenet_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
api = juicenet_data[JUICENET_API]
|
||||
coordinator = juicenet_data[JUICENET_COORDINATOR]
|
||||
|
||||
for device in api.devices:
|
||||
entities.append(JuiceNetChargeNowSwitch(device, coordinator))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity):
|
||||
"""Implementation of a JuiceNet switch."""
|
||||
|
||||
_attr_translation_key = "charge_now"
|
||||
|
||||
def __init__(self, device, coordinator):
|
||||
"""Initialise the switch."""
|
||||
super().__init__(device, "charge_now", coordinator)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self.device.override_time != 0
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Charge now."""
|
||||
await self.device.set_override(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Don't charge now."""
|
||||
await self.device.set_override(False)
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==7.0.0"]
|
||||
"requirements": ["ical==7.0.1"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==7.0.0"]
|
||||
"requirements": ["ical==7.0.1"]
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_TODO_LIST_NAME, DOMAIN
|
||||
from .store import LocalTodoListStore
|
||||
@ -124,6 +125,9 @@ class LocalTodoListEntity(TodoListEntity):
|
||||
self._attr_name = name.capitalize()
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
def _new_todo_store(self) -> TodoStore:
|
||||
return TodoStore(self._calendar, tzinfo=dt_util.DEFAULT_TIME_ZONE)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update entity state based on the local To-do items."""
|
||||
todo_items = []
|
||||
@ -147,20 +151,20 @@ class LocalTodoListEntity(TodoListEntity):
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
todo = _convert_item(item)
|
||||
TodoStore(self._calendar).add(todo)
|
||||
self._new_todo_store().add(todo)
|
||||
await self.async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update an item to the To-do list."""
|
||||
todo = _convert_item(item)
|
||||
TodoStore(self._calendar).edit(todo.uid, todo)
|
||||
self._new_todo_store().edit(todo.uid, todo)
|
||||
await self.async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete an item from the To-do list."""
|
||||
store = TodoStore(self._calendar)
|
||||
store = self._new_todo_store()
|
||||
for uid in uids:
|
||||
store.delete(uid)
|
||||
await self.async_save()
|
||||
|
@ -7,7 +7,7 @@
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/loqed",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["loqedAPI==2.1.8"],
|
||||
"requirements": ["loqedAPI==2.1.10"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/luci",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openwrt_luci_rpc"],
|
||||
"requirements": ["openwrt-luci-rpc==1.1.16"]
|
||||
"requirements": ["openwrt-luci-rpc==1.1.17"]
|
||||
}
|
||||
|
@ -134,12 +134,11 @@ class MjpegCamera(Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
# DigestAuth is not supported
|
||||
if (
|
||||
self._authentication == HTTP_DIGEST_AUTHENTICATION
|
||||
or self._still_image_url is None
|
||||
):
|
||||
return await self._async_digest_camera_image()
|
||||
return await self._async_digest_or_fallback_camera_image()
|
||||
|
||||
websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl)
|
||||
try:
|
||||
@ -157,15 +156,17 @@ class MjpegCamera(Camera):
|
||||
|
||||
return None
|
||||
|
||||
def _get_digest_auth(self) -> httpx.DigestAuth:
|
||||
"""Return a DigestAuth object."""
|
||||
def _get_httpx_auth(self) -> httpx.Auth:
|
||||
"""Return a httpx auth object."""
|
||||
username = "" if self._username is None else self._username
|
||||
return httpx.DigestAuth(username, self._password)
|
||||
digest_auth = self._authentication == HTTP_DIGEST_AUTHENTICATION
|
||||
cls = httpx.DigestAuth if digest_auth else httpx.BasicAuth
|
||||
return cls(username, self._password)
|
||||
|
||||
async def _async_digest_camera_image(self) -> bytes | None:
|
||||
async def _async_digest_or_fallback_camera_image(self) -> bytes | None:
|
||||
"""Return a still image response from the camera using digest authentication."""
|
||||
client = get_async_client(self.hass, verify_ssl=self._verify_ssl)
|
||||
auth = self._get_digest_auth()
|
||||
auth = self._get_httpx_auth()
|
||||
try:
|
||||
if self._still_image_url:
|
||||
# Fallback to MJPEG stream if still image URL is not available
|
||||
@ -196,7 +197,7 @@ class MjpegCamera(Camera):
|
||||
) -> web.StreamResponse | None:
|
||||
"""Generate an HTTP MJPEG stream from the camera using digest authentication."""
|
||||
async with get_async_client(self.hass, verify_ssl=self._verify_ssl).stream(
|
||||
"get", self._mjpeg_url, auth=self._get_digest_auth(), timeout=TIMEOUT
|
||||
"get", self._mjpeg_url, auth=self._get_httpx_auth(), timeout=TIMEOUT
|
||||
) as stream:
|
||||
response = web.StreamResponse(headers=stream.headers)
|
||||
await response.prepare(request)
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pymodbus==3.6.4"]
|
||||
"requirements": ["pymodbus==3.6.5"]
|
||||
}
|
||||
|
@ -301,16 +301,17 @@ def check_config(config: dict) -> dict:
|
||||
|
||||
def validate_entity(
|
||||
hub_name: str,
|
||||
component: str,
|
||||
entity: dict,
|
||||
minimum_scan_interval: int,
|
||||
ent_names: set,
|
||||
ent_addr: set,
|
||||
) -> bool:
|
||||
"""Validate entity."""
|
||||
name = entity[CONF_NAME]
|
||||
name = f"{component}.{entity[CONF_NAME]}"
|
||||
addr = f"{hub_name}{entity[CONF_ADDRESS]}"
|
||||
scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
if scan_interval < 5:
|
||||
if 0 < scan_interval < 5:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s %s scan_interval(%d) is lower than 5 seconds, "
|
||||
@ -368,15 +369,18 @@ def check_config(config: dict) -> dict:
|
||||
if not validate_modbus(hub, hub_name_inx):
|
||||
del config[hub_inx]
|
||||
continue
|
||||
for _component, conf_key in PLATFORMS:
|
||||
minimum_scan_interval = 9999
|
||||
no_entities = True
|
||||
for component, conf_key in PLATFORMS:
|
||||
if conf_key not in hub:
|
||||
continue
|
||||
no_entities = False
|
||||
entity_inx = 0
|
||||
entities = hub[conf_key]
|
||||
minimum_scan_interval = 9999
|
||||
while entity_inx < len(entities):
|
||||
if not validate_entity(
|
||||
hub[CONF_NAME],
|
||||
component,
|
||||
entities[entity_inx],
|
||||
minimum_scan_interval,
|
||||
ent_names,
|
||||
@ -385,7 +389,11 @@ def check_config(config: dict) -> dict:
|
||||
del entities[entity_inx]
|
||||
else:
|
||||
entity_inx += 1
|
||||
|
||||
if no_entities:
|
||||
err = f"Modbus {hub[CONF_NAME]} contain no entities, this will cause instability, please add at least one entity!"
|
||||
_LOGGER.warning(err)
|
||||
# Ensure timeout is not started/handled.
|
||||
hub[CONF_TIMEOUT] = -1
|
||||
if hub[CONF_TIMEOUT] >= minimum_scan_interval:
|
||||
hub[CONF_TIMEOUT] = minimum_scan_interval - 1
|
||||
_LOGGER.warning(
|
||||
|
@ -180,6 +180,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Create a callback to save the refresh token when it changes:
|
||||
entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token))
|
||||
|
||||
# Save the client's refresh token if it's different than what we already have:
|
||||
if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]:
|
||||
async_save_refresh_token(token)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, **entry_updates)
|
||||
|
||||
async def async_update() -> NotionData:
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aionotion"],
|
||||
"requirements": ["aionotion==2024.02.2"]
|
||||
"requirements": ["aionotion==2024.03.0"]
|
||||
}
|
||||
|
@ -3,7 +3,8 @@
|
||||
"name": "Numato USB GPIO Expander",
|
||||
"codeowners": ["@clssn"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/numato",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["numato_gpio"],
|
||||
"requirements": ["numato-gpio==0.10.0"]
|
||||
"requirements": ["numato-gpio==0.12.0"]
|
||||
}
|
||||
|
@ -132,16 +132,27 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
return None
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shutdown the coordinator."""
|
||||
await self._cleanup_device()
|
||||
await super().async_shutdown()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
try:
|
||||
device = await self._get_device()
|
||||
async with asyncio.timeout(5):
|
||||
return await _get_all_data(device, self.entry.data[CONF_MAC])
|
||||
except RAVEnConnectionError as err:
|
||||
if self._raven_device:
|
||||
await self._raven_device.close()
|
||||
self._raven_device = None
|
||||
await self._cleanup_device()
|
||||
raise UpdateFailed(f"RAVEnConnectionError: {err}") from err
|
||||
except TimeoutError:
|
||||
await self._cleanup_device()
|
||||
raise
|
||||
|
||||
async def _cleanup_device(self) -> None:
|
||||
device, self._raven_device = self._raven_device, None
|
||||
if device is not None:
|
||||
await device.close()
|
||||
|
||||
async def _get_device(self) -> RAVEnSerialDevice:
|
||||
if self._raven_device is not None:
|
||||
@ -149,15 +160,14 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
|
||||
|
||||
device = RAVEnSerialDevice(self.entry.data[CONF_DEVICE])
|
||||
|
||||
async with asyncio.timeout(5):
|
||||
await device.open()
|
||||
|
||||
try:
|
||||
try:
|
||||
async with asyncio.timeout(5):
|
||||
await device.open()
|
||||
await device.synchronize()
|
||||
self._device_info = await device.get_device_info()
|
||||
except Exception:
|
||||
await device.close()
|
||||
raise
|
||||
except:
|
||||
await device.close()
|
||||
raise
|
||||
|
||||
self._raven_device = device
|
||||
return device
|
||||
|
@ -924,7 +924,7 @@ class Recorder(threading.Thread):
|
||||
# that is pending before running the task
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(task, RecorderTask)
|
||||
if not task.commit_before:
|
||||
if task.commit_before:
|
||||
self._commit_event_session_or_retry()
|
||||
return task.run(self)
|
||||
except exc.DatabaseError as err:
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
|
||||
from .core import Recorder
|
||||
from .util import get_instance, session_scope
|
||||
from .util import filter_unique_constraint_integrity_error, get_instance, session_scope
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -61,7 +61,10 @@ def update_states_metadata(
|
||||
)
|
||||
return
|
||||
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(
|
||||
session=instance.get_session(),
|
||||
exception_filter=filter_unique_constraint_integrity_error(instance, "state"),
|
||||
) as session:
|
||||
if not states_meta_manager.update_metadata(session, entity_id, new_entity_id):
|
||||
_LOGGER.warning(
|
||||
"Cannot migrate history for entity_id `%s` to `%s` "
|
||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
import contextlib
|
||||
import dataclasses
|
||||
from datetime import datetime, timedelta
|
||||
from functools import lru_cache, partial
|
||||
@ -15,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
|
||||
|
||||
from sqlalchemy import Select, and_, bindparam, func, lambda_stmt, select, text
|
||||
from sqlalchemy.engine.row import Row
|
||||
from sqlalchemy.exc import SQLAlchemyError, StatementError
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||
import voluptuous as vol
|
||||
@ -72,6 +71,7 @@ from .models import (
|
||||
from .util import (
|
||||
execute,
|
||||
execute_stmt_lambda_element,
|
||||
filter_unique_constraint_integrity_error,
|
||||
get_instance,
|
||||
retryable_database_job,
|
||||
session_scope,
|
||||
@ -454,7 +454,9 @@ def compile_missing_statistics(instance: Recorder) -> bool:
|
||||
|
||||
with session_scope(
|
||||
session=instance.get_session(),
|
||||
exception_filter=_filter_unique_constraint_integrity_error(instance),
|
||||
exception_filter=filter_unique_constraint_integrity_error(
|
||||
instance, "statistic"
|
||||
),
|
||||
) as session:
|
||||
# Find the newest statistics run, if any
|
||||
if last_run := session.query(func.max(StatisticsRuns.start)).scalar():
|
||||
@ -486,7 +488,9 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) -
|
||||
# Return if we already have 5-minute statistics for the requested period
|
||||
with session_scope(
|
||||
session=instance.get_session(),
|
||||
exception_filter=_filter_unique_constraint_integrity_error(instance),
|
||||
exception_filter=filter_unique_constraint_integrity_error(
|
||||
instance, "statistic"
|
||||
),
|
||||
) as session:
|
||||
modified_statistic_ids = _compile_statistics(
|
||||
instance, session, start, fire_events
|
||||
@ -737,7 +741,9 @@ def update_statistics_metadata(
|
||||
if new_statistic_id is not UNDEFINED and new_statistic_id is not None:
|
||||
with session_scope(
|
||||
session=instance.get_session(),
|
||||
exception_filter=_filter_unique_constraint_integrity_error(instance),
|
||||
exception_filter=filter_unique_constraint_integrity_error(
|
||||
instance, "statistic"
|
||||
),
|
||||
) as session:
|
||||
statistics_meta_manager.update_statistic_id(
|
||||
session, DOMAIN, statistic_id, new_statistic_id
|
||||
@ -2246,54 +2252,6 @@ def async_add_external_statistics(
|
||||
_async_import_statistics(hass, metadata, statistics)
|
||||
|
||||
|
||||
def _filter_unique_constraint_integrity_error(
|
||||
instance: Recorder,
|
||||
) -> Callable[[Exception], bool]:
|
||||
def _filter_unique_constraint_integrity_error(err: Exception) -> bool:
|
||||
"""Handle unique constraint integrity errors."""
|
||||
if not isinstance(err, StatementError):
|
||||
return False
|
||||
|
||||
assert instance.engine is not None
|
||||
dialect_name = instance.engine.dialect.name
|
||||
|
||||
ignore = False
|
||||
if (
|
||||
dialect_name == SupportedDialect.SQLITE
|
||||
and "UNIQUE constraint failed" in str(err)
|
||||
):
|
||||
ignore = True
|
||||
if (
|
||||
dialect_name == SupportedDialect.POSTGRESQL
|
||||
and err.orig
|
||||
and hasattr(err.orig, "pgcode")
|
||||
and err.orig.pgcode == "23505"
|
||||
):
|
||||
ignore = True
|
||||
if (
|
||||
dialect_name == SupportedDialect.MYSQL
|
||||
and err.orig
|
||||
and hasattr(err.orig, "args")
|
||||
):
|
||||
with contextlib.suppress(TypeError):
|
||||
if err.orig.args[0] == 1062:
|
||||
ignore = True
|
||||
|
||||
if ignore:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Blocked attempt to insert duplicated statistic rows, please report"
|
||||
" at %s"
|
||||
),
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22",
|
||||
exc_info=err,
|
||||
)
|
||||
|
||||
return ignore
|
||||
|
||||
return _filter_unique_constraint_integrity_error
|
||||
|
||||
|
||||
def _import_statistics_with_session(
|
||||
instance: Recorder,
|
||||
session: Session,
|
||||
@ -2397,7 +2355,9 @@ def import_statistics(
|
||||
|
||||
with session_scope(
|
||||
session=instance.get_session(),
|
||||
exception_filter=_filter_unique_constraint_integrity_error(instance),
|
||||
exception_filter=filter_unique_constraint_integrity_error(
|
||||
instance, "statistic"
|
||||
),
|
||||
) as session:
|
||||
return _import_statistics_with_session(
|
||||
instance, session, metadata, statistics, table
|
||||
|
@ -307,11 +307,18 @@ class StatisticsMetaManager:
|
||||
recorder thread.
|
||||
"""
|
||||
self._assert_in_recorder_thread()
|
||||
if self.get(session, new_statistic_id):
|
||||
_LOGGER.error(
|
||||
"Cannot rename statistic_id `%s` to `%s` because the new statistic_id is already in use",
|
||||
old_statistic_id,
|
||||
new_statistic_id,
|
||||
)
|
||||
return
|
||||
session.query(StatisticsMeta).filter(
|
||||
(StatisticsMeta.statistic_id == old_statistic_id)
|
||||
& (StatisticsMeta.source == source)
|
||||
).update({StatisticsMeta.statistic_id: new_statistic_id})
|
||||
self._clear_cache([old_statistic_id, new_statistic_id])
|
||||
self._clear_cache([old_statistic_id])
|
||||
|
||||
def delete(self, session: Session, statistic_ids: list[str]) -> None:
|
||||
"""Clear statistics for a list of statistic_ids.
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Collection, Generator, Iterable, Sequence
|
||||
import contextlib
|
||||
from contextlib import contextmanager
|
||||
from datetime import date, datetime, timedelta
|
||||
import functools
|
||||
@ -21,7 +22,7 @@ import ciso8601
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.engine import Result, Row
|
||||
from sqlalchemy.engine.interfaces import DBAPIConnection
|
||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError
|
||||
from sqlalchemy.orm.query import Query
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||
@ -906,3 +907,54 @@ def get_index_by_name(session: Session, table_name: str, index_name: str) -> str
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def filter_unique_constraint_integrity_error(
|
||||
instance: Recorder, row_type: str
|
||||
) -> Callable[[Exception], bool]:
|
||||
"""Create a filter for unique constraint integrity errors."""
|
||||
|
||||
def _filter_unique_constraint_integrity_error(err: Exception) -> bool:
|
||||
"""Handle unique constraint integrity errors."""
|
||||
if not isinstance(err, StatementError):
|
||||
return False
|
||||
|
||||
assert instance.engine is not None
|
||||
dialect_name = instance.engine.dialect.name
|
||||
|
||||
ignore = False
|
||||
if (
|
||||
dialect_name == SupportedDialect.SQLITE
|
||||
and "UNIQUE constraint failed" in str(err)
|
||||
):
|
||||
ignore = True
|
||||
if (
|
||||
dialect_name == SupportedDialect.POSTGRESQL
|
||||
and err.orig
|
||||
and hasattr(err.orig, "pgcode")
|
||||
and err.orig.pgcode == "23505"
|
||||
):
|
||||
ignore = True
|
||||
if (
|
||||
dialect_name == SupportedDialect.MYSQL
|
||||
and err.orig
|
||||
and hasattr(err.orig, "args")
|
||||
):
|
||||
with contextlib.suppress(TypeError):
|
||||
if err.orig.args[0] == 1062:
|
||||
ignore = True
|
||||
|
||||
if ignore:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Blocked attempt to insert duplicated %s rows, please report"
|
||||
" at %s"
|
||||
),
|
||||
row_type,
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22",
|
||||
exc_info=err,
|
||||
)
|
||||
|
||||
return ignore
|
||||
|
||||
return _filter_unique_constraint_integrity_error
|
||||
|
@ -11,7 +11,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["rokuecp"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["rokuecp==0.19.1"],
|
||||
"requirements": ["rokuecp==0.19.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "roku:ecp",
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/rova",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["rova"],
|
||||
"requirements": ["rova==0.4.0"]
|
||||
"requirements": ["rova==0.4.1"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/snmp",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyasn1", "pysmi", "pysnmp"],
|
||||
"requirements": ["pysnmp-lextudio==6.0.2"]
|
||||
"requirements": ["pysnmp-lextudio==6.0.9"]
|
||||
}
|
||||
|
@ -237,7 +237,7 @@ class SnmpSwitch(SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
# If vartype set, use it - http://snmplabs.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType
|
||||
# If vartype set, use it - https://www.pysnmp.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType
|
||||
await self._execute_command(self._command_payload_on)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
@ -401,9 +401,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
|
||||
def set_timer(
|
||||
self,
|
||||
temperature: float,
|
||||
time_period: int,
|
||||
requested_overlay: str,
|
||||
temperature: float | None = None,
|
||||
time_period: int | None = None,
|
||||
requested_overlay: str | None = None,
|
||||
):
|
||||
"""Set the timer on the entity, and temperature if supported."""
|
||||
|
||||
|
@ -6,5 +6,6 @@
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tedee",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pytedee-async==0.2.15"]
|
||||
"loggers": ["pytedee_async"],
|
||||
"requirements": ["pytedee-async==0.2.16"]
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, OAUTH_SCOPES, PLATFORMS
|
||||
from .const import CLIENT, DOMAIN, OAUTH_SCOPES, PLATFORMS, SESSION
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@ -45,7 +45,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client.auto_refresh_auth = False
|
||||
await client.set_user_authentication(access_token, scope=OAUTH_SCOPES)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
CLIENT: client,
|
||||
SESSION: session,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@ -16,5 +16,7 @@ CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
DOMAIN = "twitch"
|
||||
CONF_CHANNELS = "channels"
|
||||
CLIENT = "client"
|
||||
SESSION = "session"
|
||||
|
||||
OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS, AuthScope.USER_READ_FOLLOWS]
|
||||
|
@ -19,12 +19,13 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES
|
||||
from .const import CLIENT, CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES, SESSION
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@ -51,6 +52,8 @@ ICON = "mdi:twitch"
|
||||
STATE_OFFLINE = "offline"
|
||||
STATE_STREAMING = "streaming"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def chunk_list(lst: list, chunk_size: int) -> list[list]:
|
||||
"""Split a list into chunks of chunk_size."""
|
||||
@ -97,7 +100,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize entries."""
|
||||
client = hass.data[DOMAIN][entry.entry_id]
|
||||
client = hass.data[DOMAIN][entry.entry_id][CLIENT]
|
||||
session = hass.data[DOMAIN][entry.entry_id][SESSION]
|
||||
|
||||
channels = entry.options[CONF_CHANNELS]
|
||||
|
||||
@ -107,7 +111,7 @@ async def async_setup_entry(
|
||||
for chunk in chunk_list(channels, 100):
|
||||
entities.extend(
|
||||
[
|
||||
TwitchSensor(channel, client)
|
||||
TwitchSensor(channel, session, client)
|
||||
async for channel in client.get_users(logins=chunk)
|
||||
]
|
||||
)
|
||||
@ -120,8 +124,11 @@ class TwitchSensor(SensorEntity):
|
||||
|
||||
_attr_icon = ICON
|
||||
|
||||
def __init__(self, channel: TwitchUser, client: Twitch) -> None:
|
||||
def __init__(
|
||||
self, channel: TwitchUser, session: OAuth2Session, client: Twitch
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._session = session
|
||||
self._client = client
|
||||
self._channel = channel
|
||||
self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES)
|
||||
@ -130,9 +137,17 @@ class TwitchSensor(SensorEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update device state."""
|
||||
followers = (await self._client.get_channel_followers(self._channel.id)).total
|
||||
await self._session.async_ensure_token_valid()
|
||||
await self._client.set_user_authentication(
|
||||
self._session.token["access_token"],
|
||||
OAUTH_SCOPES,
|
||||
self._session.token["refresh_token"],
|
||||
False,
|
||||
)
|
||||
followers = await self._client.get_channel_followers(self._channel.id)
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_FOLLOWING: followers,
|
||||
ATTR_FOLLOWING: followers.total,
|
||||
ATTR_VIEWS: self._channel.view_count,
|
||||
}
|
||||
if self._enable_user_auth:
|
||||
@ -166,7 +181,7 @@ class TwitchSensor(SensorEntity):
|
||||
self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = True
|
||||
self._attr_extra_state_attributes[ATTR_SUBSCRIPTION_GIFTED] = sub.is_gift
|
||||
except TwitchResourceNotFound:
|
||||
LOGGER.debug("User is not subscribed")
|
||||
LOGGER.debug("User is not subscribed to %s", self._channel.display_name)
|
||||
except TwitchAPIException as exc:
|
||||
LOGGER.error("Error response on check_user_subscription: %s", exc)
|
||||
|
||||
|
@ -79,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# Create device.
|
||||
assert discovery_info is not None
|
||||
assert discovery_info.ssdp_udn
|
||||
assert discovery_info.ssdp_all_locations
|
||||
location = get_preferred_location(discovery_info.ssdp_all_locations)
|
||||
try:
|
||||
@ -117,7 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if device.serial_number:
|
||||
identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
|
||||
|
||||
connections = {(dr.CONNECTION_UPNP, device.udn)}
|
||||
connections = {(dr.CONNECTION_UPNP, discovery_info.ssdp_udn)}
|
||||
if discovery_info.ssdp_udn != device.udn:
|
||||
connections.add((dr.CONNECTION_UPNP, device.udn))
|
||||
if device_mac_address:
|
||||
connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address))
|
||||
|
||||
|
@ -42,7 +42,7 @@ def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str:
|
||||
def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
"""Test if discovery is complete and usable."""
|
||||
return bool(
|
||||
ssdp.ATTR_UPNP_UDN in discovery_info.upnp
|
||||
discovery_info.ssdp_udn
|
||||
and discovery_info.ssdp_st
|
||||
and discovery_info.ssdp_all_locations
|
||||
and discovery_info.ssdp_usn
|
||||
@ -80,9 +80,8 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
# Paths:
|
||||
# - ssdp(discovery_info) --> ssdp_confirm(None)
|
||||
# --> ssdp_confirm({}) --> create_entry()
|
||||
# - user(None): scan --> user({...}) --> create_entry()
|
||||
# 1: ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
|
||||
# 2: user(None): scan --> user({...}) --> create_entry()
|
||||
|
||||
@property
|
||||
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
|
||||
@ -241,9 +240,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery = self._remove_discovery(usn)
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
|
||||
data = {
|
||||
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_UDN: discovery.ssdp_udn,
|
||||
CONFIG_ENTRY_ST: discovery.ssdp_st,
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
CONFIG_ENTRY_LOCATION: get_preferred_location(discovery.ssdp_all_locations),
|
||||
@ -265,9 +264,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
title = _friendly_name_from_discovery(discovery)
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
|
||||
data = {
|
||||
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_UDN: discovery.ssdp_udn,
|
||||
CONFIG_ENTRY_ST: discovery.ssdp_st,
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn,
|
||||
CONFIG_ENTRY_LOCATION: get_preferred_location(discovery.ssdp_all_locations),
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
|
@ -52,6 +52,7 @@ SERVICE_SET_VICARE_MODE_ATTR_MODE = "vicare_mode"
|
||||
|
||||
VICARE_MODE_DHW = "dhw"
|
||||
VICARE_MODE_HEATING = "heating"
|
||||
VICARE_MODE_HEATINGCOOLING = "heatingCooling"
|
||||
VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
|
||||
VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling"
|
||||
VICARE_MODE_FORCEDREDUCED = "forcedReduced"
|
||||
@ -71,6 +72,7 @@ VICARE_TO_HA_HVAC_HEATING: dict[str, HVACMode] = {
|
||||
VICARE_MODE_DHW: HVACMode.OFF,
|
||||
VICARE_MODE_DHWANDHEATINGCOOLING: HVACMode.AUTO,
|
||||
VICARE_MODE_DHWANDHEATING: HVACMode.AUTO,
|
||||
VICARE_MODE_HEATINGCOOLING: HVACMode.AUTO,
|
||||
VICARE_MODE_HEATING: HVACMode.AUTO,
|
||||
VICARE_MODE_FORCEDNORMAL: HVACMode.HEAT,
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["weatherflow4py==0.1.12"]
|
||||
"requirements": ["weatherflow4py==0.1.17"]
|
||||
}
|
||||
|
@ -491,23 +491,27 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||
|
||||
VERSION = 4
|
||||
|
||||
async def _set_unique_id_or_update_path(
|
||||
async def _set_unique_id_and_update_ignored_flow(
|
||||
self, unique_id: str, device_path: str
|
||||
) -> None:
|
||||
"""Set the flow's unique ID and update the device path if it isn't unique."""
|
||||
"""Set the flow's unique ID and update the device path in an ignored flow."""
|
||||
current_entry = await self.async_set_unique_id(unique_id)
|
||||
|
||||
if not current_entry:
|
||||
return
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_DEVICE: {
|
||||
**current_entry.data.get(CONF_DEVICE, {}),
|
||||
CONF_DEVICE_PATH: device_path,
|
||||
},
|
||||
}
|
||||
)
|
||||
if current_entry.source != config_entries.SOURCE_IGNORE:
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
# Only update the current entry if it is an ignored discovery
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_DEVICE: {
|
||||
**current_entry.data.get(CONF_DEVICE, {}),
|
||||
CONF_DEVICE_PATH: device_path,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@ -575,7 +579,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||
description = discovery_info.description
|
||||
dev_path = discovery_info.device
|
||||
|
||||
await self._set_unique_id_or_update_path(
|
||||
await self._set_unique_id_and_update_ignored_flow(
|
||||
unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}",
|
||||
device_path=dev_path,
|
||||
)
|
||||
@ -625,7 +629,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||
node_name = local_name.removesuffix(".local")
|
||||
device_path = f"socket://{discovery_info.host}:{port}"
|
||||
|
||||
await self._set_unique_id_or_update_path(
|
||||
await self._set_unique_id_and_update_ignored_flow(
|
||||
unique_id=node_name,
|
||||
device_path=device_path,
|
||||
)
|
||||
@ -650,7 +654,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN
|
||||
device_settings = discovery_data["port"]
|
||||
device_path = device_settings[CONF_DEVICE_PATH]
|
||||
|
||||
await self._set_unique_id_or_update_path(
|
||||
await self._set_unique_id_and_update_ignored_flow(
|
||||
unique_id=f"{name}_{radio_type.name}_{device_path}",
|
||||
device_path=device_path,
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||
|
@ -255,6 +255,7 @@ FLOWS = {
|
||||
"isy994",
|
||||
"izone",
|
||||
"jellyfin",
|
||||
"juicenet",
|
||||
"justnimbus",
|
||||
"jvc_projector",
|
||||
"kaleidescape",
|
||||
|
@ -2911,6 +2911,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"juicenet": {
|
||||
"name": "JuiceNet",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"justnimbus": {
|
||||
"name": "JustNimbus",
|
||||
"integration_type": "hub",
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==0.8.0
|
||||
aiodhcpwatcher==0.8.1
|
||||
aiodiscover==1.6.1
|
||||
aiohttp-fast-url-dispatcher==0.3.0
|
||||
aiohttp-zlib-ng==0.3.1
|
||||
@ -30,8 +30,8 @@ habluetooth==2.4.2
|
||||
hass-nabucasa==0.78.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240306.0
|
||||
home-assistant-intents==2024.2.28
|
||||
home-assistant-frontend==20240307.0
|
||||
home-assistant-intents==2024.3.12
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
janus==1.0.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.3.0"
|
||||
version = "2024.3.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -35,6 +35,9 @@ dependencies = [
|
||||
"bcrypt==4.1.2",
|
||||
"certifi>=2021.5.30",
|
||||
"ciso8601==2.3.1",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==0.78.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.27.0",
|
||||
@ -525,9 +528,6 @@ filterwarnings = [
|
||||
"ignore:loop argument is deprecated:DeprecationWarning:emulated_roku",
|
||||
# https://github.com/thecynic/pylutron - v0.2.10
|
||||
"ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron",
|
||||
# Fixed for Python 3.12
|
||||
# https://github.com/lextudio/pysnmp/issues/10
|
||||
"ignore:The asyncore module is deprecated and will be removed in Python 3.12:DeprecationWarning:pysnmp.carrier.asyncore.base",
|
||||
# Wrong stacklevel
|
||||
# https://bugs.launchpad.net/beautifulsoup/+bug/2034451
|
||||
"ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:bs4.builder",
|
||||
|
@ -15,6 +15,7 @@ awesomeversion==24.2.0
|
||||
bcrypt==4.1.2
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.1
|
||||
hass-nabucasa==0.78.0
|
||||
httpx==0.27.0
|
||||
home-assistant-bluetooth==1.12.0
|
||||
ifaddr==0.2.0
|
||||
|
@ -206,7 +206,7 @@ aioaseko==0.0.2
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2024.2.10
|
||||
aioautomower==2024.3.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
@ -221,7 +221,7 @@ aiobotocore==2.9.1
|
||||
aiocomelit==0.9.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==0.8.0
|
||||
aiodhcpwatcher==0.8.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.6.1
|
||||
@ -315,7 +315,7 @@ aiomusiccast==0.14.8
|
||||
aionanoleaf==0.2.1
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==2024.02.2
|
||||
aionotion==2024.03.0
|
||||
|
||||
# homeassistant.components.oncue
|
||||
aiooncue==0.3.5
|
||||
@ -419,7 +419,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.6.1
|
||||
airthings-ble==0.7.1
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings-cloud==0.2.0
|
||||
@ -514,7 +514,7 @@ aurorapy==0.2.7
|
||||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==50
|
||||
axis==54
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@ -596,20 +596,20 @@ bluetooth-data-tools==1.19.0
|
||||
bond-async==0.2.1
|
||||
|
||||
# homeassistant.components.bosch_shc
|
||||
boschshcpy==0.2.75
|
||||
boschshcpy==0.2.82
|
||||
|
||||
# homeassistant.components.amazon_polly
|
||||
# homeassistant.components.route53
|
||||
boto3==1.33.13
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==0.5.5
|
||||
bring-api==0.5.6
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.18.3
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==4.0.0
|
||||
brother==4.0.2
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@ -621,7 +621,7 @@ brunt==1.2.0
|
||||
bt-proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.6.0
|
||||
bthome-ble==3.8.0
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@ -1074,10 +1074,10 @@ hole==0.8.0
|
||||
holidays==0.44
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240306.0
|
||||
home-assistant-frontend==20240307.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
home-assistant-intents==2024.3.12
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -1115,7 +1115,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.google
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
ical==7.0.0
|
||||
ical==7.0.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@ -1253,7 +1253,7 @@ logi-circle==0.2.3
|
||||
london-tube-status==0.5
|
||||
|
||||
# homeassistant.components.loqed
|
||||
loqedAPI==2.1.8
|
||||
loqedAPI==2.1.10
|
||||
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.7.4
|
||||
@ -1409,7 +1409,7 @@ nsw-fuel-api-client==1.1.0
|
||||
nuheat==1.0.1
|
||||
|
||||
# homeassistant.components.numato
|
||||
numato-gpio==0.10.0
|
||||
numato-gpio==0.12.0
|
||||
|
||||
# homeassistant.components.compensation
|
||||
# homeassistant.components.iqvia
|
||||
@ -1474,7 +1474,7 @@ opensensemap-api==0.2.0
|
||||
openwebifpy==4.2.4
|
||||
|
||||
# homeassistant.components.luci
|
||||
openwrt-luci-rpc==1.1.16
|
||||
openwrt-luci-rpc==1.1.17
|
||||
|
||||
# homeassistant.components.ubus
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
@ -1794,7 +1794,7 @@ pyedimax==0.2.1
|
||||
pyefergy==22.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.19.1
|
||||
pyenphase==1.19.2
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.6
|
||||
@ -1878,7 +1878,7 @@ pyintesishome==1.8.0
|
||||
pyipma==3.0.7
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.14.5
|
||||
pyipp==0.15.0
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==2022.04.0
|
||||
@ -1971,7 +1971,7 @@ pymitv==1.4.3
|
||||
pymochad==0.2.0
|
||||
|
||||
# homeassistant.components.modbus
|
||||
pymodbus==3.6.4
|
||||
pymodbus==3.6.5
|
||||
|
||||
# homeassistant.components.monoprice
|
||||
pymonoprice==0.4
|
||||
@ -2155,7 +2155,7 @@ pysmartthings==0.7.8
|
||||
pysml==0.0.12
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp-lextudio==6.0.2
|
||||
pysnmp-lextudio==6.0.9
|
||||
|
||||
# homeassistant.components.snooz
|
||||
pysnooz==0.8.6
|
||||
@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0
|
||||
pytautulli==23.1.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
pytedee-async==0.2.15
|
||||
pytedee-async==0.2.16
|
||||
|
||||
# homeassistant.components.tfiac
|
||||
pytfiac==0.4
|
||||
@ -2244,6 +2244,9 @@ python-izone==1.2.9
|
||||
# homeassistant.components.joaoapps_join
|
||||
python-join-api==0.0.9
|
||||
|
||||
# homeassistant.components.juicenet
|
||||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.6.2.1
|
||||
|
||||
@ -2454,7 +2457,7 @@ rjpl==0.3.6
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.1
|
||||
rokuecp==0.19.2
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.7
|
||||
@ -2466,7 +2469,7 @@ roombapy==1.6.13
|
||||
roonapi==0.1.6
|
||||
|
||||
# homeassistant.components.rova
|
||||
rova==0.4.0
|
||||
rova==0.4.1
|
||||
|
||||
# homeassistant.components.rpi_power
|
||||
rpi-bad-power==0.1.0
|
||||
@ -2836,7 +2839,7 @@ watchdog==2.3.1
|
||||
waterfurnace==1.1.0
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.1.12
|
||||
weatherflow4py==0.1.17
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.1
|
||||
|
@ -185,7 +185,7 @@ aioaseko==0.0.2
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2024.2.10
|
||||
aioautomower==2024.3.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
@ -200,7 +200,7 @@ aiobotocore==2.9.1
|
||||
aiocomelit==0.9.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==0.8.0
|
||||
aiodhcpwatcher==0.8.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.6.1
|
||||
@ -288,7 +288,7 @@ aiomusiccast==0.14.8
|
||||
aionanoleaf==0.2.1
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==2024.02.2
|
||||
aionotion==2024.03.0
|
||||
|
||||
# homeassistant.components.oncue
|
||||
aiooncue==0.3.5
|
||||
@ -392,7 +392,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.6.1
|
||||
airthings-ble==0.7.1
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings-cloud==0.2.0
|
||||
@ -454,7 +454,7 @@ auroranoaa==0.0.3
|
||||
aurorapy==0.2.7
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==50
|
||||
axis==54
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@ -511,16 +511,16 @@ bluetooth-data-tools==1.19.0
|
||||
bond-async==0.2.1
|
||||
|
||||
# homeassistant.components.bosch_shc
|
||||
boschshcpy==0.2.75
|
||||
boschshcpy==0.2.82
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==0.5.5
|
||||
bring-api==0.5.6
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.18.3
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==4.0.0
|
||||
brother==4.0.2
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@ -529,7 +529,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.6.0
|
||||
bthome-ble==3.8.0
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
@ -873,10 +873,10 @@ hole==0.8.0
|
||||
holidays==0.44
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240306.0
|
||||
home-assistant-frontend==20240307.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
home-assistant-intents==2024.3.12
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@ -905,7 +905,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.google
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
ical==7.0.0
|
||||
ical==7.0.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@ -1001,7 +1001,7 @@ logi-circle==0.2.3
|
||||
london-tube-status==0.5
|
||||
|
||||
# homeassistant.components.loqed
|
||||
loqedAPI==2.1.8
|
||||
loqedAPI==2.1.10
|
||||
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.7.4
|
||||
@ -1127,7 +1127,7 @@ nsw-fuel-api-client==1.1.0
|
||||
nuheat==1.0.1
|
||||
|
||||
# homeassistant.components.numato
|
||||
numato-gpio==0.10.0
|
||||
numato-gpio==0.12.0
|
||||
|
||||
# homeassistant.components.compensation
|
||||
# homeassistant.components.iqvia
|
||||
@ -1390,7 +1390,7 @@ pyeconet==0.1.22
|
||||
pyefergy==22.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==1.19.1
|
||||
pyenphase==1.19.2
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@ -1453,7 +1453,7 @@ pyinsteon==1.5.3
|
||||
pyipma==3.0.7
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.14.5
|
||||
pyipp==0.15.0
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==2022.04.0
|
||||
@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0
|
||||
pymochad==0.2.0
|
||||
|
||||
# homeassistant.components.modbus
|
||||
pymodbus==3.6.4
|
||||
pymodbus==3.6.5
|
||||
|
||||
# homeassistant.components.monoprice
|
||||
pymonoprice==0.4
|
||||
@ -1673,7 +1673,7 @@ pysmartthings==0.7.8
|
||||
pysml==0.0.12
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp-lextudio==6.0.2
|
||||
pysnmp-lextudio==6.0.9
|
||||
|
||||
# homeassistant.components.snooz
|
||||
pysnooz==0.8.6
|
||||
@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0
|
||||
pytautulli==23.1.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
pytedee-async==0.2.15
|
||||
pytedee-async==0.2.16
|
||||
|
||||
# homeassistant.components.motionmount
|
||||
python-MotionMount==0.3.1
|
||||
@ -1723,6 +1723,9 @@ python-homewizard-energy==4.3.1
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.2.9
|
||||
|
||||
# homeassistant.components.juicenet
|
||||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.6.2.1
|
||||
|
||||
@ -1882,7 +1885,7 @@ rflink==0.0.66
|
||||
ring-doorbell[listen]==0.8.7
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.1
|
||||
rokuecp==0.19.2
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.7
|
||||
@ -2174,7 +2177,7 @@ wallbox==0.6.0
|
||||
watchdog==2.3.1
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.1.12
|
||||
weatherflow4py==0.1.17
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.1
|
||||
|
@ -33,6 +33,7 @@ IGNORE_VIOLATIONS = {
|
||||
"blink",
|
||||
"ezviz",
|
||||
"hdmi_cec",
|
||||
"juicenet",
|
||||
"lupusec",
|
||||
"rainbird",
|
||||
"slide",
|
||||
|
@ -3,7 +3,11 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||
from airthings_ble import (
|
||||
AirthingsBluetoothDeviceData,
|
||||
AirthingsDevice,
|
||||
AirthingsDeviceType,
|
||||
)
|
||||
|
||||
from homeassistant.components.airthings_ble.const import DOMAIN
|
||||
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
||||
@ -161,8 +165,7 @@ WAVE_DEVICE_INFO = AirthingsDevice(
|
||||
manufacturer="Airthings AS",
|
||||
hw_version="REV A",
|
||||
sw_version="G-BLE-1.5.3-master+0",
|
||||
model="Wave Plus",
|
||||
model_raw="2930",
|
||||
model=AirthingsDeviceType.WAVE_PLUS,
|
||||
name="Airthings Wave+",
|
||||
identifier="123456",
|
||||
sensors={
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Test the Airthings BLE config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from airthings_ble import AirthingsDevice
|
||||
from airthings_ble import AirthingsDevice, AirthingsDeviceType
|
||||
from bleak import BleakError
|
||||
|
||||
from homeassistant.components.airthings_ble.const import DOMAIN
|
||||
@ -28,8 +28,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None:
|
||||
with patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble(
|
||||
AirthingsDevice(
|
||||
manufacturer="Airthings AS",
|
||||
model="Wave Plus",
|
||||
model_raw="2930",
|
||||
model=AirthingsDeviceType.WAVE_PLUS,
|
||||
name="Airthings Wave Plus",
|
||||
identifier="123456",
|
||||
)
|
||||
@ -111,8 +110,7 @@ async def test_user_setup(hass: HomeAssistant) -> None:
|
||||
), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble(
|
||||
AirthingsDevice(
|
||||
manufacturer="Airthings AS",
|
||||
model="Wave Plus",
|
||||
model_raw="2930",
|
||||
model=AirthingsDeviceType.WAVE_PLUS,
|
||||
name="Airthings Wave Plus",
|
||||
identifier="123456",
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ from homeassistant.components import camera
|
||||
from homeassistant.components.camera.const import StreamType
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import WEBRTC_ANSWER
|
||||
@ -69,3 +70,37 @@ async def mock_camera_web_rtc_fixture(hass):
|
||||
return_value=WEBRTC_ANSWER,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera_with_device")
|
||||
async def mock_camera_with_device_fixture():
|
||||
"""Initialize a demo camera platform with a device."""
|
||||
dev_info = DeviceInfo(
|
||||
identifiers={("camera", "test_unique_id")},
|
||||
name="Test Camera Device",
|
||||
)
|
||||
|
||||
class UniqueIdMock(PropertyMock):
|
||||
def __get__(self, obj, obj_type=None):
|
||||
return obj.name
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.camera.Camera.has_entity_name",
|
||||
new_callable=PropertyMock(return_value=True),
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.unique_id", new=UniqueIdMock()
|
||||
), patch(
|
||||
"homeassistant.components.camera.Camera.device_info",
|
||||
new_callable=PropertyMock(return_value=dev_info),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_camera_with_no_name")
|
||||
async def mock_camera_with_no_name_fixture(mock_camera_with_device):
|
||||
"""Initialize a demo camera platform with a device and no name."""
|
||||
with patch(
|
||||
"homeassistant.components.camera.Camera._attr_name",
|
||||
new_callable=PropertyMock(return_value=None),
|
||||
):
|
||||
yield
|
||||
|
@ -16,6 +16,26 @@ async def setup_media_source(hass):
|
||||
assert await async_setup_component(hass, "media_source", {})
|
||||
|
||||
|
||||
async def test_device_with_device(
|
||||
hass: HomeAssistant, mock_camera_with_device, mock_camera
|
||||
) -> None:
|
||||
"""Test browsing when camera has a device and a name."""
|
||||
item = await media_source.async_browse_media(hass, "media-source://camera")
|
||||
assert item.not_shown == 2
|
||||
assert len(item.children) == 1
|
||||
assert item.children[0].title == "Test Camera Device Demo camera without stream"
|
||||
|
||||
|
||||
async def test_device_with_no_name(
|
||||
hass: HomeAssistant, mock_camera_with_no_name, mock_camera
|
||||
) -> None:
|
||||
"""Test browsing when camera has device and name == None."""
|
||||
item = await media_source.async_browse_media(hass, "media-source://camera")
|
||||
assert item.not_shown == 2
|
||||
assert len(item.children) == 1
|
||||
assert item.children[0].title == "Test Camera Device Demo camera without stream"
|
||||
|
||||
|
||||
async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None:
|
||||
"""Test browsing HLS camera media source."""
|
||||
item = await media_source.async_browse_media(hass, "media-source://camera")
|
||||
@ -41,6 +61,7 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None:
|
||||
assert len(item.children) == 1
|
||||
assert item.not_shown == 2
|
||||
assert item.children[0].media_content_type == "image/jpg"
|
||||
assert item.children[0].title == "Demo camera without stream"
|
||||
|
||||
|
||||
async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None:
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Test sensor of GIOS integration."""
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
@ -276,22 +278,24 @@ async def test_availability(hass: HomeAssistant) -> None:
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
incomplete_sensors = deepcopy(sensors)
|
||||
incomplete_sensors["pm2.5"] = {}
|
||||
future = utcnow() + timedelta(minutes=120)
|
||||
with patch(
|
||||
"homeassistant.components.gios.Gios._get_all_sensors",
|
||||
return_value=sensors,
|
||||
return_value=incomplete_sensors,
|
||||
), patch(
|
||||
"homeassistant.components.gios.Gios._get_indexes",
|
||||
return_value={},
|
||||
@ -299,21 +303,22 @@ async def test_availability(hass: HomeAssistant) -> None:
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == "4"
|
||||
# There is no PM2.5 data so the state should be unavailable
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Indexes are empty so the state should be unavailable
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
# Indexes are empty so the state should be unavailable
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Indexes are empty so the state should be unavailable
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
# Indexes are empty so the state should be unavailable
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
future = utcnow() + timedelta(minutes=180)
|
||||
future = utcnow() + timedelta(minutes=180)
|
||||
with patch(
|
||||
"homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors
|
||||
), patch(
|
||||
@ -323,17 +328,17 @@ async def test_availability(hass: HomeAssistant) -> None:
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == "4"
|
||||
state = hass.states.get("sensor.home_pm2_5")
|
||||
assert state
|
||||
assert state.state == "4"
|
||||
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
state = hass.states.get("sensor.home_pm2_5_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
state = hass.states.get("sensor.home_air_quality_index")
|
||||
assert state
|
||||
assert state.state == "good"
|
||||
|
||||
|
||||
async def test_invalid_indexes(hass: HomeAssistant) -> None:
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Tests for diagnostics platform of google calendar."""
|
||||
from collections.abc import Callable
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
@ -15,6 +16,7 @@ from .conftest import TEST_EVENT, ComponentSetup
|
||||
|
||||
from tests.common import CLIENT_ID, MockConfigEntry, MockUser
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@ -69,8 +71,21 @@ async def test_diagnostics(
|
||||
aiohttp_client: ClientSessionGenerator,
|
||||
socket_enabled: None,
|
||||
snapshot: SnapshotAssertion,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test diagnostics for the calendar."""
|
||||
|
||||
expires_in = 86400
|
||||
expires_at = time.time() + expires_in
|
||||
aioclient_mock.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
json={
|
||||
"refresh_token": "some-refresh-token",
|
||||
"access_token": "some-updated-token",
|
||||
"expires_at": expires_at,
|
||||
"expires_in": expires_in,
|
||||
},
|
||||
)
|
||||
mock_events_list_items(
|
||||
[
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Tests for the Google Assistant traits."""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@ -3925,16 +3926,15 @@ async def test_air_quality_description_for_aqi(hass: HomeAssistant) -> None:
|
||||
BASIC_CONFIG,
|
||||
)
|
||||
|
||||
assert trt._air_quality_description_for_aqi("0") == "healthy"
|
||||
assert trt._air_quality_description_for_aqi("75") == "moderate"
|
||||
assert trt._air_quality_description_for_aqi(0) == "healthy"
|
||||
assert trt._air_quality_description_for_aqi(75) == "moderate"
|
||||
assert (
|
||||
trt._air_quality_description_for_aqi("125") == "unhealthy for sensitive groups"
|
||||
trt._air_quality_description_for_aqi(125.0) == "unhealthy for sensitive groups"
|
||||
)
|
||||
assert trt._air_quality_description_for_aqi("175") == "unhealthy"
|
||||
assert trt._air_quality_description_for_aqi("250") == "very unhealthy"
|
||||
assert trt._air_quality_description_for_aqi("350") == "hazardous"
|
||||
assert trt._air_quality_description_for_aqi("-1") == "unknown"
|
||||
assert trt._air_quality_description_for_aqi("non-numeric") == "unknown"
|
||||
assert trt._air_quality_description_for_aqi(175) == "unhealthy"
|
||||
assert trt._air_quality_description_for_aqi(250) == "very unhealthy"
|
||||
assert trt._air_quality_description_for_aqi(350) == "hazardous"
|
||||
assert trt._air_quality_description_for_aqi(-1) == "unknown"
|
||||
|
||||
|
||||
async def test_null_device_class(hass: HomeAssistant) -> None:
|
||||
@ -3955,7 +3955,19 @@ async def test_null_device_class(hass: HomeAssistant) -> None:
|
||||
assert trt.query_attributes() == {}
|
||||
|
||||
|
||||
async def test_sensorstate(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("value", "published", "aqi"),
|
||||
[
|
||||
(100.0, 100.0, "moderate"),
|
||||
(10.0, 10.0, "healthy"),
|
||||
(0, 0.0, "healthy"),
|
||||
("", None, "unknown"),
|
||||
("unknown", None, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_sensorstate(
|
||||
hass: HomeAssistant, value: Any, published: Any, aqi: Any
|
||||
) -> None:
|
||||
"""Test SensorState trait support for sensor domain."""
|
||||
sensor_types = {
|
||||
sensor.SensorDeviceClass.AQI: ("AirQuality", "AQI"),
|
||||
@ -3977,7 +3989,7 @@ async def test_sensorstate(hass: HomeAssistant) -> None:
|
||||
hass,
|
||||
State(
|
||||
"sensor.test",
|
||||
100.0,
|
||||
value,
|
||||
{
|
||||
"device_class": sensor_type,
|
||||
},
|
||||
@ -4023,16 +4035,14 @@ async def test_sensorstate(hass: HomeAssistant) -> None:
|
||||
"currentSensorStateData": [
|
||||
{
|
||||
"name": name,
|
||||
"currentSensorState": trt._air_quality_description_for_aqi(
|
||||
trt.state.state
|
||||
),
|
||||
"rawValue": trt.state.state,
|
||||
"currentSensorState": aqi,
|
||||
"rawValue": published,
|
||||
},
|
||||
]
|
||||
}
|
||||
else:
|
||||
assert trt.query_attributes() == {
|
||||
"currentSensorStateData": [{"name": name, "rawValue": trt.state.state}]
|
||||
"currentSensorStateData": [{"name": name, "rawValue": published}]
|
||||
}
|
||||
|
||||
assert helpers.get_google_type(sensor.DOMAIN, None) is not None
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Test issues from supervisor issues."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
@ -13,7 +15,7 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_init import MOCK_ENVIRON
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@ -40,6 +42,7 @@ def mock_resolution_info(
|
||||
unsupported: list[str] | None = None,
|
||||
unhealthy: list[str] | None = None,
|
||||
issues: list[dict[str, str]] | None = None,
|
||||
suggestion_result: str = "ok",
|
||||
):
|
||||
"""Mock resolution/info endpoint with unsupported/unhealthy reasons and/or issues."""
|
||||
aioclient_mock.get(
|
||||
@ -76,7 +79,7 @@ def mock_resolution_info(
|
||||
for suggestion in suggestions:
|
||||
aioclient_mock.post(
|
||||
f"http://127.0.0.1/resolution/suggestion/{suggestion['uuid']}",
|
||||
json={"result": "ok"},
|
||||
json={"result": suggestion_result},
|
||||
)
|
||||
|
||||
|
||||
@ -528,6 +531,80 @@ async def test_supervisor_issues(
|
||||
)
|
||||
|
||||
|
||||
async def test_supervisor_issues_initial_failure(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test issues manager retries after initial update failure."""
|
||||
responses = [
|
||||
AiohttpClientMockResponse(
|
||||
method="get",
|
||||
url="http://127.0.0.1/resolution/info",
|
||||
status=HTTPStatus.BAD_REQUEST,
|
||||
json={
|
||||
"result": "error",
|
||||
"message": "System is not ready with state: setup",
|
||||
},
|
||||
),
|
||||
AiohttpClientMockResponse(
|
||||
method="get",
|
||||
url="http://127.0.0.1/resolution/info",
|
||||
status=HTTPStatus.OK,
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "reboot_required",
|
||||
"context": "system",
|
||||
"reference": None,
|
||||
},
|
||||
],
|
||||
"checks": [
|
||||
{"enabled": True, "slug": "supervisor_trust"},
|
||||
{"enabled": True, "slug": "free_space"},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
async def mock_responses(*args):
|
||||
nonlocal responses
|
||||
return responses.pop(0)
|
||||
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
side_effect=mock_responses,
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/issue/1234/suggestions",
|
||||
json={"result": "ok", "data": {"suggestions": []}},
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.hassio.issues.REQUEST_REFRESH_DELAY", new=0.1):
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 0
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
|
||||
|
||||
async def test_supervisor_issues_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
|
@ -404,6 +404,78 @@ async def test_supervisor_issue_repair_flow_skip_confirmation(
|
||||
)
|
||||
|
||||
|
||||
async def test_mount_failed_repair_flow_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test repair flow fails when repair fails to apply."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock,
|
||||
issues=[
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "mount_failed",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": "1235",
|
||||
"type": "execute_reload",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
},
|
||||
{
|
||||
"uuid": "1236",
|
||||
"type": "execute_remove",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
suggestion_result=False,
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
|
||||
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
assert repair_issue
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post(
|
||||
"/api/repairs/issues/fix",
|
||||
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
flow_id = data["flow_id"]
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/repairs/issues/fix/{flow_id}",
|
||||
json={"next_step_id": "mount_execute_reload"},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "abort",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"reason": "apply_suggestion_fail",
|
||||
"result": None,
|
||||
"description_placeholders": None,
|
||||
}
|
||||
|
||||
assert issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
|
||||
|
||||
async def test_mount_failed_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
|
@ -171,6 +171,7 @@ async def test_websocket_supervisor_api_error(
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ping",
|
||||
json={"result": "error", "message": "example error"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
await websocket_client.send_json(
|
||||
@ -183,9 +184,39 @@ async def test_websocket_supervisor_api_error(
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["error"]["code"] == "unknown_error"
|
||||
assert msg["error"]["message"] == "example error"
|
||||
|
||||
|
||||
async def test_websocket_supervisor_api_error_without_msg(
|
||||
hassio_env,
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test Supervisor websocket api error."""
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
websocket_client = await hass_ws_client(hass)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ping",
|
||||
json={},
|
||||
status=400,
|
||||
)
|
||||
|
||||
await websocket_client.send_json(
|
||||
{
|
||||
WS_ID: 1,
|
||||
WS_TYPE: WS_TYPE_API,
|
||||
ATTR_ENDPOINT: "/ping",
|
||||
ATTR_METHOD: "get",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["error"]["code"] == "unknown_error"
|
||||
assert msg["error"]["message"] == ""
|
||||
|
||||
|
||||
async def test_websocket_non_admin_user(
|
||||
hassio_env,
|
||||
hass: HomeAssistant,
|
||||
|
@ -62,6 +62,18 @@
|
||||
"connected": true,
|
||||
"statusTimestamp": 1697669932683
|
||||
},
|
||||
"workAreas": [
|
||||
{
|
||||
"workAreaId": 123456,
|
||||
"name": "Front lawn",
|
||||
"cuttingHeight": 50
|
||||
},
|
||||
{
|
||||
"workAreaId": 0,
|
||||
"name": "",
|
||||
"cuttingHeight": 50
|
||||
}
|
||||
],
|
||||
"positions": [
|
||||
{
|
||||
"latitude": 35.5402913,
|
||||
@ -120,10 +132,6 @@
|
||||
"longitude": -82.5520054
|
||||
}
|
||||
],
|
||||
"cuttingHeight": 4,
|
||||
"headlight": {
|
||||
"mode": "EVENING_ONLY"
|
||||
},
|
||||
"statistics": {
|
||||
"cuttingBladeUsageTime": 123,
|
||||
"numberOfChargingCycles": 1380,
|
||||
@ -133,6 +141,20 @@
|
||||
"totalDriveDistance": 1780272,
|
||||
"totalRunningTime": 4564800,
|
||||
"totalSearchingTime": 370800
|
||||
},
|
||||
"stayOutZones": {
|
||||
"dirty": false,
|
||||
"zones": [
|
||||
{
|
||||
"id": "81C6EEA2-D139-4FEA-B134-F22A6B3EA403",
|
||||
"name": "Springflowers",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"cuttingHeight": 4,
|
||||
"headlight": {
|
||||
"mode": "EVENING_ONLY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
100
tests/components/ipp/snapshots/test_diagnostics.ambr
Normal file
100
tests/components/ipp/snapshots/test_diagnostics.ambr
Normal file
@ -0,0 +1,100 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'data': dict({
|
||||
'info': dict({
|
||||
'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF',
|
||||
'location': None,
|
||||
'manufacturer': 'TEST',
|
||||
'model': 'HA-1000 Series',
|
||||
'more_info': 'http://192.168.1.31:80/PRESENTATION/BONJOUR',
|
||||
'name': 'Test HA-1000 Series',
|
||||
'printer_info': 'Test HA-1000 Series',
|
||||
'printer_name': 'Test Printer',
|
||||
'printer_uri_supported': list([
|
||||
'ipps://192.168.1.31:631/ipp/print',
|
||||
'ipp://192.168.1.31:631/ipp/print',
|
||||
]),
|
||||
'serial': '555534593035345555',
|
||||
'uptime': 30,
|
||||
'uuid': 'cfe92100-67c4-11d4-a45f-f8d027761251',
|
||||
'version': '20.23.06HA',
|
||||
}),
|
||||
'markers': list([
|
||||
dict({
|
||||
'color': '#000000',
|
||||
'high_level': 100,
|
||||
'level': 58,
|
||||
'low_level': 10,
|
||||
'marker_id': 0,
|
||||
'marker_type': 'ink-cartridge',
|
||||
'name': 'Black ink',
|
||||
}),
|
||||
dict({
|
||||
'color': '#00FFFF',
|
||||
'high_level': 100,
|
||||
'level': 91,
|
||||
'low_level': 10,
|
||||
'marker_id': 2,
|
||||
'marker_type': 'ink-cartridge',
|
||||
'name': 'Cyan ink',
|
||||
}),
|
||||
dict({
|
||||
'color': '#FF00FF',
|
||||
'high_level': 100,
|
||||
'level': 73,
|
||||
'low_level': 10,
|
||||
'marker_id': 4,
|
||||
'marker_type': 'ink-cartridge',
|
||||
'name': 'Magenta ink',
|
||||
}),
|
||||
dict({
|
||||
'color': '#000000',
|
||||
'high_level': 100,
|
||||
'level': 98,
|
||||
'low_level': 10,
|
||||
'marker_id': 1,
|
||||
'marker_type': 'ink-cartridge',
|
||||
'name': 'Photo black ink',
|
||||
}),
|
||||
dict({
|
||||
'color': '#FFFF00',
|
||||
'high_level': 100,
|
||||
'level': 95,
|
||||
'low_level': 10,
|
||||
'marker_id': 3,
|
||||
'marker_type': 'ink-cartridge',
|
||||
'name': 'Yellow ink',
|
||||
}),
|
||||
]),
|
||||
'state': dict({
|
||||
'message': None,
|
||||
'printer_state': 'idle',
|
||||
'reasons': None,
|
||||
}),
|
||||
'uris': list([
|
||||
dict({
|
||||
'authentication': None,
|
||||
'security': 'tls',
|
||||
'uri': 'ipps://192.168.1.31:631/ipp/print',
|
||||
}),
|
||||
dict({
|
||||
'authentication': None,
|
||||
'security': None,
|
||||
'uri': 'ipp://192.168.1.31:631/ipp/print',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
'entry': dict({
|
||||
'data': dict({
|
||||
'base_path': '/ipp/print',
|
||||
'host': '192.168.1.31',
|
||||
'port': 631,
|
||||
'ssl': False,
|
||||
'uuid': 'cfe92100-67c4-11d4-a45f-f8d027761251',
|
||||
'verify_ssl': True,
|
||||
}),
|
||||
'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251',
|
||||
}),
|
||||
})
|
||||
# ---
|
22
tests/components/ipp/test_diagnostics.py
Normal file
22
tests/components/ipp/test_diagnostics.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration."""
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics for config entry."""
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
|
||||
== snapshot
|
||||
)
|
124
tests/components/juicenet/test_config_flow.py
Normal file
124
tests/components/juicenet/test_config_flow.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Test the JuiceNet config flow."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import aiohttp
|
||||
from pyjuicenet import TokenError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.juicenet.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
def _mock_juicenet_return_value(get_devices=None):
|
||||
juicenet_mock = MagicMock()
|
||||
type(juicenet_mock).get_devices = MagicMock(return_value=get_devices)
|
||||
return juicenet_mock
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.juicenet.config_flow.Api.get_devices",
|
||||
return_value=MagicMock(),
|
||||
), patch(
|
||||
"homeassistant.components.juicenet.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.juicenet.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "JuiceNet"
|
||||
assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.juicenet.config_flow.Api.get_devices",
|
||||
side_effect=TokenError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"}
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.juicenet.config_flow.Api.get_devices",
|
||||
side_effect=aiohttp.ClientError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"}
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_catch_unknown_errors(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.juicenet.config_flow.Api.get_devices",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"}
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_import(hass: HomeAssistant) -> None:
|
||||
"""Test that import works as expected."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.juicenet.config_flow.Api.get_devices",
|
||||
return_value=MagicMock(),
|
||||
), patch(
|
||||
"homeassistant.components.juicenet.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.juicenet.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={CONF_ACCESS_TOKEN: "access_token"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "JuiceNet"
|
||||
assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user