Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into Homematic

# Conflicts:
#	homeassistant/components/thermostat/homematic.py
This commit is contained in:
Pascal Vizeli 2016-06-25 22:02:14 +02:00
commit e4d3b25f1e
43 changed files with 951 additions and 105 deletions

View file

@ -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

View file

@ -22,6 +22,9 @@ http:
# Set to 1 to enable development mode
# development: 1
frontend:
# enable the frontend
light:
# platform: hue

View file

@ -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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a
Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa

View file

@ -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 : '') + '-';

View 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()

View 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)

View 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

View file

@ -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):

View file

@ -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)

View file

@ -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."""

View file

@ -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 (

View file

@ -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

View 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()

View 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

View file

@ -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

View 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

View file

@ -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."""

View file

@ -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)

View file

@ -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

View file

@ -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":

View file

@ -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):

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""

View file

@ -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

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""

View file

@ -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."""

View file

@ -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()

View file

@ -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, {})

View file

@ -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."""

View file

@ -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)

View file

@ -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

View 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()))

View file

@ -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')