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

This commit is contained in:
Phil Cole
2017-07-26 21:56:44 +01:00
86 changed files with 3384 additions and 790 deletions

View File

@@ -172,6 +172,9 @@ omit =
homeassistant/components/twilio.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
homeassistant/components/velbus.py
homeassistant/components/*/velbus.py
homeassistant/components/velux.py
homeassistant/components/*/velux.py
@@ -211,6 +214,7 @@ omit =
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/alarm_control_panel/totalconnect.py
@@ -305,6 +309,7 @@ omit =
homeassistant/components/light/piglow.py
homeassistant/components/light/sensehat.py
homeassistant/components/light/tikteck.py
homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py

View File

@@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| | Join the chat `at discord <https://discordapp.com/channels/330944238910963714/330944238910963714>`_ | Join the dev chat `at discord <https://discordapp.com/channels/330944238910963714/330990195199442944>`_ |
==============================================================================================================================================================================================
Home Assistant |Build Status| |Coverage Status| |Chat Status|
=============================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@@ -31,6 +31,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://travis-ci.org/home-assistant/home-assistant
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg

View File

@@ -0,0 +1,235 @@
"""
Support for manual alarms controllable via MQTT.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
"""
import asyncio
import datetime
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM,
CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
CONF_DISARM_AFTER_TRIGGER)
import homeassistant.components.mqtt as mqtt
from homeassistant.helpers.event import async_track_state_change
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120
DEFAULT_DISARM_AFTER_TRIGGER = False
DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME'
DEFAULT_DISARM = 'DISARM'
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'manual_mqtt',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the manual MQTT alarm platform."""
add_devices([ManualMQTTAlarm(
hass,
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_STATE_TOPIC),
config.get(mqtt.CONF_COMMAND_TOPIC),
config.get(mqtt.CONF_QOS),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY))])
class ManualMQTTAlarm(alarm.AlarmControlPanel):
"""
Representation of an alarm status.
When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for 'trigger_time'. After that will be
triggered for 'trigger_time', after that we return to the previous state
or disarm if `disarm_after_trigger` is true.
"""
def __init__(self, hass, name, code, pending_time,
trigger_time, disarm_after_trigger,
state_topic, command_topic, qos,
payload_disarm, payload_arm_home, payload_arm_away):
"""Init the manual MQTT alarm panel."""
self._state = STATE_ALARM_DISARMED
self._hass = hass
self._name = name
self._code = str(code) if code else None
self._pending_time = datetime.timedelta(seconds=pending_time)
self._trigger_time = datetime.timedelta(seconds=trigger_time)
self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state
self._state_ts = None
self._state_topic = state_topic
self._command_topic = command_topic
self._qos = qos
self._payload_disarm = payload_disarm
self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
if self._state in (STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY) and \
self._pending_time and self._state_ts + self._pending_time > \
dt_util.utcnow():
return STATE_ALARM_PENDING
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
if self._state_ts + self._pending_time > dt_util.utcnow():
return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time +
self._trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
return self._pre_trigger_state
return self._state
@property
def code_format(self):
"""One or more characters."""
return None if self._code is None else '.+'
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._validate_code(code, STATE_ALARM_DISARMED):
return
self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
return
self._state = STATE_ALARM_ARMED_HOME
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
if self._pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
return
self._state = STATE_ALARM_ARMED_AWAY
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
if self._pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
self._state = STATE_ALARM_TRIGGERED
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
if self._trigger_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time + self._trigger_time)
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if not check:
_LOGGER.warning("Invalid code given for %s", state)
return check
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method must be run in the event loop and returns a coroutine.
"""
async_track_state_change(
self.hass, self.entity_id, self._async_state_changed_listener
)
@callback
def message_received(topic, payload, qos):
"""Run when new MQTT message has been received."""
if payload == self._payload_disarm:
self.async_alarm_disarm(self._code)
elif payload == self._payload_arm_home:
self.async_alarm_arm_home(self._code)
elif payload == self._payload_arm_away:
self.async_alarm_arm_away(self._code)
else:
_LOGGER.warning("Received unexpected payload: %s", payload)
return
return mqtt.async_subscribe(
self.hass, self._command_topic, message_received, self._qos)
@asyncio.coroutine
def _async_state_changed_listener(self, entity_id, old_state, new_state):
"""Publish state change to MQTT."""
mqtt.async_publish(self.hass, self._state_topic, new_state.state,
self._qos, True)

View File

@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.0']
REQUIREMENTS = ['amcrest==1.2.1']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,96 @@
"""
Support for Velbus Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional('is_pushbutton'): cv.boolean
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Velbus binary sensors."""
velbus = hass.data[DOMAIN]
add_devices(VelbusBinarySensor(sensor, velbus)
for sensor in config[CONF_DEVICES])
class VelbusBinarySensor(BinarySensorDevice):
"""Representation of a Velbus Binary Sensor."""
def __init__(self, binary_sensor, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = binary_sensor[CONF_NAME]
self._module = binary_sensor['module']
self._channel = binary_sensor['channel']
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
and binary_sensor['is_pushbutton']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
yield from self.hass.async_add_job(
self._velbus.subscribe, self._on_message)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.PushButtonStatusMessage):
if message.address == self._module and \
self._channel in message.get_channels():
if self._is_pushbutton:
if self._channel in message.closed:
self._toggle()
else:
pass
else:
self._toggle()
def _toggle(self):
if self._state is True:
self._state = False
else:
self._state = True
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the display name of this sensor."""
return self._name
@property
def is_on(self):
"""Return true if the sensor is on."""
return self._state

View File

@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
"""
import asyncio
import logging
import os
import voluptuous as vol
@@ -54,6 +55,7 @@ class ONVIFCamera(Camera):
def __init__(self, hass, config):
"""Initialize a ONVIF camera."""
from onvif import ONVIFService
import onvif
super().__init__()
self._name = config.get(CONF_NAME)
@@ -63,7 +65,7 @@ class ONVIFCamera(Camera):
config.get(CONF_HOST), config.get(CONF_PORT)),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
'{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir)
'{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__))
)
self._input = media.GetStreamUri().Uri
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",

View File

@@ -151,7 +151,7 @@ def _process(hass, text):
if not entity_ids:
_LOGGER.error(
"Could not find entity id %s from text %s", name, text)
return
return None
if command == 'on':
yield from hass.services.async_call(
@@ -169,6 +169,8 @@ def _process(hass, text):
_LOGGER.error('Got unsupported command %s from text %s',
command, text)
return None
class ConversationProcessView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
@@ -194,4 +196,8 @@ class ConversationProcessView(http.HomeAssistantView):
intent_result = yield from _process(hass, text)
if intent_result is None:
intent_result = intent.IntentResponse()
intent_result.async_set_speech("Sorry, I didn't understand that")
return self.json(intent_result)

View File

@@ -0,0 +1,160 @@
"""
Support for Velbus covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.velbus/
"""
import logging
import asyncio
import time
import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
SUPPORT_STOP)
from homeassistant.components.velbus import DOMAIN
from homeassistant.const import (CONF_COVERS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
COVER_SCHEMA = vol.Schema({
vol.Required('module'): cv.positive_int,
vol.Required('open_channel'): cv.positive_int,
vol.Required('close_channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
DEPENDENCIES = ['velbus']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up cover controlled by Velbus."""
devices = config.get(CONF_COVERS, {})
covers = []
velbus = hass.data[DOMAIN]
for device_name, device_config in devices.items():
covers.append(
VelbusCover(
velbus,
device_config.get(CONF_NAME, device_name),
device_config.get('module'),
device_config.get('open_channel'),
device_config.get('close_channel')
)
)
if not covers:
_LOGGER.error("No covers added")
return False
add_devices(covers)
class VelbusCover(CoverDevice):
"""Representation a Velbus cover."""
def __init__(self, velbus, name, module, open_channel, close_channel):
"""Initialize the cover."""
self._velbus = velbus
self._name = name
self._close_channel_state = None
self._open_channel_state = None
self._module = module
self._open_channel = open_channel
self._close_channel = close_channel
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage):
if message.address == self._module:
if message.channel == self._close_channel:
self._close_channel_state = message.is_on()
self.schedule_update_ha_state()
if message.channel == self._open_channel:
self._open_channel_state = message.is_on()
self.schedule_update_ha_state()
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._close_channel_state
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown.
"""
return None
def _relay_off(self, channel):
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def _relay_on(self, channel):
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def open_cover(self, **kwargs):
"""Open the cover."""
self._relay_off(self._close_channel)
time.sleep(0.3)
self._relay_on(self._open_channel)
def close_cover(self, **kwargs):
"""Close the cover."""
self._relay_off(self._open_channel)
time.sleep(0.3)
self._relay_on(self._close_channel)
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._relay_off(self._open_channel)
time.sleep(0.3)
self._relay_off(self._close_channel)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._open_channel, self._close_channel]
self._velbus.send(message)

View File

@@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.actiontec/
import logging
import re
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
@@ -17,9 +15,6 @@ import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -54,7 +49,6 @@ class ActiontecDeviceScanner(DeviceScanner):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = []
data = self.get_actiontec_data()
self.success_init = data is not None
@@ -74,7 +68,6 @@ class ActiontecDeviceScanner(DeviceScanner):
return client.ip
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the router is up to date.
@@ -84,16 +77,15 @@ class ActiontecDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
now = dt_util.now()
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = [Device(data['mac'], name, now)
for name, data in actiontec_data.items()
if data['timevalid'] > -60]
_LOGGER.info("Scan successful")
return True
now = dt_util.now()
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = [Device(data['mac'], name, now)
for name, data in actiontec_data.items()
if data['timevalid'] > -60]
_LOGGER.info("Scan successful")
return True
def get_actiontec_data(self):
"""Retrieve data from Actiontec MI424WR and return parsed result."""

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.aruba/
"""
import logging
import re
import threading
from datetime import timedelta
import voluptuous as vol
@@ -15,14 +13,11 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_DEVICES_REGEX = re.compile(
r'(?P<name>([^\s]+))\s+' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
@@ -52,8 +47,6 @@ class ArubaDeviceScanner(DeviceScanner):
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible.
@@ -74,7 +67,6 @@ class ArubaDeviceScanner(DeviceScanner):
return client['name']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Aruba Access Point is up to date.
@@ -83,13 +75,12 @@ class ArubaDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
data = self.get_aruba_data()
if not data:
return False
data = self.get_aruba_data()
if not data:
return False
self.last_results = data.values()
return True
self.last_results = data.values()
return True
def get_aruba_data(self):
"""Retrieve data from Aruba Access Point and return parsed result."""

View File

@@ -8,9 +8,7 @@ import logging
import re
import socket
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
import voluptuous as vol
@@ -18,7 +16,6 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pexpect==4.0.1']
@@ -32,8 +29,6 @@ CONF_SSH_KEY = 'ssh_key'
DEFAULT_SSH_PORT = 22
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
SECRET_GROUP = 'Password or SSH Key'
PLATFORM_SCHEMA = vol.All(
@@ -123,8 +118,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.password,
self.mode == "ap")
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible.
@@ -145,7 +138,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the ASUSWRT router is up to date.
@@ -154,19 +146,18 @@ class AsusWrtDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info('Checking Devices')
data = self.get_asuswrt_data()
if not data:
return False
_LOGGER.info('Checking Devices')
data = self.get_asuswrt_data()
if not data:
return False
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE' or
client['status'] == 'IN_ASSOCLIST']
self.last_results = active_clients
return True
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE' or
client['status'] == 'IN_ASSOCLIST']
self.last_results = active_clients
return True
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.bt_home_hub_5/
"""
import logging
import re
import threading
from datetime import timedelta
import xml.etree.ElementTree as ET
import json
from urllib.parse import unquote
@@ -19,13 +17,10 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
})
@@ -46,11 +41,7 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
"""Initialise the scanner."""
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
# Test the router is accessible
@@ -65,17 +56,15 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
if not self.last_results:
return None
if not self.last_results:
return None
return self.last_results.get(device)
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date.
@@ -84,18 +73,17 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
_LOGGER.info("Scanning")
data = _get_homehub_data(self.url)
data = _get_homehub_data(self.url)
if not data:
_LOGGER.warning("Error scanning devices")
return False
if not data:
_LOGGER.warning("Error scanning devices")
return False
self.last_results = data
self.last_results = data
return True
return True
def _get_homehub_data(url):

View File

@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.cisco_ios/
"""
import logging
from datetime import timedelta
import voluptuous as vol
@@ -14,9 +13,6 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
CONF_PORT
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +61,6 @@ class CiscoDeviceScanner(DeviceScanner):
return self.last_results
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensure the information from the Cisco router is up to date.

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.ddwrt/
"""
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -50,8 +45,6 @@ class DdWrtDeviceScanner(DeviceScanner):
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
self.mac2name = {}
@@ -69,68 +62,65 @@ class DdWrtDeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.mac2name:
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
# If not initialised and not already scanned and not found.
if device not in self.mac2name:
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return None
if not data:
return None
dhcp_leases = data.get('dhcp_leases', None)
dhcp_leases = data.get('dhcp_leases', None)
if not dhcp_leases:
return None
if not dhcp_leases:
return None
# Remove leading and trailing quotes and spaces
cleaned_str = dhcp_leases.replace(
"\"", "").replace("\'", "").replace(" ", "")
elements = cleaned_str.split(',')
num_clients = int(len(elements) / 5)
self.mac2name = {}
for idx in range(0, num_clients):
# The data is a single array
# every 5 elements represents one host, the MAC
# is the third element and the name is the first.
mac_index = (idx * 5) + 2
if mac_index < len(elements):
mac = elements[mac_index]
self.mac2name[mac] = elements[idx * 5]
# Remove leading and trailing quotes and spaces
cleaned_str = dhcp_leases.replace(
"\"", "").replace("\'", "").replace(" ", "")
elements = cleaned_str.split(',')
num_clients = int(len(elements) / 5)
self.mac2name = {}
for idx in range(0, num_clients):
# The data is a single array
# every 5 elements represents one host, the MAC
# is the third element and the name is the first.
mac_index = (idx * 5) + 2
if mac_index < len(elements):
mac = elements[mac_index]
self.mac2name[mac] = elements[idx * 5]
return self.mac2name.get(device)
return self.mac2name.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the DD-WRT router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Checking ARP")
_LOGGER.info("Checking ARP")
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
if not data:
return False
if not data:
return False
self.last_results = []
self.last_results = []
active_clients = data.get('active_wireless', None)
if not active_clients:
return False
active_clients = data.get('active_wireless', None)
if not active_clients:
return False
# The DD-WRT UI uses its own data format and then
# regex's out values so this is done here too
# Remove leading and trailing single quotes.
clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','")
# The DD-WRT UI uses its own data format and then
# regex's out values so this is done here too
# Remove leading and trailing single quotes.
clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','")
self.last_results.extend(item for item in elements
if _MAC_REGEX.match(item))
self.last_results.extend(item for item in elements
if _MAC_REGEX.match(item))
return True
return True
def get_ddwrt_data(self, url):
"""Retrieve data from DD-WRT and return parsed result."""

View File

@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.fritz/
"""
import logging
from datetime import timedelta
import voluptuous as vol
@@ -13,12 +12,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
REQUIREMENTS = ['fritzconnection==0.6.3']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers.
@@ -88,7 +84,6 @@ class FritzBoxScanner(DeviceScanner):
return None
return ret
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Retrieve latest information from the FRITZ!Box."""
if not self.success_init:

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.linksys_ap/
"""
import base64
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -16,9 +14,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
INTERFACES = 2
DEFAULT_TIMEOUT = 10
@@ -51,8 +47,6 @@ class LinksysAPDeviceScanner(object):
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.verify_ssl = config[CONF_VERIFY_SSL]
self.lock = threading.Lock()
self.last_results = []
# Check if the access point is accessible
@@ -76,24 +70,22 @@ class LinksysAPDeviceScanner(object):
"""
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
from bs4 import BeautifulSoup as BS
with self.lock:
_LOGGER.info("Checking Linksys AP")
_LOGGER.info("Checking Linksys AP")
self.last_results = []
for interface in range(INTERFACES):
request = self._make_request(interface)
self.last_results.extend(
[x.find_all('td')[1].text
for x in BS(request.content, "html.parser")
.find_all(class_='section-row')]
)
self.last_results = []
for interface in range(INTERFACES):
request = self._make_request(interface)
self.last_results.extend(
[x.find_all('td')[1].text
for x in BS(request.content, "html.parser")
.find_all(class_='section-row')]
)
return True
return True
def _make_request(self, unit=0):
# No, the '&&' is not a typo - this is expected by the web interface.

View File

@@ -1,7 +1,5 @@
"""Support for Linksys Smart Wifi routers."""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -10,9 +8,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__)
@@ -36,8 +32,6 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Check if the access point is accessible
@@ -55,48 +49,46 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
"""Return the name (if known) of the device."""
return self.last_results.get(mac)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
with self.lock:
_LOGGER.info("Checking Linksys Smart Wifi")
_LOGGER.info("Checking Linksys Smart Wifi")
self.last_results = {}
response = self._make_request()
if response.status_code != 200:
_LOGGER.error(
"Got HTTP status code %d when getting device list",
response.status_code)
return False
try:
data = response.json()
result = data["responses"][0]
devices = result["output"]["devices"]
for device in devices:
macs = device["knownMACAddresses"]
if not macs:
_LOGGER.warning(
"Skipping device without known MAC address")
continue
mac = macs[-1]
connections = device["connections"]
if not connections:
_LOGGER.debug("Device %s is not connected", mac)
continue
self.last_results = {}
response = self._make_request()
if response.status_code != 200:
_LOGGER.error(
"Got HTTP status code %d when getting device list",
response.status_code)
return False
try:
data = response.json()
result = data["responses"][0]
devices = result["output"]["devices"]
for device in devices:
macs = device["knownMACAddresses"]
if not macs:
_LOGGER.warning(
"Skipping device without known MAC address")
continue
mac = macs[-1]
connections = device["connections"]
if not connections:
_LOGGER.debug("Device %s is not connected", mac)
continue
name = None
for prop in device["properties"]:
if prop["name"] == "userDeviceName":
name = prop["value"]
if not name:
name = device.get("friendlyName", device["deviceID"])
name = None
for prop in device["properties"]:
if prop["name"] == "userDeviceName":
name = prop["value"]
if not name:
name = device.get("friendlyName", device["deviceID"])
_LOGGER.debug("Device %s is connected", mac)
self.last_results[mac] = name
except (KeyError, IndexError):
_LOGGER.exception("Router returned unexpected response")
return False
return True
_LOGGER.debug("Device %s is connected", mac)
self.last_results[mac] = name
except (KeyError, IndexError):
_LOGGER.exception("Router returned unexpected response")
return False
return True
def _make_request(self):
# Weirdly enough, this doesn't seem to require authentication

