mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
commit
63fda29eaf
@ -151,6 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
device['name'] = '{} {}'.format(device['id'], ipaddr)
|
device['name'] = '{} {}'.format(device['id'], ipaddr)
|
||||||
device[ATTR_MODE] = MODE_RGBW
|
device[ATTR_MODE] = MODE_RGBW
|
||||||
device[CONF_PROTOCOL] = None
|
device[CONF_PROTOCOL] = None
|
||||||
|
device[CONF_CUSTOM_EFFECT] = None
|
||||||
light = FluxLight(device)
|
light = FluxLight(device)
|
||||||
lights.append(light)
|
lights.append(light)
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ class PersonManager:
|
|||||||
CONF_ID: uuid.uuid4().hex,
|
CONF_ID: uuid.uuid4().hex,
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
CONF_USER_ID: user_id,
|
CONF_USER_ID: user_id,
|
||||||
CONF_DEVICE_TRACKERS: device_trackers,
|
CONF_DEVICE_TRACKERS: device_trackers or [],
|
||||||
}
|
}
|
||||||
self.storage_data[person[CONF_ID]] = person
|
self.storage_data[person[CONF_ID]] = person
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
@ -337,6 +337,12 @@ class Person(RestoreEntity):
|
|||||||
if state:
|
if state:
|
||||||
self._parse_source_state(state)
|
self._parse_source_state(state)
|
||||||
|
|
||||||
|
if self.hass.is_running:
|
||||||
|
# Update person now if hass is already running.
|
||||||
|
self.person_updated()
|
||||||
|
else:
|
||||||
|
# Wait for hass start to not have race between person
|
||||||
|
# and device trackers finishing setup.
|
||||||
@callback
|
@callback
|
||||||
def person_start_hass(now):
|
def person_start_hass(now):
|
||||||
self.person_updated()
|
self.person_updated()
|
||||||
|
@ -31,6 +31,11 @@ def _platform_validator(config):
|
|||||||
platform = importlib.import_module(
|
platform = importlib.import_module(
|
||||||
'homeassistant.components.scene.{}'.format(
|
'homeassistant.components.scene.{}'.format(
|
||||||
config[CONF_PLATFORM]))
|
config[CONF_PLATFORM]))
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
platform = importlib.import_module(
|
||||||
|
'homeassistant.components.{}.scene'.format(
|
||||||
|
config[CONF_PLATFORM]))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise vol.Invalid('Invalid platform specified') from None
|
raise vol.Invalid('Invalid platform specified') from None
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
"""Support for climate devices through the SmartThings cloud API."""
|
"""Support for climate devices through the SmartThings cloud API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional, Sequence
|
import logging
|
||||||
|
from typing import Iterable, Optional, Sequence
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE,
|
STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE,
|
||||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||||
ClimateDevice)
|
ClimateDevice, DOMAIN as CLIMATE_DOMAIN)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ UNIT_MAP = {
|
|||||||
'F': TEMP_FAHRENHEIT
|
'F': TEMP_FAHRENHEIT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_platform(
|
||||||
hass, config, async_add_entities, discovery_info=None):
|
hass, config, async_add_entities, discovery_info=None):
|
||||||
@ -50,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
|
broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[SmartThingsThermostat(device) for device in broker.devices.values()
|
[SmartThingsThermostat(device) for device in broker.devices.values()
|
||||||
if broker.any_assigned(device.device_id, 'climate')])
|
if broker.any_assigned(device.device_id, CLIMATE_DOMAIN)], True)
|
||||||
|
|
||||||
|
|
||||||
def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
|
def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]:
|
||||||
@ -90,6 +93,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):
|
|||||||
"""Init the class."""
|
"""Init the class."""
|
||||||
super().__init__(device)
|
super().__init__(device)
|
||||||
self._supported_features = self._determine_features()
|
self._supported_features = self._determine_features()
|
||||||
|
self._current_operation = None
|
||||||
|
self._operations = None
|
||||||
|
|
||||||
def _determine_features(self):
|
def _determine_features(self):
|
||||||
from pysmartthings import Capability
|
from pysmartthings import Capability
|
||||||
@ -127,6 +132,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):
|
|||||||
if operation_state:
|
if operation_state:
|
||||||
mode = STATE_TO_MODE[operation_state]
|
mode = STATE_TO_MODE[operation_state]
|
||||||
await self._device.set_thermostat_mode(mode, set_status=True)
|
await self._device.set_thermostat_mode(mode, set_status=True)
|
||||||
|
await self.async_update()
|
||||||
|
|
||||||
# Heat/cool setpoint
|
# Heat/cool setpoint
|
||||||
heating_setpoint = None
|
heating_setpoint = None
|
||||||
@ -151,6 +157,33 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):
|
|||||||
# the entity state ahead of receiving the confirming push updates
|
# the entity state ahead of receiving the confirming push updates
|
||||||
self.async_schedule_update_ha_state(True)
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the attributes of the climate device."""
|
||||||
|
thermostat_mode = self._device.status.thermostat_mode
|
||||||
|
self._current_operation = MODE_TO_STATE.get(thermostat_mode)
|
||||||
|
if self._current_operation is None:
|
||||||
|
_LOGGER.debug('Device %s (%s) returned an invalid'
|
||||||
|
'thermostat mode: %s', self._device.label,
|
||||||
|
self._device.device_id, thermostat_mode)
|
||||||
|
|
||||||
|
supported_modes = self._device.status.supported_thermostat_modes
|
||||||
|
if isinstance(supported_modes, Iterable):
|
||||||
|
operations = set()
|
||||||
|
for mode in supported_modes:
|
||||||
|
state = MODE_TO_STATE.get(mode)
|
||||||
|
if state is not None:
|
||||||
|
operations.add(state)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug('Device %s (%s) returned an invalid '
|
||||||
|
'supported thermostat mode: %s',
|
||||||
|
self._device.label, self._device.device_id,
|
||||||
|
mode)
|
||||||
|
self._operations = operations
|
||||||
|
else:
|
||||||
|
_LOGGER.debug('Device %s (%s) returned invalid supported '
|
||||||
|
'thermostat modes: %s', self._device.label,
|
||||||
|
self._device.device_id, supported_modes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_fan_mode(self):
|
def current_fan_mode(self):
|
||||||
"""Return the fan setting."""
|
"""Return the fan setting."""
|
||||||
@ -164,7 +197,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def current_operation(self):
|
def current_operation(self):
|
||||||
"""Return current operation ie. heat, cool, idle."""
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
return MODE_TO_STATE[self._device.status.thermostat_mode]
|
return self._current_operation
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
@ -187,8 +220,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def operation_list(self):
|
def operation_list(self):
|
||||||
"""Return the list of available operation modes."""
|
"""Return the list of available operation modes."""
|
||||||
return {MODE_TO_STATE[mode] for mode in
|
return self._operations
|
||||||
self._device.status.supported_thermostat_modes}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['PyXiaomiGateway==0.11.1']
|
REQUIREMENTS = ['PyXiaomiGateway==0.11.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -99,8 +99,10 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
|
|||||||
|
|
||||||
if self._write_to_hub(self._sid, **{self._data_key: rgbhex}):
|
if self._write_to_hub(self._sid, **{self._data_key: rgbhex}):
|
||||||
self._state = True
|
self._state = True
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
if self._write_to_hub(self._sid, **{self._data_key: 0}):
|
if self._write_to_hub(self._sid, **{self._data_key: 0}):
|
||||||
self._state = False
|
self._state = False
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 88
|
MINOR_VERSION = 88
|
||||||
PATCH_VERSION = '1'
|
PATCH_VERSION = '2'
|
||||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||||
|
@ -26,3 +26,7 @@ pycrypto==1000000000.0.0
|
|||||||
|
|
||||||
# Contains code to modify Home Assistant to work around our rules
|
# Contains code to modify Home Assistant to work around our rules
|
||||||
python-systemair-savecair==1000000000.0.0
|
python-systemair-savecair==1000000000.0.0
|
||||||
|
|
||||||
|
# Newer version causes pylint to take forever
|
||||||
|
# https://github.com/timothycrosley/isort/issues/848
|
||||||
|
isort==4.3.4
|
||||||
|
@ -63,7 +63,7 @@ PySwitchbot==0.5
|
|||||||
PyTransportNSW==0.1.1
|
PyTransportNSW==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_aqara
|
# homeassistant.components.xiaomi_aqara
|
||||||
PyXiaomiGateway==0.11.1
|
PyXiaomiGateway==0.11.2
|
||||||
|
|
||||||
# homeassistant.components.rpi_gpio
|
# homeassistant.components.rpi_gpio
|
||||||
# RPi.GPIO==0.6.5
|
# RPi.GPIO==0.6.5
|
||||||
|
@ -157,6 +157,10 @@ pycrypto==1000000000.0.0
|
|||||||
|
|
||||||
# Contains code to modify Home Assistant to work around our rules
|
# Contains code to modify Home Assistant to work around our rules
|
||||||
python-systemair-savecair==1000000000.0.0
|
python-systemair-savecair==1000000000.0.0
|
||||||
|
|
||||||
|
# Newer version causes pylint to take forever
|
||||||
|
# https://github.com/timothycrosley/isort/issues/848
|
||||||
|
isort==4.3.4
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ async def test_valid_invalid_user_ids(hass, hass_admin_user):
|
|||||||
|
|
||||||
async def test_setup_tracker(hass, hass_admin_user):
|
async def test_setup_tracker(hass, hass_admin_user):
|
||||||
"""Test set up person with one device tracker."""
|
"""Test set up person with one device tracker."""
|
||||||
|
hass.state = CoreState.not_running
|
||||||
user_id = hass_admin_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
@ -148,6 +149,7 @@ async def test_setup_tracker(hass, hass_admin_user):
|
|||||||
|
|
||||||
async def test_setup_two_trackers(hass, hass_admin_user):
|
async def test_setup_two_trackers(hass, hass_admin_user):
|
||||||
"""Test set up person with two device trackers."""
|
"""Test set up person with two device trackers."""
|
||||||
|
hass.state = CoreState.not_running
|
||||||
user_id = hass_admin_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
@ -191,6 +193,7 @@ async def test_setup_two_trackers(hass, hass_admin_user):
|
|||||||
|
|
||||||
async def test_ignore_unavailable_states(hass, hass_admin_user):
|
async def test_ignore_unavailable_states(hass, hass_admin_user):
|
||||||
"""Test set up person with two device trackers, one unavailable."""
|
"""Test set up person with two device trackers, one unavailable."""
|
||||||
|
hass.state = CoreState.not_running
|
||||||
user_id = hass_admin_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
@ -234,7 +237,7 @@ async def test_restore_home_state(hass, hass_admin_user):
|
|||||||
ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id}
|
ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id}
|
||||||
state = State('person.tracked_person', 'home', attrs)
|
state = State('person.tracked_person', 'home', attrs)
|
||||||
mock_restore_cache(hass, (state, ))
|
mock_restore_cache(hass, (state, ))
|
||||||
hass.state = CoreState.starting
|
hass.state = CoreState.not_running
|
||||||
mock_component(hass, 'recorder')
|
mock_component(hass, 'recorder')
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
@ -263,6 +266,21 @@ async def test_duplicate_ids(hass, hass_admin_user):
|
|||||||
assert hass.states.get('person.test_user_2') is None
|
assert hass.states.get('person.test_user_2') is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_person_during_run(hass):
|
||||||
|
"""Test that person is updated if created while hass is running."""
|
||||||
|
config = {DOMAIN: {}}
|
||||||
|
assert await async_setup_component(hass, DOMAIN, config)
|
||||||
|
hass.states.async_set(DEVICE_TRACKER, 'home')
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.components.person.async_create_person(
|
||||||
|
'tracked person', device_trackers=[DEVICE_TRACKER])
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get('person.tracked_person')
|
||||||
|
assert state.state == 'home'
|
||||||
|
|
||||||
|
|
||||||
async def test_load_person_storage(hass, hass_admin_user, storage_setup):
|
async def test_load_person_storage(hass, hass_admin_user, storage_setup):
|
||||||
"""Test set up person from storage."""
|
"""Test set up person from storage."""
|
||||||
state = hass.states.get('person.tracked_person')
|
state = hass.states.get('person.tracked_person')
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.components.climate import (
|
|||||||
from homeassistant.components.smartthings import climate
|
from homeassistant.components.smartthings import climate
|
||||||
from homeassistant.components.smartthings.const import DOMAIN
|
from homeassistant.components.smartthings.const import DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE)
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_UNKNOWN)
|
||||||
|
|
||||||
from .conftest import setup_platform
|
from .conftest import setup_platform
|
||||||
|
|
||||||
@ -95,6 +95,25 @@ def thermostat_fixture(device_factory):
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="buggy_thermostat")
|
||||||
|
def buggy_thermostat_fixture(device_factory):
|
||||||
|
"""Fixture returns a buggy thermostat."""
|
||||||
|
device = device_factory(
|
||||||
|
"Buggy Thermostat",
|
||||||
|
capabilities=[
|
||||||
|
Capability.temperature_measurement,
|
||||||
|
Capability.thermostat_cooling_setpoint,
|
||||||
|
Capability.thermostat_heating_setpoint,
|
||||||
|
Capability.thermostat_mode],
|
||||||
|
status={
|
||||||
|
Attribute.thermostat_mode: 'heating',
|
||||||
|
Attribute.cooling_setpoint: 74,
|
||||||
|
Attribute.heating_setpoint: 68}
|
||||||
|
)
|
||||||
|
device.status.attributes[Attribute.temperature] = Status(70, 'F', None)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
async def test_async_setup_platform():
|
async def test_async_setup_platform():
|
||||||
"""Test setup platform does nothing (it uses config entries)."""
|
"""Test setup platform does nothing (it uses config entries)."""
|
||||||
await climate.async_setup_platform(None, None, None)
|
await climate.async_setup_platform(None, None, None)
|
||||||
@ -152,6 +171,29 @@ async def test_thermostat_entity_state(hass, thermostat):
|
|||||||
assert state.attributes[ATTR_CURRENT_HUMIDITY] == 34
|
assert state.attributes[ATTR_CURRENT_HUMIDITY] == 34
|
||||||
|
|
||||||
|
|
||||||
|
async def test_buggy_thermostat_entity_state(hass, buggy_thermostat):
|
||||||
|
"""Tests the state attributes properly match the thermostat type."""
|
||||||
|
await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat)
|
||||||
|
state = hass.states.get('climate.buggy_thermostat')
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
assert state.attributes[ATTR_SUPPORTED_FEATURES] == \
|
||||||
|
SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | \
|
||||||
|
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE
|
||||||
|
assert ATTR_OPERATION_LIST not in state.attributes
|
||||||
|
assert state.attributes[ATTR_TEMPERATURE] is None
|
||||||
|
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius
|
||||||
|
|
||||||
|
|
||||||
|
async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat):
|
||||||
|
"""Tests when an invalid operation mode is included."""
|
||||||
|
buggy_thermostat.status.update_attribute_value(
|
||||||
|
Attribute.supported_thermostat_modes,
|
||||||
|
['heat', 'emergency heat', 'other'])
|
||||||
|
await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat)
|
||||||
|
state = hass.states.get('climate.buggy_thermostat')
|
||||||
|
assert state.attributes[ATTR_OPERATION_LIST] == {'heat'}
|
||||||
|
|
||||||
|
|
||||||
async def test_set_fan_mode(hass, thermostat):
|
async def test_set_fan_mode(hass, thermostat):
|
||||||
"""Test the fan mode is set successfully."""
|
"""Test the fan mode is set successfully."""
|
||||||
await setup_platform(hass, CLIMATE_DOMAIN, thermostat)
|
await setup_platform(hass, CLIMATE_DOMAIN, thermostat)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user