mirror of
https://github.com/home-assistant/core.git
synced 2025-08-08 15:15:09 +02:00
Merge branch 'dev' into AddClimate_MideaCCM15
This commit is contained in:
@@ -637,7 +637,7 @@ class BluetoothManager:
|
||||
else:
|
||||
# We could write out every item in the typed dict here
|
||||
# but that would be a bit inefficient and verbose.
|
||||
callback_matcher.update(matcher) # type: ignore[typeddict-item]
|
||||
callback_matcher.update(matcher)
|
||||
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
|
||||
|
||||
connectable = callback_matcher[CONNECTABLE]
|
||||
|
@@ -45,6 +45,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
|
||||
"""Representation of a Broadlink light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
|
@@ -217,6 +217,9 @@ def async_log_errors(
|
||||
class DenonDevice(MediaPlayerEntity):
|
||||
"""Representation of a Denon Media Player Device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: DenonAVR,
|
||||
@@ -225,7 +228,6 @@ class DenonDevice(MediaPlayerEntity):
|
||||
update_audyssey: bool,
|
||||
) -> None:
|
||||
"""Initialize the device."""
|
||||
self._attr_name = receiver.name
|
||||
self._attr_unique_id = unique_id
|
||||
assert config_entry.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@@ -234,7 +236,7 @@ class DenonDevice(MediaPlayerEntity):
|
||||
identifiers={(DOMAIN, config_entry.unique_id)},
|
||||
manufacturer=config_entry.data[CONF_MANUFACTURER],
|
||||
model=config_entry.data[CONF_MODEL],
|
||||
name=config_entry.title,
|
||||
name=receiver.name,
|
||||
)
|
||||
self._attr_sound_mode_list = receiver.sound_mode_list
|
||||
|
||||
|
@@ -24,7 +24,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
@@ -117,3 +117,8 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
|
||||
self._attr_native_value = time
|
||||
super()._handle_coordinator_update()
|
||||
return
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Sensor only available when open."""
|
||||
return super().available and self._attr_native_value is not None
|
||||
|
@@ -16,11 +16,12 @@ from .coordinator import MicroBotDataUpdateCoordinator
|
||||
class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordinator]):
|
||||
"""Generic entity for all MicroBots."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator, config_entry):
|
||||
"""Initialise the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._address = self.coordinator.ble_device.address
|
||||
self._attr_name = "Push"
|
||||
self._attr_unique_id = self._address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
||||
|
@@ -24,6 +24,13 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"push": {
|
||||
"name": "Push"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"calibrate": {
|
||||
"name": "Calibrate",
|
||||
|
@@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||
class MicroBotBinarySwitch(MicroBotEntity, SwitchEntity):
|
||||
"""MicroBot switch class."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "push"
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
|
@@ -48,7 +48,7 @@ class LitterRobotBinarySensorEntity(LitterRobotEntity[_RobotT], BinarySensorEnti
|
||||
|
||||
|
||||
BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, ...]] = {
|
||||
LitterRobot: (
|
||||
LitterRobot: ( # type: ignore[type-abstract] # only used for isinstance check
|
||||
RobotBinarySensorEntityDescription[LitterRobot](
|
||||
key="sleeping",
|
||||
translation_key="sleeping",
|
||||
@@ -66,7 +66,7 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, .
|
||||
is_on_fn=lambda robot: robot.sleep_mode_enabled,
|
||||
),
|
||||
),
|
||||
Robot: (
|
||||
Robot: ( # type: ignore[type-abstract] # only used for isinstance check
|
||||
RobotBinarySensorEntityDescription[Robot](
|
||||
key="power_status",
|
||||
translation_key="power_status",
|
||||
|
@@ -48,7 +48,7 @@ class RobotSelectEntityDescription(
|
||||
|
||||
|
||||
ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = {
|
||||
LitterRobot: RobotSelectEntityDescription[LitterRobot, int](
|
||||
LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check
|
||||
key="cycle_delay",
|
||||
translation_key="cycle_delay",
|
||||
icon="mdi:timer-outline",
|
||||
|
@@ -66,7 +66,7 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity):
|
||||
|
||||
|
||||
ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = {
|
||||
LitterRobot: [
|
||||
LitterRobot: [ # type: ignore[type-abstract] # only used for isinstance check
|
||||
RobotSensorEntityDescription[LitterRobot](
|
||||
key="waste_drawer_level",
|
||||
translation_key="waste_drawer",
|
||||
|
@@ -75,6 +75,7 @@ FAN_INV_MODES = list(FAN_INV_MODE_MAP)
|
||||
MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API
|
||||
MIN_TEMP = 10
|
||||
MAX_TEMP = 32
|
||||
MIN_TEMP_RANGE = 1.66667
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -313,6 +314,13 @@ class ThermostatEntity(ClimateEntity):
|
||||
try:
|
||||
if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL:
|
||||
if low_temp and high_temp:
|
||||
if high_temp - low_temp < MIN_TEMP_RANGE:
|
||||
# Ensure there is a minimum gap from the new temp. Pick
|
||||
# the temp that is not changing as the one to move.
|
||||
if abs(high_temp - self.target_temperature_high) < 0.01:
|
||||
high_temp = low_temp + MIN_TEMP_RANGE
|
||||
else:
|
||||
low_temp = high_temp - MIN_TEMP_RANGE
|
||||
await trait.set_range(low_temp, high_temp)
|
||||
elif hvac_mode == HVACMode.COOL and temp:
|
||||
await trait.set_cool(temp)
|
||||
|
@@ -12,8 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
@@ -5,7 +5,6 @@ import logging
|
||||
import ssl
|
||||
from typing import Any
|
||||
|
||||
from jsonpath import jsonpath
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -39,13 +38,13 @@ from homeassistant.helpers.template_entity import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from . import async_get_config_and_coordinator, create_rest_data_from_config
|
||||
from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME
|
||||
from .data import RestData
|
||||
from .entity import RestEntity
|
||||
from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA
|
||||
from .util import parse_json_attributes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -163,32 +162,9 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity):
|
||||
value = self.rest.data_without_xml()
|
||||
|
||||
if self._json_attrs:
|
||||
if value:
|
||||
try:
|
||||
json_dict = json_loads(value)
|
||||
if self._json_attrs_path is not None:
|
||||
json_dict = jsonpath(json_dict, self._json_attrs_path)
|
||||
# jsonpath will always store the result in json_dict[0]
|
||||
# so the next line happens to work exactly as needed to
|
||||
# find the result
|
||||
if isinstance(json_dict, list):
|
||||
json_dict = json_dict[0]
|
||||
if isinstance(json_dict, dict):
|
||||
attrs = {
|
||||
k: json_dict[k] for k in self._json_attrs if k in json_dict
|
||||
}
|
||||
self._attr_extra_state_attributes = attrs
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"JSON result was not a dictionary"
|
||||
" or list with 0th element a dictionary"
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning("REST result could not be parsed as JSON")
|
||||
_LOGGER.debug("Erroneous JSON: %s", value)
|
||||
|
||||
else:
|
||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
self._attr_extra_state_attributes = parse_json_attributes(
|
||||
value, self._json_attrs, self._json_attrs_path
|
||||
)
|
||||
|
||||
raw_value = value
|
||||
|
||||
|
40
homeassistant/components/rest/util.py
Normal file
40
homeassistant/components/rest/util.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Helpers for RESTful API."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from jsonpath import jsonpath
|
||||
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_json_attributes(
|
||||
value: str | None, json_attrs: list[str], json_attrs_path: str | None
|
||||
) -> dict[str, Any]:
|
||||
"""Parse JSON attributes."""
|
||||
if not value:
|
||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
return {}
|
||||
|
||||
try:
|
||||
json_dict = json_loads(value)
|
||||
if json_attrs_path is not None:
|
||||
json_dict = jsonpath(json_dict, json_attrs_path)
|
||||
# jsonpath will always store the result in json_dict[0]
|
||||
# so the next line happens to work exactly as needed to
|
||||
# find the result
|
||||
if isinstance(json_dict, list):
|
||||
json_dict = json_dict[0]
|
||||
if isinstance(json_dict, dict):
|
||||
return {k: json_dict[k] for k in json_attrs if k in json_dict}
|
||||
|
||||
_LOGGER.warning(
|
||||
"JSON result was not a dictionary or list with 0th element a dictionary"
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning("REST result could not be parsed as JSON")
|
||||
_LOGGER.debug("Erroneous JSON: %s", value)
|
||||
|
||||
return {}
|
@@ -618,8 +618,13 @@ class EntityPlatform:
|
||||
**device_info,
|
||||
)
|
||||
except dev_reg.DeviceInfoError as exc:
|
||||
self.logger.error("Ignoring invalid device info: %s", str(exc))
|
||||
device = None
|
||||
self.logger.error(
|
||||
"%s: Not adding entity with invalid device info: %s",
|
||||
self.platform_name,
|
||||
str(exc),
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
else:
|
||||
device = None
|
||||
|
||||
|
2
mypy.ini
2
mypy.ini
@@ -17,7 +17,7 @@ warn_unused_configs = true
|
||||
warn_unused_ignores = true
|
||||
enable_error_code = ignore-without-code, redundant-self, truthy-iterable
|
||||
disable_error_code = annotation-unchecked
|
||||
strict_concatenate = false
|
||||
extra_checks = false
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
|
@@ -11,7 +11,7 @@ astroid==2.15.4
|
||||
coverage==7.2.7
|
||||
freezegun==1.2.2
|
||||
mock-open==1.4.0
|
||||
mypy==1.4.1
|
||||
mypy==1.5.0
|
||||
pre-commit==3.3.3
|
||||
pydantic==1.10.12
|
||||
pylint==2.17.4
|
||||
|
@@ -51,8 +51,9 @@ GENERAL_SETTINGS: Final[dict[str, str]] = {
|
||||
]
|
||||
),
|
||||
"disable_error_code": ", ".join(["annotation-unchecked"]),
|
||||
# Strict_concatenate breaks passthrough ParamSpec typing
|
||||
"strict_concatenate": "false",
|
||||
# Impractical in real code
|
||||
# E.g. this breaks passthrough ParamSpec typing with Concatenate
|
||||
"extra_checks": "false",
|
||||
}
|
||||
|
||||
# This is basically the list of checks which is enabled for "strict=true".
|
||||
|
@@ -35,7 +35,7 @@
|
||||
'entity_id': 'sensor.mock_title_valve_closing',
|
||||
'last_changed': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery]
|
||||
|
@@ -758,6 +758,75 @@ async def test_thermostat_set_temperature_hvac_mode(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("setpoint", "target_low", "target_high", "expected_params"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"heatCelsius": 19.0,
|
||||
"coolCelsius": 25.0,
|
||||
},
|
||||
19.0,
|
||||
20.0,
|
||||
# Cool is accepted and lowers heat by the min range
|
||||
{"heatCelsius": 18.33333, "coolCelsius": 20.0},
|
||||
),
|
||||
(
|
||||
{
|
||||
"heatCelsius": 19.0,
|
||||
"coolCelsius": 25.0,
|
||||
},
|
||||
24.0,
|
||||
25.0,
|
||||
# Cool is accepted and lowers heat by the min range
|
||||
{"heatCelsius": 24.0, "coolCelsius": 25.66667},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_thermostat_set_temperature_range_too_close(
|
||||
hass: HomeAssistant,
|
||||
setup_platform: PlatformSetup,
|
||||
auth: FakeAuth,
|
||||
create_device: CreateDevice,
|
||||
setpoint: dict[str, Any],
|
||||
target_low: float,
|
||||
target_high: float,
|
||||
expected_params: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test setting an HVAC temperature range that is too small of a range."""
|
||||
create_device.create(
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": setpoint,
|
||||
},
|
||||
)
|
||||
await setup_platform()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVACMode.HEAT_COOL
|
||||
|
||||
# Move the target temp to be in too small of a range
|
||||
await common.async_set_temperature(
|
||||
hass,
|
||||
target_temp_low=target_low,
|
||||
target_temp_high=target_high,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == DEVICE_COMMAND
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange",
|
||||
"params": expected_params,
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_set_heat_cool(
|
||||
hass: HomeAssistant,
|
||||
setup_platform: PlatformSetup,
|
||||
|
@@ -1853,23 +1853,27 @@ async def test_device_name_defaulting_config_entry(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_info"),
|
||||
("device_info", "number_of_entities"),
|
||||
[
|
||||
# No identifiers
|
||||
{},
|
||||
{"name": "bla"},
|
||||
{"default_name": "bla"},
|
||||
({}, 1), # Empty device info does not prevent the entity from being created
|
||||
({"name": "bla"}, 0),
|
||||
({"default_name": "bla"}, 0),
|
||||
# Match multiple types
|
||||
{
|
||||
"identifiers": {("hue", "1234")},
|
||||
"name": "bla",
|
||||
"default_name": "yo",
|
||||
},
|
||||
(
|
||||
{
|
||||
"identifiers": {("hue", "1234")},
|
||||
"name": "bla",
|
||||
"default_name": "yo",
|
||||
},
|
||||
0,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_device_type_error_checking(
|
||||
hass: HomeAssistant,
|
||||
device_info: dict,
|
||||
number_of_entities: int,
|
||||
) -> None:
|
||||
"""Test catching invalid device info."""
|
||||
|
||||
@@ -1895,6 +1899,6 @@ async def test_device_type_error_checking(
|
||||
|
||||
dev_reg = dr.async_get(hass)
|
||||
assert len(dev_reg.devices) == 0
|
||||
# Entity should still be registered
|
||||
ent_reg = er.async_get(hass)
|
||||
assert ent_reg.async_get("test_domain.test_qwer") is not None
|
||||
assert len(ent_reg.entities) == number_of_entities
|
||||
assert len(hass.states.async_all()) == number_of_entities
|
||||
|
Reference in New Issue
Block a user