View File

@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.luci/
import json
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -18,9 +16,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -55,12 +50,8 @@ class LuciDeviceScanner(DeviceScanner):
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.refresh_token()
self.mac2name = None
self.success_init = self.token is not None
@@ -75,24 +66,22 @@ class LuciDeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
if self.mac2name is None:
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
result = _req_json_rpc(url, 'get_all', 'dhcp',
params={'auth': self.token})
if result:
hosts = [x for x in result.values()
if x['.type'] == 'host' and
'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _req_json_rpc
return
return self.mac2name.get(device.upper(), None)
if self.mac2name is None:
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
result = _req_json_rpc(url, 'get_all', 'dhcp',
params={'auth': self.token})
if result:
hosts = [x for x in result.values()
if x['.type'] == 'host' and
'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _req_json_rpc
return
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Luci router is up to date.
@@ -101,31 +90,30 @@ class LuciDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
_LOGGER.info("Checking ARP")
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
try:
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
except InvalidLuciTokenError:
_LOGGER.info("Refreshing token")
self.refresh_token()
return False
if result:
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
return True
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
try:
result = _req_json_rpc(url, 'net.arptable',
params={'auth': self.token})
except InvalidLuciTokenError:
_LOGGER.info("Refreshing token")
self.refresh_token()
return False
if result:
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
return True
return False
def _req_json_rpc(url, method, *args, **kwargs):
"""Perform one JSON RPC operation."""

View File

@@ -5,25 +5,17 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mikrotik/
"""
import logging
import threading
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
CONF_PORT)
from homeassistant.util import Throttle
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
REQUIREMENTS = ['librouteros==1.0.2']
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MTK_DEFAULT_API_PORT = '8728'
_LOGGER = logging.getLogger(__name__)
@@ -54,12 +46,9 @@ class MikrotikScanner(DeviceScanner):
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.connected = False
self.success_init = False
self.client = None
self.wireless_exist = None
self.success_init = self.connect_to_device()
@@ -118,51 +107,48 @@ class MikrotikScanner(DeviceScanner):
def get_device_name(self, mac):
"""Return the name of the given device or None if we don't know."""
with self.lock:
return self.last_results.get(mac)
return self.last_results.get(mac)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Retrieve latest information from the Mikrotik box."""
with self.lock:
if self.wireless_exist:
devices_tracker = 'wireless'
else:
devices_tracker = 'ip'
if self.wireless_exist:
devices_tracker = 'wireless'
else:
devices_tracker = 'ip'
_LOGGER.info(
"Loading %s devices from Mikrotik (%s) ...",
devices_tracker,
self.host
_LOGGER.info(
"Loading %s devices from Mikrotik (%s) ...",
devices_tracker,
self.host
)
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if self.wireless_exist:
devices = self.client(
cmd='/interface/wireless/registration-table/getall'
)
else:
devices = device_names
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if self.wireless_exist:
devices = self.client(
cmd='/interface/wireless/registration-table/getall'
)
else:
devices = device_names
if device_names is None and devices is None:
return False
if device_names is None and devices is None:
return False
mac_names = {device.get('mac-address'): device.get('host-name')
for device in device_names
if device.get('mac-address')}
mac_names = {device.get('mac-address'): device.get('host-name')
for device in device_names
if device.get('mac-address')}
if self.wireless_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in devices
}
else:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in device_names
if device.get('active-address')
}
if self.wireless_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in devices
}
else:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in device_names
if device.get('active-address')
}
return True
return True

View File

@@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.netgear/
"""
import logging
import threading
from datetime import timedelta
import voluptuous as vol
@@ -15,14 +13,11 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
from homeassistant.util import Throttle
REQUIREMENTS = ['pynetgear==0.3.3']
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
DEFAULT_HOST = 'routerlogin.net'
DEFAULT_USER = 'admin'
DEFAULT_PORT = 5000
@@ -56,8 +51,6 @@ class NetgearDeviceScanner(DeviceScanner):
import pynetgear
self.last_results = []
self.lock = threading.Lock()
self._api = pynetgear.Netgear(password, host, username, port)
_LOGGER.info("Logging in")
@@ -85,7 +78,6 @@ class NetgearDeviceScanner(DeviceScanner):
except StopIteration:
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Retrieve latest information from the Netgear router.
@@ -94,12 +86,11 @@ class NetgearDeviceScanner(DeviceScanner):
if not self.success_init:
return
with self.lock:
_LOGGER.info("Scanning")
_LOGGER.info("Scanning")
results = self._api.get_attached_devices()
results = self._api.get_attached_devices()
if results is None:
_LOGGER.warning("Error scanning devices")
if results is None:
_LOGGER.warning("Error scanning devices")
self.last_results = results or []
self.last_results = results or []

View File

@@ -4,11 +4,11 @@ Support for scanning a network with nmap.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.nmap_tracker/
"""
from datetime import timedelta
import logging
import re
import subprocess
from collections import namedtuple
from datetime import timedelta
import voluptuous as vol
@@ -17,7 +17,6 @@ import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOSTS
from homeassistant.util import Throttle
REQUIREMENTS = ['python-nmap==0.6.1']
@@ -29,8 +28,6 @@ CONF_HOME_INTERVAL = 'home_interval'
CONF_OPTIONS = 'scan_options'
DEFAULT_OPTIONS = '-F --host-timeout 5s'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.ensure_list,
@@ -97,7 +94,6 @@ class NmapDeviceScanner(DeviceScanner):
return filter_named[0]
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Scan the network for devices.

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.sky_hub/
"""
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -16,13 +14,10 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
})
@@ -43,11 +38,7 @@ class SkyHubDeviceScanner(DeviceScanner):
"""Initialise the scanner."""
_LOGGER.info("Initialising Sky Hub")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/'.format(self.host)
# Test the router is accessible
@@ -62,17 +53,15 @@ class SkyHubDeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
if not self.last_results:
return None
if not self.last_results:
return None
return self.last_results.get(device)
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Sky Hub is up to date.
@@ -81,18 +70,17 @@ class SkyHubDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
_LOGGER.info("Scanning")
data = _get_skyhub_data(self.url)
data = _get_skyhub_data(self.url)
if not data:
_LOGGER.warning('Error scanning devices')
return False
if not data:
_LOGGER.warning('Error scanning devices')
return False
self.last_results = data
self.last_results = data
return True
return True
def _get_skyhub_data(url):

