mirror of
https://github.com/home-assistant/core.git
synced 2025-08-06 22:25:13 +02:00
Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into dev
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
235
homeassistant/components/alarm_control_panel/manual_mqtt.py
Normal file
235
homeassistant/components/alarm_control_panel/manual_mqtt.py
Normal 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)
|
@@ -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__)
|
||||
|
96
homeassistant/components/binary_sensor/velbus.py
Normal file
96
homeassistant/components/binary_sensor/velbus.py
Normal 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
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
160
homeassistant/components/cover/velbus.py
Normal file
160
homeassistant/components/cover/velbus.py
Normal 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)
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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):
|
||||
|
@@ -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.
|
||||
|
@@ -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."""
|
||||
|
@@ -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:
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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 []
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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__)
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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'
|
||||
|
||||
|
187
homeassistant/components/fan/velbus.py
Normal file
187
homeassistant/components/fan/velbus.py
Normal 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
|
@@ -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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1 @@
|
||||
<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>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -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))
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
116
homeassistant/components/light/tplink.py
Normal file
116
homeassistant/components/light/tplink.py
Normal 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
|
104
homeassistant/components/light/velbus.py
Normal file
104
homeassistant/components/light/velbus.py
Normal 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)
|
@@ -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(
|
||||
|
@@ -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__)
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -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')
|
||||
|
@@ -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)
|
||||
|
||||
|
275
homeassistant/components/sensor/uk_transport.py
Normal file
275
homeassistant/components/sensor/uk_transport.py
Normal 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
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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):
|
||||
|
111
homeassistant/components/switch/velbus.py
Normal file
111
homeassistant/components/switch/velbus.py
Normal 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)
|
43
homeassistant/components/velbus.py
Normal file
43
homeassistant/components/velbus.py
Normal 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
|
@@ -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()
|
||||
|
@@ -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 = {}
|
||||
|
71
homeassistant/scripts/credstash.py
Normal file
71
homeassistant/scripts/credstash.py
Normal 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))
|
@@ -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)
|
||||
|
||||
|
2
pylintrc
2
pylintrc
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
|
559
tests/components/alarm_control_panel/test_manual_mqtt.py
Normal file
559
tests/components/alarm_control_panel/test_manual_mqtt.py
Normal 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])
|
93
tests/components/sensor/test_uk_transport.py
Normal file
93
tests/components/sensor/test_uk_transport.py
Normal 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'
|
@@ -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
110
tests/fixtures/uk_transport_bus.json
vendored
Normal 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
511
tests/fixtures/uk_transport_train.json
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -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):
|
||||
|
Reference in New Issue
Block a user