0.27.2 (#3151)
* Host should be optional for apcupsd component (#3072) * Zwave climate Bugfix: if some setpoints have different units, we should fetch the o… (#3078) * Bugfix: if some setpoints have different units, we should fetch the one that are active. * Move order of population for first time detection * Default to config if None unit_of_measurement * unit fix (#3083) * humidity slider (#3088) * If device was off target temp was null. Default to Heating setpoint (#3091) * Fix for BLE device tracker (#3019) * Bug fix tracked devices * Added scan_duration configuration parameter * fix homematic climate implementation (#3114) * Allow 'None' MAC to be loaded from known_devices (#3102) * Climate and cover bugfix (#3097) * Avoid None comparison for zwave cover. * Just rely on unit from config for unit_of_measurement * Explicit return None * Mqtt (#11) * Explicit return None * Missing service and wrong service name defined * Mqtt state was inverted, and never triggering * Fixed Homematic cover (#3116) * Add missing docstrings (fix PEP257 issues) (#3098) * Add missing docstrings (fix PEP257 issues) * Finish sentence * Merge pull request #3130 from turbokongen/zwave_fixes Bugfix. climate and covermqt * Back out insteon hub and fan changes (#3062) * Bump version * Special frontend build for 0.27.2pull/3272/head^2 0.27.2
parent
dfc38b76a4
commit
64cc4a47ec
|
@ -32,7 +32,7 @@ VALUE_ONLINE = 'ONLINE'
|
|||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
|
|
@ -181,7 +181,7 @@ class Thermostat(ClimateDevice):
|
|||
else:
|
||||
operation = status
|
||||
return {
|
||||
"humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
|
|
|
@ -101,7 +101,7 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
|||
for mode, state in HM_STATE_MAP.items():
|
||||
if state == operation_mode:
|
||||
code = getattr(self._hmdevice, mode, 0)
|
||||
self._hmdevice.STATE = code
|
||||
self._hmdevice.MODE = code
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.components.climate import ClimateDevice
|
|||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -59,11 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveClimate(value)])
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
@ -73,7 +73,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
"""Represents a ZWave Climate device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, too-many-instance-attributes
|
||||
def __init__(self, value):
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
@ -87,7 +87,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
self._fan_list = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = None
|
||||
self._unit = temp_unit
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
|
@ -115,18 +116,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
self._unit = value.units
|
||||
if self.current_operation is not None:
|
||||
if SET_TEMP_TO_INDEX.get(self._current_operation) \
|
||||
!= value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
continue
|
||||
self._target_temperature = int(value.data)
|
||||
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
|
@ -140,6 +129,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
|
@ -158,6 +148,17 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if SET_TEMP_TO_INDEX.get(self._current_operation) \
|
||||
!= value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
continue
|
||||
self._target_temperature = int(value.data)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -187,14 +188,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
unit = self._unit
|
||||
if unit == 'C':
|
||||
if self._unit == 'C':
|
||||
return TEMP_CELSIUS
|
||||
elif unit == 'F':
|
||||
elif self._unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
_LOGGER.exception("unit_of_measurement=%s is not valid",
|
||||
unit)
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
|
|
@ -61,6 +61,7 @@ SERVICE_TO_METHOD = {
|
|||
SERVICE_STOP_COVER: {'method': 'stop_cover'},
|
||||
SERVICE_OPEN_COVER_TILT: {'method': 'open_cover_tilt'},
|
||||
SERVICE_CLOSE_COVER_TILT: {'method': 'close_cover_tilt'},
|
||||
SERVICE_STOP_COVER_TILT: {'method': 'stop_cover_tilt'},
|
||||
SERVICE_SET_COVER_TILT_POSITION: {
|
||||
'method': 'set_cover_tilt_position',
|
||||
'schema': COVER_SET_COVER_TILT_POSITION_SCHEMA},
|
||||
|
|
|
@ -11,7 +11,7 @@ properly configured.
|
|||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_CURRENT_POSITION
|
||||
ATTR_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -41,16 +41,16 @@ class HMCover(homematic.HMDevice, CoverDevice):
|
|||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if self.available:
|
||||
return int((1 - self._hm_get_state()) * 100)
|
||||
return int(self._hm_get_state() * 100)
|
||||
return None
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if self.available:
|
||||
if ATTR_CURRENT_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_CURRENT_POSITION])
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = (100 - position) / 100.0
|
||||
level = position / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
|
||||
@property
|
||||
|
|
|
@ -97,12 +97,16 @@ class MqttCover(CoverDevice):
|
|||
hass, value_template, payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
self.update_ha_state()
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
self._state = int(payload)
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
self.update_ha_state()
|
||||
else:
|
||||
|
@ -129,11 +133,7 @@ class MqttCover(CoverDevice):
|
|||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
|
|
@ -96,6 +96,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is None:
|
||||
return None
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
|
|
|
@ -388,7 +388,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
|||
try:
|
||||
return [
|
||||
Device(hass, consider_home, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
str(dev_id).lower(), None if device.get('mac') is None
|
||||
else str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get('gravatar'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
|
|
|
@ -2,16 +2,19 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.components.device_tracker import (
|
||||
YAML_DEVICES,
|
||||
CONF_TRACK_NEW,
|
||||
CONF_SCAN_INTERVAL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA,
|
||||
load_config,
|
||||
)
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -19,6 +22,11 @@ REQUIREMENTS = ['gattlib==0.20150805']
|
|||
|
||||
BLE_PREFIX = 'BLE_'
|
||||
MIN_SEEN_NEW = 5
|
||||
CONF_SCAN_DURATION = "scan_duration"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
|
@ -51,12 +59,13 @@ def setup_scanner(hass, config, see):
|
|||
"""Discover Bluetooth LE devices."""
|
||||
_LOGGER.debug("Discovering Bluetooth LE devices")
|
||||
service = DiscoveryService()
|
||||
devices = service.discover(10)
|
||||
devices = service.discover(duration)
|
||||
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
|
||||
|
||||
return devices
|
||||
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
duration = config.get(CONF_SCAN_DURATION)
|
||||
devs_to_track = []
|
||||
devs_donot_track = []
|
||||
|
||||
|
@ -65,11 +74,13 @@ def setup_scanner(hass, config, see):
|
|||
# to 0
|
||||
for device in load_config(yaml_path, hass, 0):
|
||||
# check if device is a valid bluetooth device
|
||||
if device.mac and device.mac[:3].upper() == BLE_PREFIX:
|
||||
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
|
||||
if device.track:
|
||||
devs_to_track.append(device.mac[3:])
|
||||
_LOGGER.debug("Adding %s to BLE tracker", device.mac)
|
||||
devs_to_track.append(device.mac[4:])
|
||||
else:
|
||||
devs_donot_track.append(device.mac[3:])
|
||||
_LOGGER.debug("Adding %s to BLE do not track", device.mac)
|
||||
devs_donot_track.append(device.mac[4:])
|
||||
|
||||
# if track new devices is true discover new devices
|
||||
# on every scan.
|
||||
|
@ -96,7 +107,7 @@ def setup_scanner(hass, config, see):
|
|||
if track_new:
|
||||
for address in devs:
|
||||
if address not in devs_to_track and \
|
||||
address not in devs_donot_track:
|
||||
address not in devs_donot_track:
|
||||
_LOGGER.info("Discovered Bluetooth LE device %s", address)
|
||||
see_device(address, devs[address], new_device=True)
|
||||
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
"""
|
||||
Support for Insteon FanLinc.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.insteon/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, SUPPORT_SET_SPEED,
|
||||
SPEED_OFF, SPEED_LOW, SPEED_MED,
|
||||
SPEED_HIGH)
|
||||
from homeassistant.components.insteon_hub import (InsteonDevice, INSTEON,
|
||||
filter_devices)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEVICE_CATEGORIES = [
|
||||
{
|
||||
'DevCat': 1,
|
||||
'SubCat': [46]
|
||||
}
|
||||
]
|
||||
|
||||
DEPENDENCIES = ['insteon_hub']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Insteon Hub fan platform."""
|
||||
devs = []
|
||||
for device in filter_devices(INSTEON.devices, DEVICE_CATEGORIES):
|
||||
devs.append(InsteonFanDevice(device))
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class InsteonFanDevice(InsteonDevice, FanEntity):
|
||||
"""Represet an insteon fan device."""
|
||||
|
||||
def __init__(self, node: object) -> None:
|
||||
"""Initialize the device."""
|
||||
super(InsteonFanDevice, self).__init__(node)
|
||||
self.speed = STATE_UNKNOWN # Insteon hub can't get state via REST
|
||||
|
||||
def turn_on(self, speed: str=None):
|
||||
"""Turn the fan on."""
|
||||
self.set_speed(speed if speed else SPEED_MED)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the fan off."""
|
||||
self.set_speed(SPEED_OFF)
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Set the fan speed."""
|
||||
if self._send_command('fan', payload={'speed', speed}):
|
||||
self.speed = speed
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Get the supported features for device."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the available speeds for the fan."""
|
||||
return [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "1fd10c1fcdf56a61f60cf861d5a0368c",
|
||||
"frontend.html": "88c97d278de3320278da6c32fe9e7d61",
|
||||
"frontend.html": "610cc799225ede933a9894b64bb35717",
|
||||
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 670ba0292bfca2b65aeca70804c0856b6cabf10e
|
||||
Subproject commit 659ec6552f761ff4779dd52ee35d26f7be5e111f
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -8,93 +8,37 @@ import logging
|
|||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = 'insteon_hub' # type: str
|
||||
REQUIREMENTS = ['insteon_hub==0.5.0'] # type: list
|
||||
INSTEON = None # type: Insteon
|
||||
DEVCAT = 'DevCat' # type: str
|
||||
SUBCAT = 'SubCat' # type: str
|
||||
DEVICE_CLASSES = ['light', 'fan'] # type: list
|
||||
|
||||
DOMAIN = "insteon_hub"
|
||||
REQUIREMENTS = ['insteon_hub==0.4.5']
|
||||
INSTEON = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_successful(response: dict) -> bool:
|
||||
"""Check http response for successful status."""
|
||||
return 'status' in response and response['status'] == 'succeeded'
|
||||
def setup(hass, config):
|
||||
"""Setup Insteon Hub component.
|
||||
|
||||
|
||||
def filter_devices(devices: list, categories: list) -> list:
|
||||
"""Filter insteon device list by category/subcategory."""
|
||||
categories = (categories
|
||||
if isinstance(categories, list)
|
||||
else [categories])
|
||||
matching_devices = []
|
||||
for device in devices:
|
||||
if any(
|
||||
device.DevCat == c[DEVCAT] and
|
||||
(SUBCAT not in c or device.SubCat in c[SUBCAT])
|
||||
for c in categories):
|
||||
matching_devices.append(device)
|
||||
return matching_devices
|
||||
|
||||
|
||||
def setup(hass, config: dict) -> bool:
|
||||
"""Setup Insteon Hub component."""
|
||||
This will automatically import associated lights.
|
||||
"""
|
||||
if not validate_config(
|
||||
config,
|
||||
{DOMAIN: [CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
from insteon import Insteon
|
||||
import insteon
|
||||
|
||||
username = config[DOMAIN][CONF_USERNAME]
|
||||
password = config[DOMAIN][CONF_PASSWORD]
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
global INSTEON
|
||||
INSTEON = Insteon(username, password, api_key)
|
||||
INSTEON = insteon.Insteon(username, password, api_key)
|
||||
|
||||
if INSTEON is None:
|
||||
_LOGGER.error('Could not connect to Insteon service.')
|
||||
_LOGGER.error("Could not connect to Insteon service.")
|
||||
return
|
||||
|
||||
for device_class in DEVICE_CLASSES:
|
||||
discovery.load_platform(hass, device_class, DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InsteonDevice(Entity):
|
||||
"""Represents an insteon device."""
|
||||
|
||||
def __init__(self: Entity, node: object) -> None:
|
||||
"""Initialize the insteon device."""
|
||||
self._node = node
|
||||
|
||||
def update(self: Entity) -> None:
|
||||
"""Update state of the device."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self: Entity) -> str:
|
||||
"""Name of the insteon device."""
|
||||
return self._node.DeviceName
|
||||
|
||||
@property
|
||||
def unique_id(self: Entity) -> str:
|
||||
"""Unique identifier for the device."""
|
||||
return self._node.DeviceID
|
||||
|
||||
@property
|
||||
def supported_features(self: Entity) -> int:
|
||||
"""Supported feature flags."""
|
||||
return 0
|
||||
|
||||
def _send_command(self: Entity, command: str, level: int=None,
|
||||
payload: dict=None) -> bool:
|
||||
"""Send command to insteon device."""
|
||||
resp = self._node.send_command(command, payload=payload, level=level,
|
||||
wait=True)
|
||||
return _is_successful(resp)
|
||||
|
|
|
@ -4,76 +4,74 @@ Support for Insteon Hub lights.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_hub/
|
||||
"""
|
||||
from homeassistant.components.insteon_hub import (INSTEON, InsteonDevice)
|
||||
from homeassistant.components.insteon_hub import INSTEON
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
||||
SUPPORT_BRIGHTNESS, Light)
|
||||
|
||||
SUPPORT_INSTEON_HUB = SUPPORT_BRIGHTNESS
|
||||
|
||||
DEPENDENCIES = ['insteon_hub']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Insteon Hub light platform."""
|
||||
devs = []
|
||||
for device in INSTEON.devices:
|
||||
if device.DeviceCategory == "Switched Lighting Control":
|
||||
devs.append(InsteonLightDevice(device))
|
||||
devs.append(InsteonToggleDevice(device))
|
||||
if device.DeviceCategory == "Dimmable Lighting Control":
|
||||
devs.append(InsteonDimmableDevice(device))
|
||||
devs.append(InsteonToggleDevice(device))
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class InsteonLightDevice(InsteonDevice, Light):
|
||||
"""A representation of a light device."""
|
||||
class InsteonToggleDevice(Light):
|
||||
"""An abstract Class for an Insteon node."""
|
||||
|
||||
def __init__(self, node: object) -> None:
|
||||
def __init__(self, node):
|
||||
"""Initialize the device."""
|
||||
super(InsteonLightDevice, self).__init__(node)
|
||||
self.node = node
|
||||
self._value = 0
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update state of the device."""
|
||||
resp = self._node.send_command('get_status', wait=True)
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
return self.node.DeviceName
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this insteon node."""
|
||||
return self.node.DeviceID
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._value / 100 * 255
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
resp = self.node.send_command('get_status', wait=True)
|
||||
try:
|
||||
self._value = resp['response']['level']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_on(self) -> None:
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
return self._value != 0
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn device on."""
|
||||
if self._send_command('on'):
|
||||
self._value = 100
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn device off."""
|
||||
if self._send_command('off'):
|
||||
self._value = 0
|
||||
|
||||
|
||||
class InsteonDimmableDevice(InsteonLightDevice):
|
||||
"""A representation for a dimmable device."""
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return round(self._value / 100 * 255, 0) # type: int
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_INSTEON_HUB
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn device on."""
|
||||
level = 100 # type: int
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
level = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100, 0) # type: int
|
||||
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
|
||||
self.node.send_command('on', self._value)
|
||||
else:
|
||||
self._value = 100
|
||||
self.node.send_command('on')
|
||||
|
||||
if self._send_command('on', level=level):
|
||||
self._value = level
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn device off."""
|
||||
self.node.send_command('off')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
|
||||
__version__ = '0.27.1'
|
||||
__version__ = '0.27.2'
|
||||
REQUIRED_PYTHON_VER = (3, 4)
|
||||
|
||||
PLATFORM_FORMAT = '{}.{}'
|
||||
|
@ -244,7 +244,7 @@ SERVICE_OPEN_COVER = 'open_cover'
|
|||
SERVICE_OPEN_COVER_TILT = 'open_cover_tilt'
|
||||
SERVICE_SET_COVER_POSITION = 'set_cover_position'
|
||||
SERVICE_SET_COVER_TILT_POSITION = 'set_cover_tilt_position'
|
||||
SERVICE_STOP_COVER = 'stop'
|
||||
SERVICE_STOP_COVER = 'stop_cover'
|
||||
SERVICE_STOP_COVER_TILT = 'stop_cover_tilt'
|
||||
|
||||
SERVICE_MOVE_UP = 'move_up'
|
||||
|
|
|
@ -204,7 +204,7 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
|
|||
influxdb==3.0.0
|
||||
|
||||
# homeassistant.components.insteon_hub
|
||||
insteon_hub==0.5.0
|
||||
insteon_hub==0.4.5
|
||||
|
||||
# homeassistant.components.media_player.kodi
|
||||
jsonrpc-requests==0.3
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
"""Tests for the insteon hub fan platform."""
|
||||
import unittest
|
||||
|
||||
from homeassistant.const import (STATE_OFF, STATE_ON)
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
|
||||
ATTR_SPEED)
|
||||
from homeassistant.components.fan.insteon_hub import (InsteonFanDevice,
|
||||
SUPPORT_SET_SPEED)
|
||||
|
||||
|
||||
class Node(object):
|
||||
"""Fake insteon node."""
|
||||
|
||||
def __init__(self, name, id, dev_cat, sub_cat):
|
||||
"""Initialize fake insteon node."""
|
||||
self.DeviceName = name
|
||||
self.DeviceID = id
|
||||
self.DevCat = dev_cat
|
||||
self.SubCat = sub_cat
|
||||
self.response = None
|
||||
|
||||
def send_command(self, command, payload, level, wait):
|
||||
"""Send fake command."""
|
||||
return self.response
|
||||
|
||||
|
||||
class TestInsteonHubFanDevice(unittest.TestCase):
|
||||
"""Test around insteon hub fan device methods."""
|
||||
|
||||
_NODE = Node('device', '12345', '1', '46')
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize test data."""
|
||||
self._DEVICE = InsteonFanDevice(self._NODE)
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test data."""
|
||||
self._DEVICE = None
|
||||
|
||||
def test_properties(self):
|
||||
"""Test basic properties."""
|
||||
self.assertEqual(self._NODE.DeviceName, self._DEVICE.name)
|
||||
self.assertEqual(self._NODE.DeviceID, self._DEVICE.unique_id)
|
||||
self.assertEqual(SUPPORT_SET_SPEED, self._DEVICE.supported_features)
|
||||
|
||||
for speed in [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]:
|
||||
self.assertIn(speed, self._DEVICE.speed_list)
|
||||
|
||||
def test_turn_on(self):
|
||||
"""Test the turning on device."""
|
||||
self._NODE.response = {
|
||||
'status': 'succeeded'
|
||||
}
|
||||
self.assertEqual(STATE_OFF, self._DEVICE.state)
|
||||
self._DEVICE.turn_on()
|
||||
|
||||
self.assertEqual(STATE_ON, self._DEVICE.state)
|
||||
|
||||
self._DEVICE.turn_on(SPEED_MED)
|
||||
|
||||
self.assertEqual(STATE_ON, self._DEVICE.state)
|
||||
self.assertEqual(SPEED_MED, self._DEVICE.state_attributes[ATTR_SPEED])
|
||||
|
||||
def test_turn_off(self):
|
||||
"""Test turning off device."""
|
||||
self._NODE.response = {
|
||||
'status': 'succeeded'
|
||||
}
|
||||
self.assertEqual(STATE_OFF, self._DEVICE.state)
|
||||
self._DEVICE.turn_on()
|
||||
self.assertEqual(STATE_ON, self._DEVICE.state)
|
||||
self._DEVICE.turn_off()
|
||||
self.assertEqual(STATE_OFF, self._DEVICE.state)
|
|
@ -1,3 +1,4 @@
|
|||
"""The tests for the emulated Hue component."""
|
||||
import time
|
||||
import json
|
||||
import threading
|
||||
|
@ -11,8 +12,7 @@ import homeassistant.components as core_components
|
|||
from homeassistant.components import emulated_hue, http, light, mqtt
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.emulated_hue import (
|
||||
HUE_API_STATE_ON, HUE_API_STATE_BRI
|
||||
)
|
||||
HUE_API_STATE_ON, HUE_API_STATE_BRI)
|
||||
|
||||
from tests.common import get_test_instance_port, get_test_home_assistant
|
||||
|
||||
|
@ -27,6 +27,7 @@ mqtt_broker = None
|
|||
|
||||
|
||||
def setUpModule():
|
||||
"""Setup things to be run when tests are started."""
|
||||
global mqtt_broker
|
||||
|
||||
mqtt_broker = MQTTBroker('127.0.0.1', MQTT_BROKER_PORT)
|
||||
|
@ -34,12 +35,14 @@ def setUpModule():
|
|||
|
||||
|
||||
def tearDownModule():
|
||||
"""Stop everything that was started."""
|
||||
global mqtt_broker
|
||||
|
||||
mqtt_broker.stop()
|
||||
|
||||
|
||||
def setup_hass_instance(emulated_hue_config):
|
||||
"""Setup the Home Assistant instance to test."""
|
||||
hass = get_test_home_assistant()
|
||||
|
||||
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||
|
@ -55,15 +58,19 @@ def setup_hass_instance(emulated_hue_config):
|
|||
|
||||
|
||||
def start_hass_instance(hass):
|
||||
"""Start the Home Assistant instance to test."""
|
||||
hass.start()
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
class TestEmulatedHue(unittest.TestCase):
|
||||
"""Test the emulated Hue component."""
|
||||
|
||||
hass = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = setup_hass_instance({
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT
|
||||
|
@ -73,9 +80,11 @@ class TestEmulatedHue(unittest.TestCase):
|
|||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_description_xml(self):
|
||||
"""Test the description."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
result = requests.get(
|
||||
|
@ -91,6 +100,7 @@ class TestEmulatedHue(unittest.TestCase):
|
|||
self.fail('description.xml is not valid XML!')
|
||||
|
||||
def test_create_username(self):
|
||||
"""Test the creation of an username."""
|
||||
request_json = {'devicetype': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
|
@ -107,6 +117,7 @@ class TestEmulatedHue(unittest.TestCase):
|
|||
self.assertTrue('username' in success_json['success'])
|
||||
|
||||
def test_valid_username_request(self):
|
||||
"""Test request with a valid username."""
|
||||
request_json = {'invalid_key': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
|
@ -117,8 +128,11 @@ class TestEmulatedHue(unittest.TestCase):
|
|||
|
||||
|
||||
class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
"""Test class for emulated hue component."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = setup_hass_instance({
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT,
|
||||
|
@ -177,9 +191,11 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_discover_lights(self):
|
||||
"""Test the discovery of lights."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
|
||||
|
||||
|
@ -194,6 +210,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertTrue('light.kitchen_light' not in result_json)
|
||||
|
||||
def test_get_light_state(self):
|
||||
"""Test the getting of light state."""
|
||||
# Turn office light on and set to 127 brightness
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_ON,
|
||||
|
@ -229,6 +246,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_light_state(self):
|
||||
"""Test the seeting of light states."""
|
||||
self.perform_put_test_on_office_light()
|
||||
|
||||
# Turn the bedroom light on first
|
||||
|
@ -264,6 +282,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_with_form_urlencoded_content_type(self):
|
||||
"""Test the form with urlencoded content."""
|
||||
# Needed for Alexa
|
||||
self.perform_put_test_on_office_light(
|
||||
'application/x-www-form-urlencoded')
|
||||
|
@ -278,6 +297,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
def test_entity_not_found(self):
|
||||
"""Test for entity which are not found."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format("not.existant_entity")),
|
||||
|
@ -293,6 +313,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_allowed_methods(self):
|
||||
"""Test the allowed methods."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format("light.office_light")))
|
||||
|
@ -313,6 +334,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
def test_proper_put_state_request(self):
|
||||
"""Test the request to set the state."""
|
||||
# Test proper on value parsing
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
|
@ -334,6 +356,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
|
||||
def perform_put_test_on_office_light(self,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light."""
|
||||
# Turn the office light off first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
|
@ -361,6 +384,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
self.assertEqual(office_light.attributes[light.ATTR_BRIGHTNESS], 56)
|
||||
|
||||
def perform_get_light_state(self, entity_id, expected_status):
|
||||
"""Test the gettting of a light state."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format(entity_id)), timeout=5)
|
||||
|
@ -377,6 +401,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||
|
||||
def perform_put_light_state(self, entity_id, is_on, brightness=None,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light state."""
|
||||
url = BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(entity_id))
|
||||
|
||||
|
@ -432,6 +457,7 @@ class MQTTBroker(object):
|
|||
self._thread.join()
|
||||
|
||||
def _run_loop(self):
|
||||
"""Run the loop."""
|
||||
asyncio.set_event_loop(self._loop)
|
||||
self._loop.run_until_complete(self._broker_coroutine())
|
||||
|
||||
|
@ -442,4 +468,5 @@ class MQTTBroker(object):
|
|||
|
||||
@asyncio.coroutine
|
||||
def _broker_coroutine(self):
|
||||
"""The Broker coroutine."""
|
||||
yield from self._broker.start()
|
||||
|
|
Loading…
Reference in New Issue