View File

@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.snmp/
"""
import binascii
import logging
import threading
from datetime import timedelta
import voluptuous as vol
@@ -15,7 +13,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -28,8 +25,6 @@ CONF_BASEOID = 'baseoid'
DEFAULT_COMMUNITY = 'public'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
@@ -68,9 +63,6 @@ class SnmpScanner(DeviceScanner):
privProtocol=cfg.usmAesCfb128Protocol
)
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
self.lock = threading.Lock()
self.last_results = []
# Test the router is accessible
@@ -90,7 +82,6 @@ class SnmpScanner(DeviceScanner):
# We have no names
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the device is up to date.
@@ -99,13 +90,12 @@ class SnmpScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
data = self.get_snmp_data()
if not data:
return False
data = self.get_snmp_data()
if not data:
return False
self.last_results = data
return True
self.last_results = data
return True
def get_snmp_data(self):
"""Fetch MAC addresses from access point via SNMP."""

View File

@@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.swisscom/
"""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -15,9 +13,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -41,9 +36,6 @@ class SwisscomDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible.
@@ -64,7 +56,6 @@ class SwisscomDeviceScanner(DeviceScanner):
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Swisscom router is up to date.
@@ -73,16 +64,15 @@ class SwisscomDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Loading data from Swisscom Internet Box")
data = self.get_swisscom_data()
if not data:
return False
_LOGGER.info("Loading data from Swisscom Internet Box")
data = self.get_swisscom_data()
if not data:
return False
active_clients = [client for client in data.values() if
client['status']]
self.last_results = active_clients
return True
active_clients = [client for client in data.values() if
client['status']]
self.last_results = active_clients
return True
def get_swisscom_data(self):
"""Retrieve data from Swisscom and return parsed result."""

View File

@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.thomson/
import logging
import re
import telnetlib
import threading
from datetime import timedelta
import voluptuous as vol
@@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
@@ -54,9 +49,6 @@ class ThomsonDeviceScanner(DeviceScanner):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible.
@@ -77,7 +69,6 @@ class ThomsonDeviceScanner(DeviceScanner):
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the THOMSON router is up to date.
@@ -86,17 +77,16 @@ class ThomsonDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
data = self.get_thomson_data()
if not data:
return False
_LOGGER.info("Checking ARP")
data = self.get_thomson_data()
if not data:
return False
# Flag C stands for CONNECTED
active_clients = [client for client in data.values() if
client['status'].find('C') != -1]
self.last_results = active_clients
return True
# Flag C stands for CONNECTED
active_clients = [client for client in data.values() if
client['status'].find('C') != -1]
self.last_results = active_clients
return True
def get_thomson_data(self):
"""Retrieve data from THOMSON and return parsed result."""

View File

