Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into Homematic
# Conflicts: # homeassistant/components/thermostat/homematic.py
This commit is contained in:
commit
e4d3b25f1e
43 changed files with 951 additions and 105 deletions
|
@ -117,6 +117,8 @@ omit =
|
|||
homeassistant/components/downloader.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
|
@ -128,6 +130,7 @@ omit =
|
|||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
|
@ -188,6 +191,7 @@ omit =
|
|||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
|
|
|
@ -22,6 +22,9 @@ http:
|
|||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
frontend:
|
||||
# enable the frontend
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
CORE = "7962327e4a29e51d4a6f4ee6cca9acc3"
|
||||
UI = "570e1b8744a58024fc4e256f5e024424"
|
||||
CORE = "db0bb387f4d3bcace002d62b94baa348"
|
||||
UI = "5b306b7e7d36799b7b67f592cbe94703"
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a
|
||||
Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
var PrecacheConfig = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
var PrecacheConfig = [["/","70eeeca780a5f23c7632c2876dd1795a"],["/devEvent","70eeeca780a5f23c7632c2876dd1795a"],["/devInfo","70eeeca780a5f23c7632c2876dd1795a"],["/devService","70eeeca780a5f23c7632c2876dd1795a"],["/devState","70eeeca780a5f23c7632c2876dd1795a"],["/devTemplate","70eeeca780a5f23c7632c2876dd1795a"],["/history","70eeeca780a5f23c7632c2876dd1795a"],["/logbook","70eeeca780a5f23c7632c2876dd1795a"],["/map","70eeeca780a5f23c7632c2876dd1795a"],["/states","70eeeca780a5f23c7632c2876dd1795a"],["/static/core-db0bb387f4d3bcace002d62b94baa348.js","f938163a392465dc87af3a0094376621"],["/static/frontend-5b306b7e7d36799b7b67f592cbe94703.html","70eeeca780a5f23c7632c2876dd1795a"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
/* eslint-enable quotes, comma-spacing */
|
||||
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
|
||||
|
||||
|
|
Binary file not shown.
96
homeassistant/components/garage_door/rpi_gpio.py
Normal file
96
homeassistant/components/garage_door/rpi_gpio.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
Support for building a Raspberry Pi garage controller in HA.
|
||||
|
||||
Instructions for building the controller can be found here
|
||||
https://github.com/andrewshilliday/garage-door-controller
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/garage_door.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DOORS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema({
|
||||
'name': str,
|
||||
'relay_pin': int,
|
||||
'state_pin': int,
|
||||
})
|
||||
]
|
||||
)
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
'platform': str,
|
||||
vol.Required('doors'): _DOORS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the garage door platform."""
|
||||
doors = []
|
||||
doors_conf = config.get('doors')
|
||||
|
||||
for door in doors_conf:
|
||||
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
|
||||
door['state_pin']))
|
||||
add_devices(doors)
|
||||
|
||||
|
||||
class RPiGPIOGarageDoor(GarageDoorDevice):
|
||||
"""Representation of a Raspberry garage door."""
|
||||
|
||||
def __init__(self, name, relay_pin, state_pin):
|
||||
"""Initialize the garage door."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._relay_pin = relay_pin
|
||||
self._state_pin = state_pin
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, 'DOWN')
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this garage door."""
|
||||
return "{}.{}".format(self.__class__, self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self._state = rpi_gpio.read_input(self._state_pin) is True
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return self._state
|
||||
|
||||
def _trigger(self):
|
||||
"""Trigger the door."""
|
||||
rpi_gpio.write_output(self._relay_pin, False)
|
||||
sleep(0.2)
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
def close_door(self):
|
||||
"""Close the door."""
|
||||
if not self.is_closed:
|
||||
self._trigger()
|
||||
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
if self.is_closed:
|
||||
self._trigger()
|
70
homeassistant/components/garage_door/zwave.py
Normal file
70
homeassistant/components/garage_door/zwave.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
"""
|
||||
Support for Zwave garage door components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/garagedoor.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.garage_door import DOMAIN
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave garage door device."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave garage door."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self._state = value.data
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._state
|
||||
|
||||
def close_door(self):
|
||||
"""Close the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, False)
|
||||
|
||||
def open_door(self):
|
||||
"""Open the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, True)
|
122
homeassistant/components/hdmi_cec.py
Normal file
122
homeassistant/components/hdmi_cec.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
"""
|
||||
CEC component.
|
||||
|
||||
Requires libcec + Python bindings.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CEC = None
|
||||
DOMAIN = 'hdmi_cec'
|
||||
SERVICE_SELECT_DEVICE = 'select_device'
|
||||
SERVICE_POWER_ON = 'power_on'
|
||||
SERVICE_STANDBY = 'standby'
|
||||
CONF_DEVICES = 'devices'
|
||||
ATTR_DEVICE = 'device'
|
||||
MAX_DEPTH = 4
|
||||
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices),
|
||||
cv.string)
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICES): DEVICE_SCHEMA
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def parse_mapping(mapping, parents=None):
|
||||
"""Parse configuration device mapping."""
|
||||
if parents is None:
|
||||
parents = []
|
||||
for addr, val in mapping.items():
|
||||
cur = parents + [str(addr)]
|
||||
if isinstance(val, dict):
|
||||
yield from parse_mapping(val, cur)
|
||||
elif isinstance(val, str):
|
||||
yield (val, cur)
|
||||
|
||||
|
||||
def pad_physical_address(addr):
|
||||
"""Right-pad a physical address."""
|
||||
return addr + ['0'] * (MAX_DEPTH - len(addr))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup CEC capability."""
|
||||
global _CEC
|
||||
|
||||
# cec is only available if libcec is properly installed
|
||||
# and the Python bindings are accessible.
|
||||
try:
|
||||
import cec
|
||||
except ImportError:
|
||||
_LOGGER.error("libcec must be installed")
|
||||
return False
|
||||
|
||||
# Parse configuration into a dict of device name
|
||||
# to physical address represented as a list of
|
||||
# four elements.
|
||||
flat = {}
|
||||
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
|
||||
flat[pair[0]] = pad_physical_address(pair[1])
|
||||
|
||||
# Configure libcec.
|
||||
cfg = cec.libcec_configuration()
|
||||
cfg.strDeviceName = 'HASS'
|
||||
cfg.bActivateSource = 0
|
||||
cfg.bMonitorOnly = 1
|
||||
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
|
||||
|
||||
# Set up CEC adapter.
|
||||
_CEC = cec.ICECAdapter.Create(cfg)
|
||||
|
||||
def _power_on(call):
|
||||
"""Power on all devices."""
|
||||
_CEC.PowerOnDevices()
|
||||
|
||||
def _standby(call):
|
||||
"""Standby all devices."""
|
||||
_CEC.StandbyDevices()
|
||||
|
||||
def _select_device(call):
|
||||
"""Select the active device."""
|
||||
path = flat.get(call.data[ATTR_DEVICE])
|
||||
if not path:
|
||||
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
||||
cmds = []
|
||||
for i in range(1, MAX_DEPTH - 1):
|
||||
addr = pad_physical_address(path[:i])
|
||||
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
|
||||
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
|
||||
for cmd in cmds:
|
||||
_CEC.Transmit(_CEC.CommandFromString(cmd))
|
||||
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
|
||||
|
||||
def _start_cec(event):
|
||||
"""Open CEC adapter."""
|
||||
adapters = _CEC.DetectAdapters()
|
||||
if len(adapters) == 0:
|
||||
_LOGGER.error("No CEC adapter found")
|
||||
return
|
||||
|
||||
if _CEC.Open(adapters[0].strComName):
|
||||
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
|
||||
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
|
||||
_select_device)
|
||||
else:
|
||||
_LOGGER.error("Failed to open adapter")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
||||
return True
|
|
@ -425,39 +425,39 @@ class HvacDevice(Entity):
|
|||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
|
|
@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
"""Represents a HeatControl hvac."""
|
||||
|
||||
|
@ -98,7 +98,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_properties()
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
|
|
@ -248,7 +248,8 @@ def setup(hass, config):
|
|||
class Light(ToggleEntity):
|
||||
"""Representation of a light."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
# pylint: disable=no-self-use, abstract-method
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for LimitlessLED bulbs.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.limitlessled/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for MySensors lights.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mysensors/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
|
|
214
homeassistant/components/media_player/cmus.py
Normal file
214
homeassistant/components/media_player/cmus.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
Support for interacting with and controlling the cmus music player.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.mpd/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD,
|
||||
CONF_PORT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pycmus>=0.1.0']
|
||||
|
||||
SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
||||
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discover_info=None):
|
||||
"""Setup the Cmus platform."""
|
||||
from pycmus import exceptions
|
||||
|
||||
host = config.get(CONF_HOST, None)
|
||||
password = config.get(CONF_PASSWORD, None)
|
||||
port = config.get(CONF_PORT, None)
|
||||
name = config.get(CONF_NAME, None)
|
||||
if host and not password:
|
||||
_LOGGER.error("A password must be set if using a remote cmus server")
|
||||
return False
|
||||
try:
|
||||
cmus_remote = CmusDevice(host, password, port, name)
|
||||
except exceptions.InvalidPassword:
|
||||
_LOGGER.error("The provided password was rejected by cmus")
|
||||
return False
|
||||
add_devices([cmus_remote])
|
||||
|
||||
|
||||
class CmusDevice(MediaPlayerDevice):
|
||||
"""Representation of a running cmus."""
|
||||
|
||||
# pylint: disable=no-member, too-many-public-methods, abstract-method
|
||||
def __init__(self, server, password, port, name):
|
||||
"""Initialize the CMUS device."""
|
||||
from pycmus import remote
|
||||
|
||||
if server:
|
||||
port = port or 3000
|
||||
self.cmus = remote.PyCmus(server=server, password=password,
|
||||
port=port)
|
||||
auto_name = "cmus-%s" % server
|
||||
else:
|
||||
self.cmus = remote.PyCmus()
|
||||
auto_name = "cmus-local"
|
||||
self._name = name or auto_name
|
||||
self.status = {}
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
status = self.cmus.get_status_dict()
|
||||
if not status:
|
||||
_LOGGER.warning("Recieved no status from cmus")
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the media state."""
|
||||
if 'status' not in self.status:
|
||||
self.update()
|
||||
if self.status['status'] == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif self.status['status'] == 'paused':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self.status.get('file')
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""Content type of the current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self.status.get('duration')
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self.status['tag'].get('title')
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('artist')
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self.status['tag'].get('tracknumber')
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self.status['tag'].get('album')
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('albumartist')
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return the volume level."""
|
||||
left = self.status['set'].get('vol_left')[0]
|
||||
right = self.status['set'].get('vol_right')[0]
|
||||
if left != right:
|
||||
volume = float(left + right) / 2
|
||||
else:
|
||||
volume = left
|
||||
return int(volume)/100
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_CMUS
|
||||
|
||||
def turn_off(self):
|
||||
"""Service to send the CMUS the command to stop playing."""
|
||||
self.cmus.player_stop()
|
||||
|
||||
def turn_on(self):
|
||||
"""Service to send the CMUS the command to start playing."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.cmus.set_volume(int(volume * 100))
|
||||
|
||||
def volume_up(self):
|
||||
"""Function to send CMUS the command for volume up."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) + 5)
|
||||
|
||||
def volume_down(self):
|
||||
"""Function to send CMUS the command for volume down."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) - 5)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play command."""
|
||||
if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]:
|
||||
self.cmus.player_play_file(media_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Invalid media type %s. Only %s and %s are supported",
|
||||
media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send the pause command."""
|
||||
self.cmus.player_pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_next()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_prev()
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Send seek command."""
|
||||
self.cmus.seek(position)
|
||||
|
||||
def media_play(self):
|
||||
"""Send the play command."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def media_stop(self):
|
||||
"""Send the stop command."""
|
||||
self.cmus.stop()
|
18
homeassistant/components/persistent_notification.py
Normal file
18
homeassistant/components/persistent_notification.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
A component which is collecting configuration errors.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/persistent_notification/
|
||||
"""
|
||||
|
||||
DOMAIN = "persistent_notification"
|
||||
|
||||
|
||||
def create(hass, entity, msg):
|
||||
"""Create a state for an error."""
|
||||
hass.states.set('{}.{}'.format(DOMAIN, entity), msg)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the persistent notification component."""
|
||||
return True
|
|
@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
if value.index != 1:
|
||||
if value.index != 0:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
|
@ -49,33 +49,22 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == 38 and value.index == 0:
|
||||
return value.data
|
||||
return self._value.data
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == 38 and value.index == 0:
|
||||
value.data = 255
|
||||
break
|
||||
self._node.set_dimmer(self._value.value_id, 100)
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == 38 and value.index == 0:
|
||||
value.data = 0
|
||||
break
|
||||
self._node.set_dimmer(self._value.value_id, 0)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
|
@ -84,3 +73,4 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||
# Rollershutter will toggle between UP (True), DOWN (False).
|
||||
# It also stops the shutter if the same value is sent while moving.
|
||||
value.data = value.data
|
||||
break
|
||||
|
|
100
homeassistant/components/sensor/openexchangerates.py
Normal file
100
homeassistant/components/sensor/openexchangerates.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""Support for openexchangerates.org exchange rates service."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import requests
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_RESOURCE = 'https://openexchangerates.org/api/latest.json'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=100)
|
||||
CONF_BASE = 'base'
|
||||
CONF_QUOTE = 'quote'
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'Exchange Rate Sensor'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Openexchangerates sensor."""
|
||||
payload = config.get('payload', None)
|
||||
rest = OpenexchangeratesData(
|
||||
_RESOURCE,
|
||||
config.get(CONF_API_KEY),
|
||||
config.get(CONF_BASE, 'USD'),
|
||||
config.get(CONF_QUOTE),
|
||||
payload
|
||||
)
|
||||
response = requests.get(_RESOURCE, params={'base': config.get(CONF_BASE,
|
||||
'USD'),
|
||||
'app_id':
|
||||
config.get(CONF_API_KEY)},
|
||||
timeout=10)
|
||||
if response.status_code != 200:
|
||||
_LOGGER.error("Check your OpenExchangeRates API")
|
||||
return False
|
||||
rest.update()
|
||||
add_devices([OpenexchangeratesSensor(rest, config.get(CONF_NAME,
|
||||
DEFAULT_NAME),
|
||||
config.get(CONF_QUOTE))])
|
||||
|
||||
|
||||
class OpenexchangeratesSensor(Entity):
|
||||
"""Implementing the Openexchangerates sensor."""
|
||||
|
||||
def __init__(self, rest, name, quote):
|
||||
"""Initialize the sensor."""
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._quote = quote
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return other attributes of the sensor."""
|
||||
return self.rest.data
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.rest.update()
|
||||
value = self.rest.data
|
||||
self._state = round(value[str(self._quote)], 4)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class OpenexchangeratesData(object):
|
||||
"""Get data from Openexchangerates.org."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, resource, api_key, base, quote, data):
|
||||
"""Initialize the data object."""
|
||||
self._resource = resource
|
||||
self._api_key = api_key
|
||||
self._base = base
|
||||
self._quote = quote
|
||||
self.data = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from openexchangerates."""
|
||||
try:
|
||||
result = requests.get(self._resource, params={'base': self._base,
|
||||
'app_id':
|
||||
self._api_key},
|
||||
timeout=10)
|
||||
self.data = result.json()['rates']
|
||||
except requests.exceptions.HTTPError:
|
||||
_LOGGER.error("Check Openexchangerates API Key")
|
||||
self.data = None
|
||||
return False
|
|
@ -108,7 +108,7 @@ def setup(hass, config):
|
|||
class SwitchDevice(ToggleEntity):
|
||||
"""Representation of a switch."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
# pylint: disable=no-self-use, abstract-method
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Return the current power usage in mWh."""
|
||||
|
|
|
@ -4,7 +4,6 @@ Use serial protocol of acer projector to obtain state of the projector.
|
|||
This component allows to control almost all projectors from acer using
|
||||
their RS232 serial communication protocol.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
@ -61,7 +60,8 @@ class AcerSwitch(SwitchDevice):
|
|||
write_timeout=write_timeout, **kwargs)
|
||||
self._serial_port = serial_port
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = False
|
||||
self._available = False
|
||||
self._attributes = {
|
||||
LAMP_HOURS: STATE_UNKNOWN,
|
||||
INPUT_SOURCE: STATE_UNKNOWN,
|
||||
|
@ -100,14 +100,19 @@ class AcerSwitch(SwitchDevice):
|
|||
return match.group(1)
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if projector is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the projector."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state of the projector."""
|
||||
def is_on(self):
|
||||
"""Return if the projector is turned on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
|
@ -120,11 +125,13 @@ class AcerSwitch(SwitchDevice):
|
|||
msg = CMD_DICT[LAMP]
|
||||
awns = self._write_read_format(msg)
|
||||
if awns == 'Lamp 1':
|
||||
self._state = STATE_ON
|
||||
self._state = True
|
||||
self._available = True
|
||||
elif awns == 'Lamp 0':
|
||||
self._state = STATE_OFF
|
||||
self._state = False
|
||||
self._available = True
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
self._available = False
|
||||
|
||||
for key in self._attributes.keys():
|
||||
msg = CMD_DICT.get(key, None)
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for device running with the aREST RESTful framework.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.arest/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
|
|
@ -65,6 +65,10 @@ class WOLSwitch(SwitchDevice):
|
|||
self._wol.send_magic_packet(self._mac_address)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Do nothing."""
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
"""Check if device is on and update the state."""
|
||||
if platform.system().lower() == "windows":
|
||||
|
|
|
@ -273,29 +273,29 @@ class ThermostatDevice(Entity):
|
|||
"""Return true if the fan is on."""
|
||||
return None
|
||||
|
||||
def set_temperate(self, temperature):
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set hvac mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_fan_on(self):
|
||||
"""Turn fan on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_fan_off(self):
|
||||
"""Turn fan off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
|
|
@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
class DemoThermostat(ThermostatDevice):
|
||||
"""Representation of a demo thermostat."""
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
class Thermostat(ThermostatDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, import-error
|
||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
||||
class EQ3BTSmartThermostat(ThermostatDevice):
|
||||
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the heat control thermostat."""
|
||||
name = config.get(CONF_NAME)
|
||||
|
@ -55,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
min_temp, max_temp, target_temp)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
class HeatControl(ThermostatDevice):
|
||||
"""Representation of a HeatControl device."""
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class HeatmiserV3Thermostat(ThermostatDevice):
|
||||
"""Representation of a HeatmiserV3 thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, heatmiser, device, name, serport):
|
||||
"""Initialize the thermostat."""
|
||||
self.heatmiser = heatmiser
|
||||
|
|
|
@ -49,7 +49,6 @@ def _setup_round(username, password, config, add_devices):
|
|||
|
||||
|
||||
# config will be used later
|
||||
# pylint: disable=unused-argument
|
||||
def _setup_us(username, password, config, add_devices):
|
||||
"""Setup user."""
|
||||
import somecomfort
|
||||
|
@ -74,7 +73,6 @@ def _setup_us(username, password, config, add_devices):
|
|||
return True
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the honeywel thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
|
@ -98,7 +96,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class RoundThermostat(ThermostatDevice):
|
||||
"""Representation of a Honeywell Round Connected thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, device, zone_id, master, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
|
@ -182,6 +180,7 @@ class RoundThermostat(ThermostatDevice):
|
|||
self._is_dhw = False
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HoneywellUSThermostat(ThermostatDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
for structure, device in nest.devices()])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class NestThermostat(ThermostatDevice):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ProliphixThermostat(ThermostatDevice):
|
||||
"""Representation a Proliphix thermostat."""
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
add_devices(tstats)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RadioThermostat(ThermostatDevice):
|
||||
"""Representation of a Radio Thermostat."""
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
"""Represents a HeatControl thermostat."""
|
||||
|
||||
|
@ -80,7 +81,7 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
|||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
|
||||
|
|
|
@ -39,23 +39,39 @@ SERVICE_TEST_NETWORK = "test_network"
|
|||
|
||||
EVENT_SCENE_ACTIVATED = "zwave.scene_activated"
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
|
||||
COMMAND_CLASS_DOOR_LOCK = 98
|
||||
COMMAND_CLASS_SWITCH_BINARY = 37
|
||||
COMMAND_CLASS_SENSOR_BINARY = 48
|
||||
COMMAND_CLASS_WHATEVER = None
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 49
|
||||
COMMAND_CLASS_METER = 50
|
||||
COMMAND_CLASS_ALARM = 113
|
||||
COMMAND_CLASS_SWITCH_BINARY = 37
|
||||
COMMAND_CLASS_SENSOR_BINARY = 48
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
|
||||
COMMAND_CLASS_DOOR_LOCK = 98
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68
|
||||
COMMAND_CLASS_BATTERY = 128
|
||||
COMMAND_CLASS_ALARM = 113 # 0x71
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44
|
||||
|
||||
GENERIC_COMMAND_CLASS_WHATEVER = None
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH = 17
|
||||
GENERIC_COMMAND_CLASS_BINARY_SWITCH = 16
|
||||
GENERIC_COMMAND_CLASS_ENTRY_CONTROL = 64
|
||||
GENERIC_COMMAND_CLASS_BINARY_SENSOR = 32
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR = 33
|
||||
GENERIC_COMMAND_CLASS_METER = 49
|
||||
GENERIC_COMMAND_CLASS_ALARM_SENSOR = 161
|
||||
GENERIC_COMMAND_CLASS_THERMOSTAT = 8
|
||||
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER = None
|
||||
SPECIFIC_DEVICE_CLASS_NOT_USED = 0
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH = 1
|
||||
SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK = 2
|
||||
SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR = 3
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK = 3
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE = 4
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_DOOR = 5
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A = 5
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B = 6
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON = 7
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C = 7
|
||||
|
||||
GENRE_WHATEVER = None
|
||||
|
@ -71,51 +87,68 @@ TYPE_DECIMAL = "Decimal"
|
|||
# value type, genre type, specific device class).
|
||||
DISCOVERY_COMPONENTS = [
|
||||
('sensor',
|
||||
[GENERIC_COMMAND_CLASS_WHATEVER],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
COMMAND_CLASS_METER,
|
||||
COMMAND_CLASS_ALARM],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_USER),
|
||||
('light',
|
||||
[GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH,
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE],
|
||||
[COMMAND_CLASS_SWITCH_MULTILEVEL],
|
||||
TYPE_BYTE,
|
||||
GENRE_USER,
|
||||
[SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH,
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE]),
|
||||
GENRE_USER),
|
||||
('switch',
|
||||
[GENERIC_COMMAND_CLASS_BINARY_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SWITCH_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_USER),
|
||||
('binary_sensor',
|
||||
[GENERIC_COMMAND_CLASS_BINARY_SENSOR,
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SENSOR_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_USER),
|
||||
('thermostat',
|
||||
[GENERIC_COMMAND_CLASS_THERMOSTAT],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_WHATEVER),
|
||||
('hvac',
|
||||
[GENERIC_COMMAND_CLASS_THERMOSTAT],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_THERMOSTAT_FAN_MODE],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_WHATEVER),
|
||||
('lock',
|
||||
[GENERIC_COMMAND_CLASS_ENTRY_CONTROL],
|
||||
[SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK,
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK],
|
||||
[COMMAND_CLASS_DOOR_LOCK],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER,
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER),
|
||||
GENRE_USER),
|
||||
('rollershutter',
|
||||
[COMMAND_CLASS_SWITCH_MULTILEVEL],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER,
|
||||
[GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A,
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B,
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C,
|
||||
SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR]),
|
||||
SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR],
|
||||
[COMMAND_CLASS_WHATEVER],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER),
|
||||
('garage_door',
|
||||
[GENERIC_COMMAND_CLASS_ENTRY_CONTROL],
|
||||
[SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON,
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_DOOR],
|
||||
[COMMAND_CLASS_SWITCH_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER)
|
||||
]
|
||||
|
||||
|
||||
|
@ -244,25 +277,49 @@ def setup(hass, config):
|
|||
def value_added(node, value):
|
||||
"""Called when a value is added to a node on the network."""
|
||||
for (component,
|
||||
command_ids,
|
||||
generic_device_class,
|
||||
specific_device_class,
|
||||
command_class,
|
||||
value_type,
|
||||
value_genre,
|
||||
specific_device_class) in DISCOVERY_COMPONENTS:
|
||||
value_genre) in DISCOVERY_COMPONENTS:
|
||||
|
||||
if value.command_class not in command_ids:
|
||||
_LOGGER.debug("Component=%s Node_id=%s query start",
|
||||
component, node.node_id)
|
||||
if node.generic not in generic_device_class and \
|
||||
None not in generic_device_class:
|
||||
_LOGGER.debug("node.generic %s not None and in \
|
||||
generic_device_class %s",
|
||||
node.generic, generic_device_class)
|
||||
continue
|
||||
if value_type is not None and value_type != value.type:
|
||||
if node.specific not in specific_device_class and \
|
||||
None not in specific_device_class:
|
||||
_LOGGER.debug("node.specific %s is not None and in \
|
||||
specific_device_class %s", node.specific,
|
||||
specific_device_class)
|
||||
continue
|
||||
if value_genre is not None and value_genre != value.genre:
|
||||
if value.command_class not in command_class and \
|
||||
None not in command_class:
|
||||
_LOGGER.debug("value.command_class %s is not None \
|
||||
and in command_class %s",
|
||||
value.command_class, command_class)
|
||||
continue
|
||||
if specific_device_class is not None and \
|
||||
specific_device_class != node.specific:
|
||||
if value_type != value.type and value_type is not None:
|
||||
_LOGGER.debug("value.type %s != value_type %s",
|
||||
value.type, value_type)
|
||||
continue
|
||||
if value_genre != value.genre and value_genre is not None:
|
||||
_LOGGER.debug("value.genre %s != value_genre %s",
|
||||
value.genre, value_genre)
|
||||
continue
|
||||
|
||||
# Configure node
|
||||
_LOGGER.debug("Node_id=%s Value type=%s Genre=%s \
|
||||
Specific Device_class=%s", node.node_id,
|
||||
value.type, value.genre, specific_device_class)
|
||||
_LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, \
|
||||
Specific_command_class=%s, \
|
||||
Command_class=%s, Value type=%s, \
|
||||
Genre=%s", node.node_id,
|
||||
node.generic, node.specific,
|
||||
value.command_class, value.type,
|
||||
value.genre)
|
||||
name = "{}.{}".format(component, _object_id(value))
|
||||
|
||||
node_config = customize.get(name, {})
|
||||
|
|
|
@ -229,17 +229,15 @@ class ToggleEntity(Entity):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return True if entity is on."""
|
||||
return False
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
_LOGGER.warning('Method turn_on not implemented for %s',
|
||||
self.entity_id)
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
_LOGGER.warning('Method turn_off not implemented for %s',
|
||||
self.entity_id)
|
||||
raise NotImplementedError()
|
||||
|
||||
def toggle(self, **kwargs):
|
||||
"""Toggle the entity off."""
|
||||
|
|
|
@ -5,10 +5,15 @@ from collections import OrderedDict
|
|||
|
||||
import glob
|
||||
import yaml
|
||||
try:
|
||||
import keyring
|
||||
except ImportError:
|
||||
keyring = None
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SECRET_NAMESPACE = 'homeassistant'
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
|
@ -119,10 +124,49 @@ def _env_var_yaml(loader, node):
|
|||
raise HomeAssistantError(node.value)
|
||||
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def _secret_yaml(loader, node):
|
||||
"""Load secrets and embed it into the configuration YAML."""
|
||||
# Create secret cache on loader and load secret.yaml
|
||||
if not hasattr(loader, '_SECRET_CACHE'):
|
||||
loader._SECRET_CACHE = {}
|
||||
|
||||
secret_path = os.path.join(os.path.dirname(loader.name), 'secrets.yaml')
|
||||
if secret_path not in loader._SECRET_CACHE:
|
||||
if os.path.isfile(secret_path):
|
||||
loader._SECRET_CACHE[secret_path] = load_yaml(secret_path)
|
||||
secrets = loader._SECRET_CACHE[secret_path]
|
||||
if 'logger' in secrets:
|
||||
logger = str(secrets['logger']).lower()
|
||||
if logger == 'debug':
|
||||
_LOGGER.setLevel(logging.DEBUG)
|
||||
else:
|
||||
_LOGGER.error("secrets.yaml: 'logger: debug' expected,"
|
||||
" but 'logger: %s' found", logger)
|
||||
del secrets['logger']
|
||||
else:
|
||||
loader._SECRET_CACHE[secret_path] = None
|
||||
secrets = loader._SECRET_CACHE[secret_path]
|
||||
|
||||
# Retrieve secret, first from secrets.yaml, then from keyring
|
||||
if secrets is not None and node.value in secrets:
|
||||
_LOGGER.debug('Secret %s retrieved from secrets.yaml.', node.value)
|
||||
return secrets[node.value]
|
||||
elif keyring:
|
||||
# do ome keyring stuff
|
||||
pwd = keyring.get_password(_SECRET_NAMESPACE, node.value)
|
||||
if pwd:
|
||||
_LOGGER.debug('Secret %s retrieved from keyring.', node.value)
|
||||
return pwd
|
||||
|
||||
_LOGGER.error('Secret %s not defined.', node.value)
|
||||
raise HomeAssistantError(node.value)
|
||||
|
||||
yaml.SafeLoader.add_constructor('!include', _include_yaml)
|
||||
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
||||
_ordered_dict)
|
||||
yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml)
|
||||
yaml.SafeLoader.add_constructor('!secret', _secret_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_merge_list',
|
||||
_include_dir_merge_list_yaml)
|
||||
|
|
|
@ -247,6 +247,9 @@ pyasn1==0.1.9
|
|||
# homeassistant.components.media_player.cast
|
||||
pychromecast==0.7.2
|
||||
|
||||
# homeassistant.components.media_player.cmus
|
||||
pycmus>=0.1.0
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
# homeassistant.components.zwave
|
||||
pydispatcher==2.0.5
|
||||
|
|
31
tests/components/media_player/test_cmus.py
Normal file
31
tests/components/media_player/test_cmus.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
"""The tests for the Demo Media player platform."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from homeassistant.components.media_player import cmus
|
||||
from homeassistant import const
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
entity_id = 'media_player.cmus'
|
||||
|
||||
|
||||
class TestCmusMediaPlayer(unittest.TestCase):
|
||||
"""Test the media_player module."""
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@mock.patch('homeassistant.components.media_player.cmus.CmusDevice')
|
||||
def test_password_required_with_host(self, cmus_mock):
|
||||
"""Test that a password is required when specifying a remote host."""
|
||||
fake_config = {
|
||||
const.CONF_HOST: 'a_real_hostname',
|
||||
}
|
||||
self.assertFalse(
|
||||
cmus.setup_platform(self.hass, fake_config, mock.MagicMock()))
|
|
@ -3,8 +3,9 @@ import io
|
|||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from homeassistant.util import yaml
|
||||
import homeassistant.config as config_util
|
||||
from tests.common import get_test_config_dir
|
||||
|
||||
|
||||
class TestYaml(unittest.TestCase):
|
||||
|
@ -135,3 +136,81 @@ class TestYaml(unittest.TestCase):
|
|||
"key2": "two",
|
||||
"key3": "three"
|
||||
}
|
||||
|
||||
|
||||
def load_yaml(fname, string):
|
||||
"""Write a string to file and return the parsed yaml."""
|
||||
with open(fname, 'w') as file:
|
||||
file.write(string)
|
||||
return config_util.load_yaml_config_file(fname)
|
||||
|
||||
|
||||
class FakeKeyring():
|
||||
"""Fake a keyring class."""
|
||||
|
||||
def __init__(self, secrets_dict):
|
||||
"""Store keyring dictionary."""
|
||||
self._secrets = secrets_dict
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def get_password(self, domain, name):
|
||||
"""Retrieve password."""
|
||||
assert domain == yaml._SECRET_NAMESPACE
|
||||
return self._secrets.get(name)
|
||||
|
||||
|
||||
class TestSecrets(unittest.TestCase):
|
||||
"""Test the secrets parameter in the yaml utility."""
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""Create & load secrets file."""
|
||||
config_dir = get_test_config_dir()
|
||||
self._yaml_path = os.path.join(config_dir,
|
||||
config_util.YAML_CONFIG_FILE)
|
||||
self._secret_path = os.path.join(config_dir, 'secrets.yaml')
|
||||
|
||||
load_yaml(self._secret_path,
|
||||
'http_pw: pwhttp\n'
|
||||
'comp1_un: un1\n'
|
||||
'comp1_pw: pw1\n'
|
||||
'stale_pw: not_used\n'
|
||||
'logger: debug\n')
|
||||
self._yaml = load_yaml(self._yaml_path,
|
||||
'http:\n'
|
||||
' api_password: !secret http_pw\n'
|
||||
'component:\n'
|
||||
' username: !secret comp1_un\n'
|
||||
' password: !secret comp1_pw\n'
|
||||
'')
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
"""Clean up secrets."""
|
||||
for path in [self._yaml_path, self._secret_path]:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
|
||||
def test_secrets_from_yaml(self):
|
||||
"""Did secrets load ok."""
|
||||
expected = {'api_password': 'pwhttp'}
|
||||
self.assertEqual(expected, self._yaml['http'])
|
||||
|
||||
expected = {
|
||||
'username': 'un1',
|
||||
'password': 'pw1'}
|
||||
self.assertEqual(expected, self._yaml['component'])
|
||||
|
||||
def test_secrets_keyring(self):
|
||||
"""Test keyring fallback & get_password."""
|
||||
yaml.keyring = None # Ensure its not there
|
||||
yaml_str = 'http:\n api_password: !secret http_pw_keyring'
|
||||
with self.assertRaises(yaml.HomeAssistantError):
|
||||
load_yaml(self._yaml_path, yaml_str)
|
||||
|
||||
yaml.keyring = FakeKeyring({'http_pw_keyring': 'yeah'})
|
||||
_yaml = load_yaml(self._yaml_path, yaml_str)
|
||||
self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml)
|
||||
|
||||
def test_secrets_logger_removed(self):
|
||||
"""Ensure logger: debug was removed."""
|
||||
with self.assertRaises(yaml.HomeAssistantError):
|
||||
load_yaml(self._yaml_path, 'api_password: !secret logger')
|
||||
|
|
Loading…
Add table
Reference in a new issue