@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.tomato/
import json
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -17,9 +15,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
CONF_HTTP_ID = 'http_id'
@@ -54,8 +49,6 @@ class TomatoDeviceScanner(DeviceScanner):
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
self.lock = threading.Lock()
self.last_results = {"wldev": [], "dhcpd_lease": []}
self.success_init = self._update_tomato_info()
@@ -76,50 +69,48 @@ class TomatoDeviceScanner(DeviceScanner):
return filter_named[0]
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_tomato_info(self):
"""Ensure the information from the Tomato router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
self.logger.info("Scanning")
self.logger.info("Scanning")
try:
response = requests.Session().send(self.req, timeout=3)
# Calling and parsing the Tomato api here. We only need the
# wldev and dhcpd_lease values.
if response.status_code == 200:
try:
response = requests.Session().send(self.req, timeout=3)
# Calling and parsing the Tomato api here. We only need the
# wldev and dhcpd_lease values.
if response.status_code == 200:
for param, value in \
self.parse_api_pattern.findall(response.text):
for param, value in \
self.parse_api_pattern.findall(response.text):
if param == 'wldev' or param == 'dhcpd_lease':
self.last_results[param] = \
json.loads(value.replace("'", '"'))
return True
if param == 'wldev' or param == 'dhcpd_lease':
self.last_results[param] = \
json.loads(value.replace("'", '"'))
return True
elif response.status_code == 401:
# Authentication error
self.logger.exception((
"Failed to authenticate, "
"please check your username and password"))
return False
except requests.exceptions.ConnectionError:
# We get this if we could not connect to the router or
# an invalid http_id was supplied.
self.logger.exception("Failed to connect to the router or "
"invalid http_id supplied")
elif response.status_code == 401:
# Authentication error
self.logger.exception((
"Failed to authenticate, "
"please check your username and password"))
return False
except requests.exceptions.Timeout:
# We get this if we could not connect to the router or
# an invalid http_id was supplied.
self.logger.exception("Connection to the router timed out")
return False
except requests.exceptions.ConnectionError:
# We get this if we could not connect to the router or
# an invalid http_id was supplied.
self.logger.exception("Failed to connect to the router or "
"invalid http_id supplied")
return False
except ValueError:
# If JSON decoder could not parse the response.
self.logger.exception("Failed to parse response from router")
return False
except requests.exceptions.Timeout:
# We get this if we could not connect to the router or
# an invalid http_id was supplied.
self.logger.exception("Connection to the router timed out")
return False
except ValueError:
# If JSON decoder could not parse the response.
self.logger.exception("Failed to parse response from router")
return False

View File

@@ -8,8 +8,7 @@ import base64
import hashlib
import logging
import re
import threading
from datetime import timedelta, datetime
from datetime import datetime
import requests
import voluptuous as vol
@@ -18,9 +17,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
@@ -59,7 +55,6 @@ class TplinkDeviceScanner(DeviceScanner):
self.password = password
self.last_results = {}
self.lock = threading.Lock()
self.success_init = self._update_info()
def scan_devices(self):
@@ -72,28 +67,26 @@ class TplinkDeviceScanner(DeviceScanner):
"""Get firmware doesn't save the name of the wireless device."""
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the TP-Link router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
page = requests.get(
url, auth=(self.username, self.password),
headers={'referer': referer}, timeout=4)
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
page = requests.get(
url, auth=(self.username, self.password),
headers={'referer': referer}, timeout=4)
result = self.parse_macs.findall(page.text)
result = self.parse_macs.findall(page.text)
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
return True
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
return True
return False
return False
class Tplink2DeviceScanner(TplinkDeviceScanner):
@@ -109,48 +102,46 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
"""Get firmware doesn't save the name of the wireless device."""
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the TP-Link router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/data/map_access_wireless_client_grid.json' \
.format(self.host)
referer = 'http://{}'.format(self.host)
url = 'http://{}/data/map_access_wireless_client_grid.json' \
.format(self.host)
referer = 'http://{}'.format(self.host)
# Router uses Authorization cookie instead of header
# Let's create the cookie
username_password = '{}:{}'.format(self.username, self.password)
b64_encoded_username_password = base64.b64encode(
username_password.encode('ascii')
).decode('ascii')
cookie = 'Authorization=Basic {}' \
.format(b64_encoded_username_password)
# Router uses Authorization cookie instead of header
# Let's create the cookie
username_password = '{}:{}'.format(self.username, self.password)
b64_encoded_username_password = base64.b64encode(
username_password.encode('ascii')
).decode('ascii')
cookie = 'Authorization=Basic {}' \
.format(b64_encoded_username_password)
response = requests.post(
url, headers={'referer': referer, 'cookie': cookie},
timeout=4)
try:
result = response.json().get('data')
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac_addr'].replace('-', ':'): device['name']
for device in result
}
return True
response = requests.post(
url, headers={'referer': referer, 'cookie': cookie},
timeout=4)
try:
result = response.json().get('data')
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac_addr'].replace('-', ':'): device['name']
for device in result
}
return True
return False
class Tplink3DeviceScanner(TplinkDeviceScanner):
"""This class queries the Archer C9 router with version 150811 or high."""
@@ -202,70 +193,67 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
response.text)
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the TP-Link router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
if (self.stok == '') or (self.sysauth == ''):
self._get_auth_tokens()
if (self.stok == '') or (self.sysauth == ''):
self._get_auth_tokens()
_LOGGER.info("Loading wireless clients...")
_LOGGER.info("Loading wireless clients...")
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
'form=statistics').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
'form=statistics').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
response = requests.post(url,
params={'operation': 'load'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth},
timeout=5)
response = requests.post(url,
params={'operation': 'load'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth},
timeout=5)
try:
json_response = response.json()
try:
json_response = response.json()
if json_response.get('success'):
result = response.json().get('data')
else:
if json_response.get('errorcode') == 'timeout':
_LOGGER.info("Token timed out. Relogging on next scan")
self.stok = ''
self.sysauth = ''
return False
_LOGGER.error(
"An unknown error happened while fetching data")
if json_response.get('success'):
result = response.json().get('data')
else:
if json_response.get('errorcode') == 'timeout':
_LOGGER.info("Token timed out. Relogging on next scan")
self.stok = ''
self.sysauth = ''
return False
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct")
_LOGGER.error(
"An unknown error happened while fetching data")
return False
if result:
self.last_results = {
device['mac'].replace('-', ':'): device['mac']
for device in result
}
return True
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct")
return False
if result:
self.last_results = {
device['mac'].replace('-', ':'): device['mac']
for device in result
}
return True
return False
def _log_out(self):
with self.lock:
_LOGGER.info("Logging out of router admin interface...")
_LOGGER.info("Logging out of router admin interface...")
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
'form=logout').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
'form=logout').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
requests.post(url,
params={'operation': 'write'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
self.stok = ''
self.sysauth = ''
requests.post(url,
params={'operation': 'write'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
self.stok = ''
self.sysauth = ''
class Tplink4DeviceScanner(TplinkDeviceScanner):
@@ -318,38 +306,36 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
_LOGGER.error("Couldn't fetch auth tokens")
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the TP-Link router is up to date.
Return boolean if scanning successful.
"""
with self.lock:
if (self.credentials == '') or (self.token == ''):
self._get_auth_tokens()
if (self.credentials == '') or (self.token == ''):
self._get_auth_tokens()
_LOGGER.info("Loading wireless clients...")
_LOGGER.info("Loading wireless clients...")
mac_results = []
mac_results = []
# Check both the 2.4GHz and 5GHz client list URLs
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
url = 'http://{}/{}/userRpm/{}' \
.format(self.host, self.token, clients_url)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
# Check both the 2.4GHz and 5GHz client list URLs
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
url = 'http://{}/{}/userRpm/{}' \
.format(self.host, self.token, clients_url)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
mac_results.extend(self.parse_macs.findall(page.text))
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
mac_results.extend(self.parse_macs.findall(page.text))
if not mac_results:
return False
if not mac_results:
return False
self.last_results = [mac.replace("-", ":") for mac in mac_results]
return True
self.last_results = [mac.replace("-", ":") for mac in mac_results]
return True
class Tplink5DeviceScanner(TplinkDeviceScanner):
@@ -365,69 +351,67 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
"""Get firmware doesn't save the name of the wireless device."""
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the TP-Link AP is up to date.
Return boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
_LOGGER.info("Loading wireless clients...")
base_url = 'http://{}'.format(self.host)
base_url = 'http://{}'.format(self.host)
header = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
" rv:53.0) Gecko/20100101 Firefox/53.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; "
"charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": "http://" + self.host + "/",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Cache-Control": "no-cache"
}
header = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
" rv:53.0) Gecko/20100101 Firefox/53.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; "
"charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": "http://" + self.host + "/",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Cache-Control": "no-cache"
}
password_md5 = hashlib.md5(
self.password.encode('utf')).hexdigest().upper()
password_md5 = hashlib.md5(
self.password.encode('utf')).hexdigest().upper()
# create a session to handle cookie easier
session = requests.session()
session.get(base_url, headers=header)
# create a session to handle cookie easier
session = requests.session()
session.get(base_url, headers=header)
login_data = {"username": self.username, "password": password_md5}
session.post(base_url, login_data, headers=header)
login_data = {"username": self.username, "password": password_md5}
session.post(base_url, login_data, headers=header)
# a timestamp is required to be sent as get parameter
timestamp = int(datetime.now().timestamp() * 1e3)
# a timestamp is required to be sent as get parameter
timestamp = int(datetime.now().timestamp() * 1e3)
client_list_url = '{}/data/monitor.client.client.json'.format(
base_url)
client_list_url = '{}/data/monitor.client.client.json'.format(
base_url)
get_params = {
'operation': 'load',
'_': timestamp
}
response = session.get(client_list_url,
headers=header,
params=get_params)
session.close()
try:
list_of_devices = response.json()
except ValueError:
_LOGGER.error("AP didn't respond with JSON. "
"Check if credentials are correct.")
return False
if list_of_devices:
self.last_results = {
device['MAC'].replace('-', ':'): device['DeviceName']
for device in list_of_devices['data']
}
return True
get_params = {
'operation': 'load',
'_': timestamp
}
response = session.get(client_list_url,
headers=header,
params=get_params)
session.close()
try:
list_of_devices = response.json()
except ValueError:
_LOGGER.error("AP didn't respond with JSON. "
"Check if credentials are correct.")
return False
if list_of_devices:
self.last_results = {
device['MAC'].replace('-', ':'): device['DeviceName']
for device in list_of_devices['data']
}
return True
return False

View File

@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.ubus/
import json
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -17,12 +15,8 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
from homeassistant.exceptions import HomeAssistantError
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -70,7 +64,6 @@ class UbusDeviceScanner(DeviceScanner):
self.password = config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/ubus'.format(host)
@@ -89,33 +82,31 @@ class UbusDeviceScanner(DeviceScanner):
@_refresh_on_acccess_denied
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
if self.leasefile is None:
result = _req_json_rpc(
self.url, self.session_id, 'call', 'uci', 'get',
config="dhcp", type="dnsmasq")
if result:
values = result["values"].values()
self.leasefile = next(iter(values))["leasefile"]
else:
return
if self.leasefile is None:
result = _req_json_rpc(
self.url, self.session_id, 'call', 'uci', 'get',
config="dhcp", type="dnsmasq")
if result:
values = result["values"].values()
self.leasefile = next(iter(values))["leasefile"]
else:
return
if self.mac2name is None:
result = _req_json_rpc(
self.url, self.session_id, 'call', 'file', 'read',
path=self.leasefile)
if result:
self.mac2name = dict()
for line in result["data"].splitlines():
hosts = line.split(" ")
self.mac2name[hosts[1].upper()] = hosts[3]
else:
# Error, handled in the _req_json_rpc
return
if self.mac2name is None:
result = _req_json_rpc(
self.url, self.session_id, 'call', 'file', 'read',
path=self.leasefile)
if result:
self.mac2name = dict()
for line in result["data"].splitlines():
hosts = line.split(" ")
self.mac2name[hosts[1].upper()] = hosts[3]
else:
# Error, handled in the _req_json_rpc
return
return self.mac2name.get(device.upper(), None)
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
@_refresh_on_acccess_denied
def _update_info(self):
"""Ensure the information from the Luci router is up to date.
@@ -125,25 +116,24 @@ class UbusDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
_LOGGER.info("Checking ARP")
if not self.hostapd:
hostapd = _req_json_rpc(
self.url, self.session_id, 'list', 'hostapd.*', '')
self.hostapd.extend(hostapd.keys())
if not self.hostapd:
hostapd = _req_json_rpc(
self.url, self.session_id, 'list', 'hostapd.*', '')
self.hostapd.extend(hostapd.keys())
self.last_results = []
results = 0
for hostapd in self.hostapd:
result = _req_json_rpc(
self.url, self.session_id, 'call', hostapd, 'get_clients')
self.last_results = []
results = 0
for hostapd in self.hostapd:
result = _req_json_rpc(
self.url, self.session_id, 'call', hostapd, 'get_clients')
if result:
results = results + 1
self.last_results.extend(result['clients'].keys())
if result:
results = results + 1
self.last_results.extend(result['clients'].keys())
return bool(results)
return bool(results)
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):

View File

@@ -9,8 +9,7 @@ import logging
from homeassistant.util import slugify
from homeassistant.helpers.dispatcher import (
dispatcher_connect, dispatcher_send)
from homeassistant.components.volvooncall import (
DATA_KEY, SIGNAL_VEHICLE_SEEN)
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN
_LOGGER = logging.getLogger(__name__)

View File

@@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.xiaomi/
"""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
@@ -15,12 +13,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME, default='admin'): cv.string,
@@ -47,8 +42,6 @@ class XiaomiDeviceScanner(DeviceScanner):
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
self.token = _get_token(self.host, self.username, self.password)
@@ -62,21 +55,19 @@ class XiaomiDeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
if self.mac2name is None:
result = self._retrieve_list_with_retry()
if result:
hosts = [x for x in result
if 'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _retrieve_list_with_retry
return
return self.mac2name.get(device.upper(), None)
if self.mac2name is None:
result = self._retrieve_list_with_retry()
if result:
hosts = [x for x in result
if 'mac' in x and 'name' in x]
mac2name_list = [
(x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list)
else:
# Error, handled in the _retrieve_list_with_retry
return
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the informations from the router are up to date.
@@ -85,12 +76,11 @@ class XiaomiDeviceScanner(DeviceScanner):
if not self.success_init:
return False
with self.lock:
result = self._retrieve_list_with_retry()
if result:
self._store_result(result)
return True
return False
result = self._retrieve_list_with_retry()
if result:
self._store_result(result)
return True
return False
def _retrieve_list_with_retry(self):
"""Retrieve the device list with a retry if token is invalid.

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.0.1']
REQUIREMENTS = ['netdisco==1.1.0']
DOMAIN = 'discovery'

View File

@@ -0,0 +1,187 @@
"""
Support for Velbus platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.fan import (
SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED,
PLATFORM_SCHEMA)
from homeassistant.components.velbus import DOMAIN
from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel_low'): cv.positive_int,
vol.Required('channel_medium'): cv.positive_int,
vol.Required('channel_high'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Fans."""
velbus = hass.data[DOMAIN]
add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES])
class VelbusFan(FanEntity):
"""Representation of a Velbus Fan."""
def __init__(self, fan, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = fan[CONF_NAME]
self._module = fan['module']
self._channel_low = fan['channel_low']
self._channel_medium = fan['channel_medium']
self._channel_high = fan['channel_high']
self._channels = [self._channel_low, self._channel_medium,
self._channel_high]
self._channels_state = [False, False, False]
self._speed = STATE_OFF
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel in self._channels:
if message.channel == self._channel_low:
self._channels_state[0] = message.is_on()
elif message.channel == self._channel_medium:
self._channels_state[1] = message.is_on()
elif message.channel == self._channel_high:
self._channels_state[2] = message.is_on()
self._calculate_speed()
self.schedule_update_ha_state()
def _calculate_speed(self):
if self._is_off():
self._speed = STATE_OFF
elif self._is_low():
self._speed = SPEED_LOW
elif self._is_medium():
self._speed = SPEED_MEDIUM
elif self._is_high():
self._speed = SPEED_HIGH
def _is_off(self):
return self._channels_state[0] is False and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_low(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_medium(self):
return self._channels_state[0] is True and \
self._channels_state[1] is True and \
self._channels_state[2] is False
def _is_high(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is True
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def speed(self):
"""Return the current speed."""
return self._speed
@property
def speed_list(self):
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed, **kwargs):
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self):
"""Turn off the entity."""
self.set_speed(STATE_OFF)
def set_speed(self, speed):
"""Set the speed of the fan."""
channels_off = []
channels_on = []
if speed == STATE_OFF:
channels_off = self._channels
elif speed == SPEED_LOW:
channels_off = [self._channel_medium, self._channel_high]
channels_on = [self._channel_low]
elif speed == SPEED_MEDIUM:
channels_off = [self._channel_high]
channels_on = [self._channel_low, self._channel_medium]
elif speed == SPEED_HIGH:
channels_off = [self._channel_medium]
channels_on = [self._channel_low, self._channel_high]
for channel in channels_off:
self._relay_off(channel)
for channel in channels_on:
self._relay_on(channel)
self.schedule_update_ha_state()
def _relay_on(self, channel):
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def _relay_off(self, channel):
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = self._channels
self._velbus.send(message)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_SET_SPEED

View File

@@ -3,22 +3,22 @@
FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "7bd9aa75b2602768e66cf7e20845d7c4",
"frontend.html": "c44e49b9a0d9b9e4a626b7af34ca97d0",
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "72a5c1856cece8d9246328e84185ab0b",
"panels/ha-panel-config.html": "c0e043028cfa75d6d4dc5e0de0bb6dc1",
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
"panels/ha-panel-automation.html": "1982116c49ad26ee8d89295edc797084",
"panels/ha-panel-config.html": "fafeac72f83dd6cc42218f8978f6a7af",
"panels/ha-panel-dev-event.html": "77784d5f0c73fcc3b29b6cc050bdf324",
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
"panels/ha-panel-dev-service.html": "ac2c50e486927dc4443e93d79f08c06e",
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
"panels/ha-panel-dev-template.html": "82cd543177c417e5c6612e07df851e6b",
"panels/ha-panel-hassio.html": "96d563215cf7bf7b0eeaf8625bafa4ef",
"panels/ha-panel-dev-service.html": "86a42a17f4894478b6b77bc636beafd0",
"panels/ha-panel-dev-state.html": "31ef6ffe3347cdda5bb0cbbc54b62cde",
"panels/ha-panel-dev-template.html": "d1d76e20fe9622cddee33e67318abde8",
"panels/ha-panel-hassio.html": "262d31efd9add719e0325da5cf79a096",
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
"panels/ha-panel-iframe.html": "238189f21e670b6dcfac937e5ebd7d3b",
"panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094",
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
"panels/ha-panel-map.html": "b4923812c695dd8a69ad3da380ffe7b4",
"panels/ha-panel-shopping-list.html": "75602d06b41702c8093bd91c10374101",
"panels/ha-panel-zwave.html": "8c8e7844d33163f560e1f691550a8369"
"panels/ha-panel-map.html": "50501cd53eb4304e9e46eb719aa894b7",
"panels/ha-panel-shopping-list.html": "c04af28c6475b90cbf2cf63ba1b841d0",
"panels/ha-panel-zwave.html": "422f95f820f8b6b231265351ffcf4dd1"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-hassio"><template><style>[hidden]{display:none!important}</style><hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main></template></dom-module><script>Polymer({is:"ha-panel-hassio",properties:{hass:Object,narrow:Boolean,showMenu:Boolean,route:{type:Object,value:{prefix:"/hassio",path:"",__queryParams:{}}},loaded:{type:Boolean,value:!1}},attached:function(){window.HASS_DEV||this.importHref("/api/hassio/panel",null,function(){alert("Failed to load the Hass.io panel from supervisor.")})}})</script></body></html>
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-hassio"><template><style>[hidden]{display:none!important}</style><hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main></template></dom-module><script>Polymer({is:"ha-panel-hassio",properties:{hass:Object,narrow:Boolean,showMenu:Boolean,route:Object,loaded:{type:Boolean,value:!1}},attached:function(){window.HASS_DEV||this.importHref("/api/hassio/panel",null,function(){alert("Failed to load the Hass.io panel from supervisor.")})}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -119,19 +119,42 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
from sqlalchemy import and_, func
with session_scope(hass=hass) as session:
most_recent_state_ids = session.query(
func.max(States.state_id).label('max_state_id')
).filter(
(States.created >= run.start) &
(States.created < utc_point_in_time) &
(~States.domain.in_(IGNORE_DOMAINS)))
if entity_ids and len(entity_ids) == 1:
# Use an entirely different (and extremely fast) query if we only
# have a single entity id
most_recent_state_ids = session.query(
States.state_id.label('max_state_id')
).filter(
(States.created < utc_point_in_time) &
(States.entity_id.in_(entity_ids))
).order_by(
States.created.desc())
if filters:
most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
if filters:
most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
most_recent_state_ids = most_recent_state_ids.group_by(
States.entity_id).subquery()
most_recent_state_ids = most_recent_state_ids.limit(1)
else:
# We have more than one entity to look at (most commonly we want
# all entities,) so we need to do a search on all states since the
# last recorder run started.
most_recent_state_ids = session.query(
func.max(States.state_id).label('max_state_id')
).filter(
(States.created >= run.start) &
(States.created < utc_point_in_time) &
(~States.domain.in_(IGNORE_DOMAINS)))
if filters:
most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
most_recent_state_ids = most_recent_state_ids.group_by(
States.entity_id)
most_recent_state_ids = most_recent_state_ids.subquery()
query = session.query(States).join(most_recent_state_ids, and_(
States.state_id == most_recent_state_ids.c.max_state_id))

View File

@@ -94,20 +94,25 @@ class KeyboardRemote(threading.Thread):
if self.dev is not None:
_LOGGER.debug("Keyboard connected, %s", self.device_id)
else:
id_folder = '/dev/input/by-id/'
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
_LOGGER.debug(
'Keyboard not connected, %s.\n\
Check /dev/input/event* permissions.\
Possible device names are:\n %s.\n \
Possible device descriptors are %s:\n %s',
self.device_id,
device_names,
id_folder,
os.listdir(id_folder)
Check /dev/input/event* permissions.',
self.device_id
)
id_folder = '/dev/input/by-id/'
if os.path.isdir(id_folder):
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
_LOGGER.debug(
'Possible device names are:\n %s.\n \
Possible device descriptors are %s:\n %s',
device_names,
id_folder,
os.listdir(id_folder)
)
threading.Thread.__init__(self)
self.stopped = threading.Event()
self.hass = hass

View File

@@ -33,7 +33,7 @@ import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.5.2', 'aiolifx_effects==0.1.1']
REQUIREMENTS = ['aiolifx==0.5.4', 'aiolifx_effects==0.1.1']
UDP_BROADCAST_PORT = 56700

View File

@@ -0,0 +1,116 @@
"""
Support for TPLink lights.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.tplink/
"""
import logging
from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP)
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
from homeassistant.util.color import \
color_temperature_kelvin_to_mired as kelvin_to_mired
REQUIREMENTS = ['pyHS100==0.2.4.2']
_LOGGER = logging.getLogger(__name__)
SUPPORT_TPLINK = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Initialise pyLB100 SmartBulb."""
from pyHS100 import SmartBulb
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
add_devices([TPLinkSmartBulb(SmartBulb(host), name)], True)
def brightness_to_percentage(byt):
"""Convert brightness from absolute 0..255 to percentage."""
return (byt*100.0)/255.0
def brightness_from_percentage(percent):
"""Convert percentage to absolute value 0..255."""
return (percent*255.0)/100.0
class TPLinkSmartBulb(Light):
"""Representation of a TPLink Smart Bulb."""
def __init__(self, smartbulb, name):
"""Initialize the bulb."""
self.smartbulb = smartbulb
# Use the name set on the device if not set
if name is None:
self._name = self.smartbulb.alias
else:
self._name = name
self._state = None
_LOGGER.debug("Setting up TP-Link Smart Bulb")
@property
def name(self):
"""Return the name of the Smart Bulb, if any."""
return self._name
def turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_COLOR_TEMP in kwargs:
self.smartbulb.color_temp = \
mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
if ATTR_KELVIN in kwargs:
self.smartbulb.color_temp = kwargs[ATTR_KELVIN]
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)
self.smartbulb.brightness = brightness_to_percentage(brightness)
self.smartbulb.state = self.smartbulb.BULB_STATE_ON
def turn_off(self):
"""Turn the light off."""
self.smartbulb.state = self.smartbulb.BULB_STATE_OFF
@property
def color_temp(self):
"""Return the color temperature of this light in mireds for HA."""
if self.smartbulb.is_color:
if (self.smartbulb.color_temp is not None and
self.smartbulb.color_temp != 0):
return kelvin_to_mired(self.smartbulb.color_temp)
else:
return None
else:
return None
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return brightness_from_percentage(self.smartbulb.brightness)
@property
def is_on(self):
"""True if device is on."""
return self.smartbulb.state == \
self.smartbulb.BULB_STATE_ON
def update(self):
"""Update the TP-Link Bulb's state."""
from pyHS100 import SmartPlugException
try:
self._state = self.smartbulb.state == \
self.smartbulb.BULB_STATE_ON
except (SmartPlugException, OSError) as ex:
_LOGGER.warning('Could not read state for %s: %s', self.name, ex)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_TPLINK

View File

@@ -0,0 +1,104 @@
"""
Support for Velbus lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.light import Light, PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Lights."""
velbus = hass.data[DOMAIN]
add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES])
class VelbusLight(Light):
"""Representation of a Velbus Light."""
def __init__(self, light, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = light[CONF_NAME]
self._module = light['module']
self._channel = light['channel']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel == self._channel:
self._state = message.is_on()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def is_on(self):
"""Return true if the light is on."""
return self._state
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._channel]
self._velbus.send(message)

View File

@@ -9,11 +9,14 @@ import logging
from homeassistant.components import light, zha
from homeassistant.util.color import color_RGB_to_xy
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zha']
DEFAULT_DURATION = 0.5
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -48,6 +51,7 @@ class Light(zha.Entity, light.Light):
import bellows.zigbee.zcl.clusters as zcl_clusters
if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters:
self._supported_features |= light.SUPPORT_BRIGHTNESS
self._supported_features |= light.SUPPORT_TRANSITION
self._brightness = 0
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
# Not sure all color lights necessarily support this directly
@@ -62,14 +66,15 @@ class Light(zha.Entity, light.Light):
@property
def is_on(self) -> bool:
"""Return true if entity is on."""
if self._state == 'unknown':
if self._state == STATE_UNKNOWN:
return False
return bool(self._state)
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the entity on."""
duration = 5 # tenths of s
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
duration = duration * 10 # tenths of s
if light.ATTR_COLOR_TEMP in kwargs:
temperature = kwargs[light.ATTR_COLOR_TEMP]
yield from self._endpoint.light_color.move_to_color_temp(
@@ -91,7 +96,8 @@ class Light(zha.Entity, light.Light):
)
if self._brightness is not None:
brightness = kwargs.get('brightness', self._brightness or 255)
brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255)
self._brightness = brightness
# Move to level with on/off:
yield from self._endpoint.level.move_to_level_with_on_off(

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pychromecast==0.8.1']
REQUIREMENTS = ['pychromecast==0.8.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -216,6 +216,9 @@ class Metrics:
value = state_helper.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_zwave(self, state):
self._battery(state)
class PrometheusView(HomeAssistantView):
"""Handle Prometheus requests."""

View File

@@ -433,9 +433,8 @@ class FitbitSensor(Entity):
def update(self):
"""Get the latest data from the Fitbit API and update the states."""
if self.resource_type == 'devices/battery':
response = self.client.get_devices()
self._state = response[0].get('battery')
if self.resource_type == 'devices/battery' and self.extra:
self._state = self.extra.get('battery')
else:
container = self.resource_type.replace("/", "-")
response = self.client.time_series(self.resource_type, period='7d')

View File

@@ -45,20 +45,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lyft sensor."""
from lyft_rides.auth import ClientCredentialGrant
from lyft_rides.errors import APIError
auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID),
client_secret=config.get(
CONF_CLIENT_SECRET),
scopes="public",
is_sandbox_mode=False)
session = auth_flow.get_session()
try:
session = auth_flow.get_session()
timeandpriceest = LyftEstimate(
session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE],
config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE))
timeandpriceest.fetch_data()
except APIError as exc:
_LOGGER.error("Error setting up Lyft platform: %s", exc)
return False
wanted_product_ids = config.get(CONF_PRODUCT_IDS)
dev = []
timeandpriceest = LyftEstimate(
session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE],
config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE))
for product_id, product in timeandpriceest.products.items():
if (wanted_product_ids is not None) and \
(product_id not in wanted_product_ids):
@@ -188,14 +195,18 @@ class LyftEstimate(object):
self.end_latitude = end_latitude
self.end_longitude = end_longitude
self.products = None
self.__real_update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest product info and estimates from the Lyft API."""
self.__real_update()
from lyft_rides.errors import APIError
try:
self.fetch_data()
except APIError as exc:
_LOGGER.error("Error fetching Lyft data: %s", exc)
def __real_update(self):
def fetch_data(self):
"""Get the latest product info and estimates from the Lyft API."""
from lyft_rides.client import LyftRidesClient
client = LyftRidesClient(self._session)

View File

@@ -0,0 +1,275 @@
"""Support for UK public transport data provided by transportapi.com.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.uk_transport/
"""
import logging
import re
from datetime import datetime, timedelta
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_ATCOCODE = 'atcocode'
ATTR_LOCALITY = 'locality'
ATTR_STOP_NAME = 'stop_name'
ATTR_REQUEST_TIME = 'request_time'
ATTR_NEXT_BUSES = 'next_buses'
ATTR_STATION_CODE = 'station_code'
ATTR_CALLING_AT = 'calling_at'
ATTR_NEXT_TRAINS = 'next_trains'
CONF_API_APP_KEY = 'app_key'
CONF_API_APP_ID = 'app_id'
CONF_QUERIES = 'queries'
CONF_MODE = 'mode'
CONF_ORIGIN = 'origin'
CONF_DESTINATION = 'destination'
_QUERY_SCHEME = vol.Schema({
vol.Required(CONF_MODE):
vol.All(cv.ensure_list, [vol.In(list(['bus', 'train']))]),
vol.Required(CONF_ORIGIN): cv.string,
vol.Required(CONF_DESTINATION): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_APP_ID): cv.string,
vol.Required(CONF_API_APP_KEY): cv.string,
vol.Required(CONF_QUERIES): [_QUERY_SCHEME],
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Get the uk_transport sensor."""
sensors = []
number_sensors = len(config.get(CONF_QUERIES))
interval = timedelta(seconds=87*number_sensors)
for query in config.get(CONF_QUERIES):
if 'bus' in query.get(CONF_MODE):
stop_atcocode = query.get(CONF_ORIGIN)
bus_direction = query.get(CONF_DESTINATION)
sensors.append(
UkTransportLiveBusTimeSensor(
config.get(CONF_API_APP_ID),
config.get(CONF_API_APP_KEY),
stop_atcocode,
bus_direction,
interval))
elif 'train' in query.get(CONF_MODE):
station_code = query.get(CONF_ORIGIN)
calling_at = query.get(CONF_DESTINATION)
sensors.append(
UkTransportLiveTrainTimeSensor(
config.get(CONF_API_APP_ID),
config.get(CONF_API_APP_KEY),
station_code,
calling_at,
interval))
add_devices(sensors, True)
class UkTransportSensor(Entity):
"""
Sensor that reads the UK transport web API.
transportapi.com provides comprehensive transport data for UK train, tube
and bus travel across the UK via simple JSON API. Subclasses of this
base class can be used to access specific types of information.
"""
TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/"
ICON = 'mdi:train'
def __init__(self, name, api_app_id, api_app_key, url):
"""Initialize the sensor."""
self._data = {}
self._api_app_id = api_app_id
self._api_app_key = api_app_key
self._url = self.TRANSPORT_API_URL_BASE + url
self._name = name
self._state = None
@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 unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return "min"
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self.ICON
def _do_api_request(self, params):
"""Perform an API request."""
request_params = dict({
'app_id': self._api_app_id,
'app_key': self._api_app_key,
}, **params)
response = requests.get(self._url, params=request_params)
if response.status_code != 200:
_LOGGER.warning('Invalid response from API')
elif 'error' in response.json():
if 'exceeded' in response.json()['error']:
self._state = 'Useage limites exceeded'
if 'invalid' in response.json()['error']:
self._state = 'Credentials invalid'
else:
self._data = response.json()
class UkTransportLiveBusTimeSensor(UkTransportSensor):
"""Live bus time sensor from UK transportapi.com."""
ICON = 'mdi:bus'
def __init__(self, api_app_id, api_app_key,
stop_atcocode, bus_direction, interval):
"""Construct a live bus time sensor."""
self._stop_atcocode = stop_atcocode
self._bus_direction = bus_direction
self._next_buses = []
self._destination_re = re.compile(
'{}'.format(bus_direction), re.IGNORECASE
)
sensor_name = 'Next bus to {}'.format(bus_direction)
stop_url = 'bus/stop/{}/live.json'.format(stop_atcocode)
UkTransportSensor.__init__(
self, sensor_name, api_app_id, api_app_key, stop_url
)
self.update = Throttle(interval)(self._update)
def _update(self):
"""Get the latest live departure data for the specified stop."""
params = {'group': 'route', 'nextbuses': 'no'}
self._do_api_request(params)
if self._data != {}:
self._next_buses = []
for (route, departures) in self._data['departures'].items():
for departure in departures:
if self._destination_re.search(departure['direction']):
self._next_buses.append({
'route': route,
'direction': departure['direction'],
'scheduled': departure['aimed_departure_time'],
'estimated': departure['best_departure_estimate']
})
self._state = min(map(
_delta_mins, [bus['scheduled'] for bus in self._next_buses]
))
@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
attrs = {}
if self._data is not None:
for key in [
ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME,
ATTR_REQUEST_TIME
]:
attrs[key] = self._data.get(key)
attrs[ATTR_NEXT_BUSES] = self._next_buses
return attrs
class UkTransportLiveTrainTimeSensor(UkTransportSensor):
"""Live train time sensor from UK transportapi.com."""
ICON = 'mdi:train'
def __init__(self, api_app_id, api_app_key,
station_code, calling_at, interval):
"""Construct a live bus time sensor."""
self._station_code = station_code
self._calling_at = calling_at
self._next_trains = []
sensor_name = 'Next train to {}'.format(calling_at)
query_url = 'train/station/{}/live.json'.format(station_code)
UkTransportSensor.__init__(
self, sensor_name, api_app_id, api_app_key, query_url
)
self.update = Throttle(interval)(self._update)
def _update(self):
"""Get the latest live departure data for the specified stop."""
params = {'darwin': 'false',
'calling_at': self._calling_at,
'train_status': 'passenger'}
self._do_api_request(params)
self._next_trains = []
if self._data != {}:
if self._data['departures']['all'] == []:
self._state = 'No departures'
else:
for departure in self._data['departures']['all']:
self._next_trains.append({
'origin_name': departure['origin_name'],
'destination_name': departure['destination_name'],
'status': departure['status'],
'scheduled': departure['aimed_departure_time'],
'estimated': departure['expected_departure_time'],
'platform': departure['platform'],
'operator_name': departure['operator_name']
})
self._state = min(map(
_delta_mins,
[train['scheduled'] for train in self._next_trains]
))
@property
def device_state_attributes(self):
"""Return other details about the sensor state."""
attrs = {}
if self._data is not None:
attrs[ATTR_STATION_CODE] = self._station_code
attrs[ATTR_CALLING_AT] = self._calling_at
if self._next_trains:
attrs[ATTR_NEXT_TRAINS] = self._next_trains
return attrs
def _delta_mins(hhmm_time_str):
"""Calculate time delta in minutes to a time in hh:mm format."""
now = datetime.now()
hhmm_time = datetime.strptime(hhmm_time_str, '%H:%M')
hhmm_datetime = datetime(
now.year, now.month, now.day,
hour=hhmm_time.hour, minute=hhmm_time.minute
)
if hhmm_datetime < now:
hhmm_datetime += timedelta(days=1)
delta_mins = (hhmm_datetime - now).seconds // 60
return delta_mins

View File

@@ -70,11 +70,16 @@ class ListTopItemsIntent(intent.IntentHandler):
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
items = intent_obj.hass.data[DOMAIN][-5:]
response = intent_obj.create_response()
response.async_set_speech(
"These are the top 5 items in your shopping list: {}".format(
', '.join(reversed(intent_obj.hass.data[DOMAIN][-5:]))))
intent_obj.hass.bus.async_fire(EVENT)
if len(items) == 0:
response.async_set_speech(
"There are no items on your shopping list")
else:
response.async_set_speech(
"These are the top {} items on your shopping list: {}".format(
min(len(items), 5), ', '.join(reversed(items))))
return response

View File

@@ -15,6 +15,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.const import CONF_NAME, CONF_PLATFORM
from homeassistant.helpers.event import track_time_change
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import slugify
from homeassistant.util.color import (
color_temperature_to_rgb, color_RGB_to_xy,
color_temperature_kelvin_to_mired)
@@ -111,7 +112,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Update lights."""
flux.flux_update()
hass.services.register(DOMAIN, name + '_update', update)
service_name = slugify("{} {}".format(name, 'update'))
hass.services.register(DOMAIN, service_name, update)
class FluxSwitch(SwitchDevice):

View File

@@ -0,0 +1,111 @@
"""
Support for Velbus switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SWITCH_SCHEMA = {
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES):
vol.All(cv.ensure_list, [SWITCH_SCHEMA])
})
DEPENDENCIES = ['velbus']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Switch."""
velbus = hass.data[DOMAIN]
devices = []
for switch in config[CONF_DEVICES]:
devices.append(VelbusSwitch(switch, velbus))
add_devices(devices)
return True
class VelbusSwitch(SwitchDevice):
"""Representation of a switch."""
def __init__(self, switch, velbus):
"""Initialize a Velbus switch."""
self._velbus = velbus
self._name = switch[CONF_NAME]
self._module = switch['module']
self._channel = switch['channel']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel == self._channel:
self._state = message.is_on()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this switch."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def is_on(self):
"""Return true if the switch is on."""
return self._state
def turn_on(self, **kwargs):
"""Instruct the switch to turn on."""
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def turn_off(self, **kwargs):
"""Instruct the switch to turn off."""
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._channel]
self._velbus.send(message)

View File

@@ -0,0 +1,43 @@
"""
Support for Velbus platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/velbus/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT
REQUIREMENTS = ['python-velbus==2.0.11']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'velbus'
VELBUS_MESSAGE = 'velbus.message'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Velbus platform."""
import velbus
port = config[DOMAIN].get(CONF_PORT)
connection = velbus.VelbusUSBConnection(port)
controller = velbus.Controller(connection)
hass.data[DOMAIN] = controller
def stop_velbus(event):
"""Disconnect from serial port."""
_LOGGER.debug("Shutting down ")
connection.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus)
return True

View File

@@ -11,17 +11,21 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.weather import (
WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP)
WeatherEntity, PLATFORM_SCHEMA,
ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME)
from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN)
REQUIREMENTS = ["yahooweather==0.8"]
_LOGGER = logging.getLogger(__name__)
DATA_CONDITION = 'yahoo_condition'
ATTR_FORECAST_CONDITION = 'condition'
ATTRIBUTION = "Weather details provided by Yahoo! Inc."
CONF_FORECAST = 'forecast'
ATTR_FORECAST_TEMP_LOW = 'templow'
CONF_WOEID = 'woeid'
DEFAULT_NAME = 'Yweather'
@@ -33,23 +37,22 @@ CONDITION_CLASSES = {
'fog': [19, 20, 21, 22, 23],
'hail': [17, 18, 35],
'lightning': [37],
'lightning-rainy': [38, 39],
'lightning-rainy': [38, 39, 47],
'partlycloudy': [44],
'pouring': [40, 45],
'rainy': [9, 11, 12],
'snowy': [8, 13, 14, 15, 16, 41, 42, 43],
'snowy-rainy': [5, 6, 7, 10, 46, 47],
'sunny': [32],
'snowy-rainy': [5, 6, 7, 10, 46],
'sunny': [32, 33, 34],
'windy': [24],
'windy-variant': [],
'exceptional': [0, 1, 2, 3, 4, 25, 36],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_WOEID, default=None): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_FORECAST, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0, max=5)),
})
@@ -59,7 +62,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
unit = hass.config.units.temperature_unit
woeid = config.get(CONF_WOEID)
forecast = config.get(CONF_FORECAST)
name = config.get(CONF_NAME)
yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F
@@ -77,22 +79,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.critical("Can't retrieve weather data from Yahoo!")
return False
if forecast >= len(yahoo_api.yahoo.Forecast):
_LOGGER.error("Yahoo! only support %d days forecast",
len(yahoo_api.yahoo.Forecast))
return False
# create condition helper
if DATA_CONDITION not in hass.data:
hass.data[DATA_CONDITION] = [str(x) for x in range(0, 50)]
for cond, condlst in CONDITION_CLASSES.items():
for condi in condlst:
hass.data[DATA_CONDITION][condi] = cond
add_devices([YahooWeatherWeather(yahoo_api, name, forecast)], True)
add_devices([YahooWeatherWeather(yahoo_api, name)], True)
class YahooWeatherWeather(WeatherEntity):
"""Representation of Yahoo! weather data."""
def __init__(self, weather_data, name, forecast):
def __init__(self, weather_data, name):
"""Initialize the sensor."""
self._name = name
self._data = weather_data
self._forecast = forecast
@property
def name(self):
@@ -103,9 +106,9 @@ class YahooWeatherWeather(WeatherEntity):
def condition(self):
"""Return the current condition."""
try:
return [k for k, v in CONDITION_CLASSES.items() if
int(self._data.yahoo.Now['code']) in v][0]
except IndexError:
return self.hass.data[DATA_CONDITION][int(
self._data.yahoo.Now['code'])]
except (ValueError, IndexError):
return STATE_UNKNOWN
@property
@@ -138,6 +141,11 @@ class YahooWeatherWeather(WeatherEntity):
"""Return the wind speed."""
return self._data.yahoo.Wind['speed']
@property
def wind_bearing(self):
"""Return the wind direction."""
return self._data.yahoo.Wind['direction']
@property
def attribution(self):
"""Return the attribution."""
@@ -147,19 +155,17 @@ class YahooWeatherWeather(WeatherEntity):
def forecast(self):
"""Return the forecast array."""
try:
forecast_condition = \
[k for k, v in CONDITION_CLASSES.items() if
int(self._data.yahoo.Forecast[self._forecast]['code'])
in v][0]
except IndexError:
return [
{
ATTR_FORECAST_TIME: v['date'],
ATTR_FORECAST_TEMP:int(v['high']),
ATTR_FORECAST_TEMP_LOW: int(v['low']),
ATTR_FORECAST_CONDITION:
self.hass.data[DATA_CONDITION][int(v['code'])]
} for v in self._data.yahoo.Forecast]
except (ValueError, IndexError):
return STATE_UNKNOWN
return [{
ATTR_FORECAST_CONDITION: forecast_condition,
ATTR_FORECAST_TEMP:
self._data.yahoo.Forecast[self._forecast]['high'],
}]
def update(self):
"""Get the latest data from Yahoo! and updates the states."""
self._data.update()

View File

@@ -134,7 +134,7 @@ class Intent:
class IntentResponse:
"""Response to an intent."""
def __init__(self, intent):
def __init__(self, intent=None):
"""Initialize an IntentResponse."""
self.intent = intent
self.speech = {}

View File

@@ -0,0 +1,71 @@
"""Script to get, put and delete secrets stored in credstash."""
import argparse
import getpass
from homeassistant.util.yaml import _SECRET_NAMESPACE
REQUIREMENTS = ['credstash==1.13.2', 'botocore==1.4.93']
def run(args):
"""Handle credstash script."""
parser = argparse.ArgumentParser(
description=("Modify Home-Assistant secrets in credstash."
"Use the secrets in configuration files with: "
"!secret <name>"))
parser.add_argument(
'--script', choices=['credstash'])
parser.add_argument(
'action', choices=['get', 'put', 'del', 'list'],
help="Get, put or delete a secret, or list all available secrets")
parser.add_argument(
'name', help="Name of the secret", nargs='?', default=None)
parser.add_argument(
'value', help="The value to save when putting a secret",
nargs='?', default=None)
import credstash
import botocore
args = parser.parse_args(args)
table = _SECRET_NAMESPACE
try:
credstash.listSecrets(table=table)
except botocore.errorfactory.ClientError:
credstash.createDdbTable(table=table)
if args.action == 'list':
secrets = [i['name'] for i in credstash.listSecrets(table=table)]
deduped_secrets = sorted(set(secrets))
print('Saved secrets:')
for secret in deduped_secrets:
print(secret)
return 0
if args.name is None:
parser.print_help()
return 1
if args.action == 'put':
if args.value:
the_secret = args.value
else:
the_secret = getpass.getpass('Please enter the secret for {}: '
.format(args.name))
current_version = credstash.getHighestVersion(args.name, table=table)
credstash.putSecret(args.name,
the_secret,
version=int(current_version) + 1,
table=table)
print('Secret {} put successfully'.format(args.name))
elif args.action == 'get':
the_secret = credstash.getSecret(args.name, table=table)
if the_secret is None:
print('Secret {} not found'.format(args.name))
else:
print('Secret {}={}'.format(args.name, the_secret))
elif args.action == 'del':
credstash.deleteSecrets(args.name, table=table)
print('Deleted secret {}'.format(args.name))

View File

@@ -12,6 +12,11 @@ try:
except ImportError:
keyring = None
try:
import credstash
except ImportError:
credstash = None
from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
@@ -200,8 +205,13 @@ def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node):
def _env_var_yaml(loader: SafeLineLoader,
node: yaml.nodes.Node):
"""Load environment variables and embed it into the configuration YAML."""
if node.value in os.environ:
return os.environ[node.value]
args = node.value.split()
# Check for a default value
if len(args) > 1:
return os.getenv(args[0], ' '.join(args[1:]))
elif args[0] in os.environ:
return os.environ[args[0]]
else:
_LOGGER.error("Environment variable %s not defined.", node.value)
raise HomeAssistantError(node.value)
@@ -257,6 +267,15 @@ def _secret_yaml(loader: SafeLineLoader,
_LOGGER.debug("Secret %s retrieved from keyring", node.value)
return pwd
if credstash:
try:
pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE)
if pwd:
_LOGGER.debug("Secret %s retrieved from credstash", node.value)
return pwd
except credstash.ItemNotFound:
pass
_LOGGER.error("Secret %s not defined", node.value)
raise HomeAssistantError(node.value)

View File

@@ -14,6 +14,8 @@ reports=no
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
generated-members=botocore.errorfactory
disable=
abstract-class-little-used,
abstract-class-not-used,

View File

@@ -49,7 +49,7 @@ aiodns==1.1.1
aiohttp_cors==0.5.3
# homeassistant.components.light.lifx
aiolifx==0.5.2
aiolifx==0.5.4
# homeassistant.components.light.lifx
aiolifx_effects==0.1.1
@@ -61,7 +61,7 @@ aiopvapi==1.4
alarmdecoder==0.12.3
# homeassistant.components.amcrest
amcrest==1.2.0
amcrest==1.2.1
# homeassistant.components.media_player.anthemav
anthemav==1.1.8
@@ -115,6 +115,9 @@ blockchain==1.3.3
# homeassistant.components.tts.amazon_polly
boto3==1.4.3
# homeassistant.scripts.credstash
botocore==1.4.93
# homeassistant.components.sensor.broadlink
# homeassistant.components.switch.broadlink
broadlink==0.5
@@ -136,6 +139,9 @@ colorlog>2.1,<3
# homeassistant.components.binary_sensor.concord232
concord232==0.14
# homeassistant.scripts.credstash
credstash==1.13.2
# homeassistant.components.sensor.crimereports
crimereports==1.0.0
@@ -404,7 +410,7 @@ myusps==1.1.2
nad_receiver==0.0.6
# homeassistant.components.discovery
netdisco==1.0.1
netdisco==1.1.0
# homeassistant.components.sensor.neurio_energy
neurio==0.3.1
@@ -503,6 +509,7 @@ py-cpuinfo==3.3.0
# homeassistant.components.hdmi_cec
pyCEC==0.4.13
# homeassistant.components.light.tplink
# homeassistant.components.switch.tplink
pyHS100==0.2.4.2
@@ -535,7 +542,7 @@ pybbox==0.0.5-alpha
# pybluez==0.22
# homeassistant.components.media_player.cast
pychromecast==0.8.1
pychromecast==0.8.2
# homeassistant.components.media_player.cmus
pycmus==0.1.0
@@ -754,6 +761,9 @@ python-telegram-bot==6.1.0
# homeassistant.components.sensor.twitch
python-twitch==1.3.0
# homeassistant.components.velbus
python-velbus==2.0.11
# homeassistant.components.media_player.vlc
python-vlc==1.1.2

View File

@@ -0,0 +1,559 @@
"""The tests for the manual_mqtt Alarm Control Panel component."""
from datetime import timedelta
import unittest
from unittest.mock import patch
from homeassistant.setup import setup_component
from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
from homeassistant.components import alarm_control_panel
import homeassistant.util.dt as dt_util
from tests.common import (
fire_time_changed, get_test_home_assistant,
mock_mqtt_component, fire_mqtt_message, assert_setup_component)
CODE = 'HELLO_CODE'
class TestAlarmControlPanelManualMqtt(unittest.TestCase):
"""Test the manual_mqtt alarm module."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.mock_publish = mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
"""Stop down everything that was started."""
self.hass.stop()
def test_fail_setup_without_state_topic(self):
"""Test for failing with no state topic."""
with assert_setup_component(0) as config:
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'mqtt_alarm',
'command_topic': 'alarm/command'
}
})
assert not config[alarm_control_panel.DOMAIN]
def test_fail_setup_without_command_topic(self):
"""Test failing with no command topic."""
with assert_setup_component(0):
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'mqtt_alarm',
'state_topic': 'alarm/state'
}
})
def test_arm_home_no_pending(self):
"""Test arm home method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 0,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_home(self.hass, CODE)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_HOME,
self.hass.states.get(entity_id).state)
def test_arm_home_with_pending(self):
"""Test arm home method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 1,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_HOME,
self.hass.states.get(entity_id).state)
def test_arm_home_with_invalid_code(self):
"""Attempt to arm home without a valid code."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 1,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_home(self.hass, CODE + '2')
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_arm_away_no_pending(self):
"""Test arm home method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 0,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_AWAY,
self.hass.states.get(entity_id).state)
def test_arm_away_with_pending(self):
"""Test arm home method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 1,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_away(self.hass, CODE)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_AWAY,
self.hass.states.get(entity_id).state)
def test_arm_away_with_invalid_code(self):
"""Attempt to arm away without a valid code."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'code': CODE,
'pending_time': 1,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_arm_away(self.hass, CODE + '2')
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_trigger_no_pending(self):
"""Test triggering when no pending submitted method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'trigger_time': 1,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=60)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_TRIGGERED,
self.hass.states.get(entity_id).state)
def test_trigger_with_pending(self):
"""Test arm home method."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 2,
'trigger_time': 3,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_TRIGGERED,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_trigger_with_disarm_after_trigger(self):
"""Test disarm after trigger."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'trigger_time': 5,
'pending_time': 0,
'disarm_after_trigger': True,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_TRIGGERED,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_disarm_while_pending_trigger(self):
"""Test disarming while pending state."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'trigger_time': 5,
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_disarm_during_trigger_with_invalid_code(self):
"""Test disarming while code is invalid."""
self.assertTrue(setup_component(
self.hass, alarm_control_panel.DOMAIN,
{'alarm_control_panel': {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 5,
'code': CODE + '2',
'disarm_after_trigger': False,
'command_topic': 'alarm/command',
'state_topic': 'alarm/state',
}}))
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_TRIGGERED,
self.hass.states.get(entity_id).state)
def test_arm_home_via_command_topic(self):
"""Test arming home via command topic."""
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 1,
'state_topic': 'alarm/state',
'command_topic': 'alarm/command',
'payload_arm_home': 'ARM_HOME',
}
})
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
# Fire the arm command via MQTT; ensure state changes to pending
fire_mqtt_message(self.hass, 'alarm/command', 'ARM_HOME')
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_HOME,
self.hass.states.get(entity_id).state)
def test_arm_away_via_command_topic(self):
"""Test arming away via command topic."""
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 1,
'state_topic': 'alarm/state',
'command_topic': 'alarm/command',
'payload_arm_away': 'ARM_AWAY',
}
})
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
# Fire the arm command via MQTT; ensure state changes to pending
fire_mqtt_message(self.hass, 'alarm/command', 'ARM_AWAY')
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_ARMED_AWAY,
self.hass.states.get(entity_id).state)
def test_disarm_pending_via_command_topic(self):
"""Test disarming pending alarm via command topic."""
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 1,
'state_topic': 'alarm/state',
'command_topic': 'alarm/command',
'payload_disarm': 'DISARM',
}
})
entity_id = 'alarm_control_panel.test'
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
alarm_control_panel.alarm_trigger(self.hass)
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
# Now that we're pending, receive a command to disarm
fire_mqtt_message(self.hass, 'alarm/command', 'DISARM')
self.hass.block_till_done()
self.assertEqual(STATE_ALARM_DISARMED,
self.hass.states.get(entity_id).state)
def test_state_changes_are_published_to_mqtt(self):
"""Test publishing of MQTT messages when state changes."""
assert setup_component(self.hass, alarm_control_panel.DOMAIN, {
alarm_control_panel.DOMAIN: {
'platform': 'manual_mqtt',
'name': 'test',
'pending_time': 1,
'trigger_time': 1,
'state_topic': 'alarm/state',
'command_topic': 'alarm/command',
}
})
# Component should send disarmed alarm state on startup
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True),
self.mock_publish.mock_calls[-2][1])
# Arm in home mode
alarm_control_panel.alarm_arm_home(self.hass)
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True),
self.mock_publish.mock_calls[-2][1])
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_ARMED_HOME, 0, True),
self.mock_publish.mock_calls[-2][1])
# Arm in away mode
alarm_control_panel.alarm_arm_away(self.hass)
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True),
self.mock_publish.mock_calls[-2][1])
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_ARMED_AWAY, 0, True),
self.mock_publish.mock_calls[-2][1])
# Disarm
alarm_control_panel.alarm_disarm(self.hass)
self.hass.block_till_done()
self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True),
self.mock_publish.mock_calls[-2][1])

View File

@@ -0,0 +1,93 @@
"""The tests for the uk_transport platform."""
import re
import requests_mock
import unittest
from homeassistant.components.sensor.uk_transport import (
UkTransportSensor,
ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME, ATTR_NEXT_BUSES,
ATTR_STATION_CODE, ATTR_CALLING_AT, ATTR_NEXT_TRAINS,
CONF_API_APP_KEY, CONF_API_APP_ID)
from homeassistant.setup import setup_component
from tests.common import load_fixture, get_test_home_assistant
BUS_ATCOCODE = '340000368SHE'
BUS_DIRECTION = 'Wantage'
TRAIN_STATION_CODE = 'WIM'
TRAIN_DESTINATION_NAME = 'WAT'
VALID_CONFIG = {
'platform': 'uk_transport',
CONF_API_APP_ID: 'foo',
CONF_API_APP_KEY: 'ebcd1234',
'queries': [{
'mode': 'bus',
'origin': BUS_ATCOCODE,
'destination': BUS_DIRECTION},
{
'mode': 'train',
'origin': TRAIN_STATION_CODE,
'destination': TRAIN_DESTINATION_NAME}]
}
class TestUkTransportSensor(unittest.TestCase):
"""Test the uk_transport platform."""
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.config = VALID_CONFIG
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@requests_mock.Mocker()
def test_bus(self, mock_req):
"""Test for operational uk_transport sensor with proper attributes."""
with requests_mock.Mocker() as mock_req:
uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*')
mock_req.get(uri, text=load_fixture('uk_transport_bus.json'))
self.assertTrue(
setup_component(self.hass, 'sensor', {'sensor': self.config}))
bus_state = self.hass.states.get('sensor.next_bus_to_wantage')
assert type(bus_state.state) == str
assert bus_state.name == 'Next bus to {}'.format(BUS_DIRECTION)
assert bus_state.attributes.get(ATTR_ATCOCODE) == BUS_ATCOCODE
assert bus_state.attributes.get(ATTR_LOCALITY) == 'Harwell Campus'
assert bus_state.attributes.get(ATTR_STOP_NAME) == 'Bus Station'
assert len(bus_state.attributes.get(ATTR_NEXT_BUSES)) == 2
direction_re = re.compile(BUS_DIRECTION)
for bus in bus_state.attributes.get(ATTR_NEXT_BUSES):
print(bus['direction'], direction_re.match(bus['direction']))
assert direction_re.search(bus['direction']) is not None
@requests_mock.Mocker()
def test_train(self, mock_req):
"""Test for operational uk_transport sensor with proper attributes."""
with requests_mock.Mocker() as mock_req:
uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*')
mock_req.get(uri, text=load_fixture('uk_transport_train.json'))
self.assertTrue(
setup_component(self.hass, 'sensor', {'sensor': self.config}))
train_state = self.hass.states.get('sensor.next_train_to_WAT')
assert type(train_state.state) == str
assert train_state.name == 'Next train to {}'.format(
TRAIN_DESTINATION_NAME)
assert train_state.attributes.get(
ATTR_STATION_CODE) == TRAIN_STATION_CODE
assert train_state.attributes.get(
ATTR_CALLING_AT) == TRAIN_DESTINATION_NAME
assert len(train_state.attributes.get(ATTR_NEXT_TRAINS)) == 25
assert train_state.attributes.get(
ATTR_NEXT_TRAINS)[0]['destination_name'] == 'London Waterloo'
assert train_state.attributes.get(
ATTR_NEXT_TRAINS)[0]['estimated'] == '06:13'

View File

@@ -38,7 +38,7 @@ def test_recent_items_intent(hass):
)
assert response.speech['plain']['speech'] == \
"These are the top 5 items in your shopping list: soda, wine, beer"
"These are the top 3 items on your shopping list: soda, wine, beer"
@asyncio.coroutine

110
tests/fixtures/uk_transport_bus.json vendored Normal file
View File

@@ -0,0 +1,110 @@
{
"atcocode": "340000368SHE",
"bearing": "",
"departures": {
"32A": [{
"aimed_departure_time": "10:18",
"best_departure_estimate": "10:18",
"date": "2017-05-09",
"dir": "outbound",
"direction": "Market Place (Wantage)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/10:18/timetable.json?app_id=e99&app_key=058",
"line": "32A",
"line_name": "32A",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
},
{
"aimed_departure_time": "11:00",
"best_departure_estimate": "11:00",
"date": "2017-05-09",
"dir": "outbound",
"direction": "Stratton Way (Abingdon Town Centre)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:00/timetable.json?app_id=e99&app_key=058",
"line": "32A",
"line_name": "32A",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
},
{
"aimed_departure_time": "11:18",
"best_departure_estimate": "11:18",
"date": "2017-05-09",
"dir": "outbound",
"direction": "Market Place (Wantage)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:18/timetable.json?app_id=e99&app_key=058",
"line": "32A",
"line_name": "32A",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
}
],
"X32": [{
"aimed_departure_time": "10:09",
"best_departure_estimate": "10:09",
"date": "2017-05-09",
"dir": "inbound",
"direction": "Parkway Station (Didcot)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:09/timetable.json?app_id=e99&app_key=058",
"line": "X32",
"line_name": "X32",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
},
{
"aimed_departure_time": "10:30",
"best_departure_estimate": "10:30",
"date": "2017-05-09",
"dir": "inbound",
"direction": "Parks Road (Oxford City Centre)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:30/timetable.json?app_id=e99&app_key=058",
"line": "X32",
"line_name": "X32",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
},
{
"aimed_departure_time": "10:39",
"best_departure_estimate": "10:39",
"date": "2017-05-09",
"dir": "inbound",
"direction": "Parkway Station (Didcot)",
"expected_departure_date": null,
"expected_departure_time": null,
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:39/timetable.json?app_id=e99&app_key=058",
"line": "X32",
"line_name": "X32",
"mode": "bus",
"operator": "THTR",
"operator_name": "Thames Travel",
"source": "Traveline timetable (not a nextbuses live region)"
}
]
},
"indicator": "in",
"locality": "Harwell Campus",
"name": "Bus Station (in)",
"request_time": "2017-05-09T10:03:41+01:00",
"smscode": "oxfajwgp",
"stop_name": "Bus Station"
}

511
tests/fixtures/uk_transport_train.json vendored Normal file
View File

@@ -0,0 +1,511 @@
{
"date": "2017-07-10",
"time_of_day": "06:10",
"request_time": "2017-07-10T06:10:05+01:00",
"station_name": "Wimbledon",
"station_code": "WIM",
"departures": {
"all": [
{
"mode": "train",
"service": "24671405",
"train_uid": "W36814",
"platform": "8",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:13",
"aimed_arrival_time": null,
"aimed_pass_time": null,
"origin_name": "Wimbledon",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "STARTS HERE",
"expected_arrival_time": null,
"expected_departure_time": "06:13",
"best_arrival_estimate_mins": null,
"best_departure_estimate_mins": 2
},
{
"mode": "train",
"service": "24673205",
"train_uid": "W36613",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:14",
"aimed_arrival_time": "06:13",
"aimed_pass_time": null,
"origin_name": "Hampton Court",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "EARLY",
"expected_arrival_time": "06:13",
"expected_departure_time": "06:14",
"best_arrival_estimate_mins": 2,
"best_departure_estimate_mins": 3
},
{
"mode": "train",
"service": "24673505",
"train_uid": "W36012",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:20",
"aimed_arrival_time": "06:20",
"aimed_pass_time": null,
"origin_name": "Guildford",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:20",
"expected_departure_time": "06:20",
"best_arrival_estimate_mins": 9,
"best_departure_estimate_mins": 9
},
{
"mode": "train",
"service": "24673305",
"train_uid": "W34087",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:23",
"aimed_arrival_time": "06:23",
"aimed_pass_time": null,
"origin_name": "Dorking",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "XX",
"status": "ON TIME",
"expected_arrival_time": "06:23",
"expected_departure_time": "06:23",
"best_arrival_estimate_mins": 12,
"best_departure_estimate_mins": 12
},
{
"mode": "train",
"service": "24671505",
"train_uid": "W37471",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:32",
"aimed_arrival_time": "06:31",
"aimed_pass_time": null,
"origin_name": "London Waterloo",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:31",
"expected_departure_time": "06:32",
"best_arrival_estimate_mins": 20,
"best_departure_estimate_mins": 21
},
{
"mode": "train",
"service": "24673605",
"train_uid": "W35790",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:35",
"aimed_arrival_time": "06:35",
"aimed_pass_time": null,
"origin_name": "Woking",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:35",
"expected_departure_time": "06:35",
"best_arrival_estimate_mins": 24,
"best_departure_estimate_mins": 24
},
{
"mode": "train",
"service": "24673705",
"train_uid": "W35665",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:38",
"aimed_arrival_time": "06:38",
"aimed_pass_time": null,
"origin_name": "Epsom",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:38",
"expected_departure_time": "06:38",
"best_arrival_estimate_mins": 27,
"best_departure_estimate_mins": 27
},
{
"mode": "train",
"service": "24671405",
"train_uid": "W36816",
"platform": "8",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:43",
"aimed_arrival_time": "06:43",
"aimed_pass_time": null,
"origin_name": "London Waterloo",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:43",
"expected_departure_time": "06:43",
"best_arrival_estimate_mins": 32,
"best_departure_estimate_mins": 32
},
{
"mode": "train",
"service": "24673205",
"train_uid": "W36618",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:44",
"aimed_arrival_time": "06:43",
"aimed_pass_time": null,
"origin_name": "Hampton Court",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:43",
"expected_departure_time": "06:44",
"best_arrival_estimate_mins": 32,
"best_departure_estimate_mins": 33
},
{
"mode": "train",
"service": "24673105",
"train_uid": "W36429",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:47",
"aimed_arrival_time": "06:46",
"aimed_pass_time": null,
"origin_name": "Shepperton",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:46",
"expected_departure_time": "06:47",
"best_arrival_estimate_mins": 35,
"best_departure_estimate_mins": 36
},
{
"mode": "train",
"service": "24629204",
"train_uid": "W36916",
"platform": "6",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:47",
"aimed_arrival_time": "06:47",
"aimed_pass_time": null,
"origin_name": "Basingstoke",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "LATE",
"expected_arrival_time": "06:48",
"expected_departure_time": "06:48",
"best_arrival_estimate_mins": 37,
"best_departure_estimate_mins": 37
},
{
"mode": "train",
"service": "24673505",
"train_uid": "W36016",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:50",
"aimed_arrival_time": "06:49",
"aimed_pass_time": null,
"origin_name": "Guildford",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:49",
"expected_departure_time": "06:50",
"best_arrival_estimate_mins": 38,
"best_departure_estimate_mins": 39
},
{
"mode": "train",
"service": "24673705",
"train_uid": "W35489",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:53",
"aimed_arrival_time": "06:52",
"aimed_pass_time": null,
"origin_name": "Guildford",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "EARLY",
"expected_arrival_time": "06:52",
"expected_departure_time": "06:53",
"best_arrival_estimate_mins": 41,
"best_departure_estimate_mins": 42
},
{
"mode": "train",
"service": "24673405",
"train_uid": "W37107",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "06:58",
"aimed_arrival_time": "06:57",
"aimed_pass_time": null,
"origin_name": "Chessington South",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "06:57",
"expected_departure_time": "06:58",
"best_arrival_estimate_mins": 46,
"best_departure_estimate_mins": 47
},
{
"mode": "train",
"service": "24671505",
"train_uid": "W37473",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:02",
"aimed_arrival_time": "07:01",
"aimed_pass_time": null,
"origin_name": "London Waterloo",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "EARLY",
"expected_arrival_time": "07:01",
"expected_departure_time": "07:02",
"best_arrival_estimate_mins": 50,
"best_departure_estimate_mins": 51
},
{
"mode": "train",
"service": "24673605",
"train_uid": "W35795",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:05",
"aimed_arrival_time": "07:04",
"aimed_pass_time": null,
"origin_name": "Woking",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:04",
"expected_departure_time": "07:05",
"best_arrival_estimate_mins": 53,
"best_departure_estimate_mins": 54
},
{
"mode": "train",
"service": "24673305",
"train_uid": "W34090",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:08",
"aimed_arrival_time": "07:07",
"aimed_pass_time": null,
"origin_name": "Dorking",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "XX",
"status": "ON TIME",
"expected_arrival_time": "07:07",
"expected_departure_time": "07:08",
"best_arrival_estimate_mins": 56,
"best_departure_estimate_mins": 57
},
{
"mode": "train",
"service": "24673205",
"train_uid": "W36623",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:13",
"aimed_arrival_time": "07:12",
"aimed_pass_time": null,
"origin_name": "Hampton Court",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:12",
"expected_departure_time": "07:13",
"best_arrival_estimate_mins": 61,
"best_departure_estimate_mins": 62
},
{
"mode": "train",
"service": "24671405",
"train_uid": "W36819",
"platform": "8",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:13",
"aimed_arrival_time": "07:13",
"aimed_pass_time": null,
"origin_name": "London Waterloo",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:13",
"expected_departure_time": "07:13",
"best_arrival_estimate_mins": 62,
"best_departure_estimate_mins": 62
},
{
"mode": "train",
"service": "24673105",
"train_uid": "W36434",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:16",
"aimed_arrival_time": "07:15",
"aimed_pass_time": null,
"origin_name": "Shepperton",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:15",
"expected_departure_time": "07:16",
"best_arrival_estimate_mins": 64,
"best_departure_estimate_mins": 65
},
{
"mode": "train",
"service": "24673505",
"train_uid": "W36019",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:19",
"aimed_arrival_time": "07:18",
"aimed_pass_time": null,
"origin_name": "Guildford",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:18",
"expected_departure_time": "07:19",
"best_arrival_estimate_mins": 67,
"best_departure_estimate_mins": 68
},
{
"mode": "train",
"service": "24673705",
"train_uid": "W35494",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:22",
"aimed_arrival_time": "07:21",
"aimed_pass_time": null,
"origin_name": "Guildford",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:21",
"expected_departure_time": "07:22",
"best_arrival_estimate_mins": 70,
"best_departure_estimate_mins": 71
},
{
"mode": "train",
"service": "24673205",
"train_uid": "W36810",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:25",
"aimed_arrival_time": "07:24",
"aimed_pass_time": null,
"origin_name": "Esher",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:24",
"expected_departure_time": "07:25",
"best_arrival_estimate_mins": 73,
"best_departure_estimate_mins": 74
},
{
"mode": "train",
"service": "24673405",
"train_uid": "W37112",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:28",
"aimed_arrival_time": "07:27",
"aimed_pass_time": null,
"origin_name": "Chessington South",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:27",
"expected_departure_time": "07:28",
"best_arrival_estimate_mins": 76,
"best_departure_estimate_mins": 77
},
{
"mode": "train",
"service": "24671505",
"train_uid": "W37476",
"platform": "5",
"operator": "SW",
"operator_name": "South West Trains",
"aimed_departure_time": "07:32",
"aimed_arrival_time": "07:31",
"aimed_pass_time": null,
"origin_name": "London Waterloo",
"source": "Network Rail",
"destination_name": "London Waterloo",
"category": "OO",
"status": "ON TIME",
"expected_arrival_time": "07:31",
"expected_departure_time": "07:32",
"best_arrival_estimate_mins": 80,
"best_departure_estimate_mins": 81
}
]
}
}

View File

@@ -2,6 +2,7 @@
import io
import os
import unittest
import logging
from unittest.mock import patch
from homeassistant.exceptions import HomeAssistantError
@@ -59,6 +60,13 @@ class TestYaml(unittest.TestCase):
assert doc['password'] == "secret_password"
del os.environ["PASSWORD"]
def test_environment_variable_default(self):
"""Test config file with default value for environment variable."""
conf = "password: !env_var PASSWORD secret_password"
with io.StringIO(conf) as file:
doc = yaml.yaml.safe_load(file)
assert doc['password'] == "secret_password"
def test_invalid_enviroment_variable(self):
"""Test config file with no enviroment variable sat."""
conf = "password: !env_var PASSWORD"
@@ -372,6 +380,16 @@ class TestSecrets(unittest.TestCase):
_yaml = load_yaml(self._yaml_path, yaml_str)
self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml)
@patch.object(yaml, 'credstash')
def test_secrets_credstash(self, mock_credstash):
"""Test credstash fallback & get_password."""
mock_credstash.getSecret.return_value = 'yeah'
yaml_str = 'http:\n api_password: !secret http_pw_credstash'
_yaml = load_yaml(self._yaml_path, yaml_str)
log = logging.getLogger()
log.error(_yaml['http'])
self.assertEqual({'api_password': 'yeah'}, _yaml['http'])
def test_secrets_logger_removed(self):
"""Ensure logger: debug was removed."""
with self.assertRaises(yaml.HomeAssistantError):