mirror of
https://github.com/home-assistant/core.git
synced 2025-08-07 14:45:09 +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/twilio.py
|
||||||
homeassistant/components/notify/twilio_sms.py
|
homeassistant/components/notify/twilio_sms.py
|
||||||
homeassistant/components/notify/twilio_call.py
|
homeassistant/components/notify/twilio_call.py
|
||||||
|
|
||||||
|
homeassistant/components/velbus.py
|
||||||
|
homeassistant/components/*/velbus.py
|
||||||
|
|
||||||
homeassistant/components/velux.py
|
homeassistant/components/velux.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/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/concord232.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/nx584.py
|
||||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||||
@@ -305,6 +309,7 @@ omit =
|
|||||||
homeassistant/components/light/piglow.py
|
homeassistant/components/light/piglow.py
|
||||||
homeassistant/components/light/sensehat.py
|
homeassistant/components/light/sensehat.py
|
||||||
homeassistant/components/light/tikteck.py
|
homeassistant/components/light/tikteck.py
|
||||||
|
homeassistant/components/light/tplink.py
|
||||||
homeassistant/components/light/tradfri.py
|
homeassistant/components/light/tradfri.py
|
||||||
homeassistant/components/light/x10.py
|
homeassistant/components/light/x10.py
|
||||||
homeassistant/components/light/yeelight.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.
|
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
|
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
: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
|
.. |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
|
: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
|
.. |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
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['amcrest==1.2.0']
|
REQUIREMENTS = ['amcrest==1.2.1']
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ['ffmpeg']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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 asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ class ONVIFCamera(Camera):
|
|||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize a ONVIF camera."""
|
"""Initialize a ONVIF camera."""
|
||||||
from onvif import ONVIFService
|
from onvif import ONVIFService
|
||||||
|
import onvif
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._name = config.get(CONF_NAME)
|
self._name = config.get(CONF_NAME)
|
||||||
@@ -63,7 +65,7 @@ class ONVIFCamera(Camera):
|
|||||||
config.get(CONF_HOST), config.get(CONF_PORT)),
|
config.get(CONF_HOST), config.get(CONF_PORT)),
|
||||||
config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD),
|
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
|
self._input = media.GetStreamUri().Uri
|
||||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||||
|
@@ -151,7 +151,7 @@ def _process(hass, text):
|
|||||||
if not entity_ids:
|
if not entity_ids:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Could not find entity id %s from text %s", name, text)
|
"Could not find entity id %s from text %s", name, text)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if command == 'on':
|
if command == 'on':
|
||||||
yield from hass.services.async_call(
|
yield from hass.services.async_call(
|
||||||
@@ -169,6 +169,8 @@ def _process(hass, text):
|
|||||||
_LOGGER.error('Got unsupported command %s from text %s',
|
_LOGGER.error('Got unsupported command %s from text %s',
|
||||||
command, text)
|
command, text)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ConversationProcessView(http.HomeAssistantView):
|
class ConversationProcessView(http.HomeAssistantView):
|
||||||
"""View to retrieve shopping list content."""
|
"""View to retrieve shopping list content."""
|
||||||
@@ -194,4 +196,8 @@ class ConversationProcessView(http.HomeAssistantView):
|
|||||||
|
|
||||||
intent_result = yield from _process(hass, text)
|
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)
|
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 logging
|
||||||
import re
|
import re
|
||||||
import telnetlib
|
import telnetlib
|
||||||
import threading
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import timedelta
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@@ -17,9 +15,6 @@ import homeassistant.util.dt as dt_util
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -54,7 +49,6 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
data = self.get_actiontec_data()
|
data = self.get_actiontec_data()
|
||||||
self.success_init = data is not None
|
self.success_init = data is not None
|
||||||
@@ -74,7 +68,6 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
return client.ip
|
return client.ip
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the router is up to date.
|
"""Ensure the information from the router is up to date.
|
||||||
|
|
||||||
@@ -84,16 +77,15 @@ class ActiontecDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
now = dt_util.now()
|
||||||
now = dt_util.now()
|
actiontec_data = self.get_actiontec_data()
|
||||||
actiontec_data = self.get_actiontec_data()
|
if not actiontec_data:
|
||||||
if not actiontec_data:
|
return False
|
||||||
return False
|
self.last_results = [Device(data['mac'], name, now)
|
||||||
self.last_results = [Device(data['mac'], name, now)
|
for name, data in actiontec_data.items()
|
||||||
for name, data in actiontec_data.items()
|
if data['timevalid'] > -60]
|
||||||
if data['timevalid'] > -60]
|
_LOGGER.info("Scan successful")
|
||||||
_LOGGER.info("Scan successful")
|
return True
|
||||||
return True
|
|
||||||
|
|
||||||
def get_actiontec_data(self):
|
def get_actiontec_data(self):
|
||||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||||
|
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.aruba/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -15,14 +13,11 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pexpect==4.0.1']
|
REQUIREMENTS = ['pexpect==4.0.1']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|
||||||
|
|
||||||
_DEVICES_REGEX = re.compile(
|
_DEVICES_REGEX = re.compile(
|
||||||
r'(?P<name>([^\s]+))\s+' +
|
r'(?P<name>([^\s]+))\s+' +
|
||||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\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.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
# Test the router is accessible.
|
# Test the router is accessible.
|
||||||
@@ -74,7 +67,6 @@ class ArubaDeviceScanner(DeviceScanner):
|
|||||||
return client['name']
|
return client['name']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the Aruba Access Point is up to date.
|
"""Ensure the information from the Aruba Access Point is up to date.
|
||||||
|
|
||||||
@@ -83,13 +75,12 @@ class ArubaDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
data = self.get_aruba_data()
|
||||||
data = self.get_aruba_data()
|
if not data:
|
||||||
if not data:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
self.last_results = data.values()
|
self.last_results = data.values()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_aruba_data(self):
|
def get_aruba_data(self):
|
||||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||||
|
@@ -8,9 +8,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import telnetlib
|
import telnetlib
|
||||||
import threading
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -18,7 +16,6 @@ from homeassistant.components.device_tracker import (
|
|||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||||
from homeassistant.util import Throttle
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pexpect==4.0.1']
|
REQUIREMENTS = ['pexpect==4.0.1']
|
||||||
@@ -32,8 +29,6 @@ CONF_SSH_KEY = 'ssh_key'
|
|||||||
|
|
||||||
DEFAULT_SSH_PORT = 22
|
DEFAULT_SSH_PORT = 22
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
SECRET_GROUP = 'Password or SSH Key'
|
SECRET_GROUP = 'Password or SSH Key'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(
|
PLATFORM_SCHEMA = vol.All(
|
||||||
@@ -123,8 +118,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||||||
self.password,
|
self.password,
|
||||||
self.mode == "ap")
|
self.mode == "ap")
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
# Test the router is accessible.
|
# Test the router is accessible.
|
||||||
@@ -145,7 +138,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||||||
return client['host']
|
return client['host']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the ASUSWRT router is up to date.
|
"""Ensure the information from the ASUSWRT router is up to date.
|
||||||
|
|
||||||
@@ -154,19 +146,18 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info('Checking Devices')
|
||||||
_LOGGER.info('Checking Devices')
|
data = self.get_asuswrt_data()
|
||||||
data = self.get_asuswrt_data()
|
if not data:
|
||||||
if not data:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
active_clients = [client for client in data.values() if
|
active_clients = [client for client in data.values() if
|
||||||
client['status'] == 'REACHABLE' or
|
client['status'] == 'REACHABLE' or
|
||||||
client['status'] == 'DELAY' or
|
client['status'] == 'DELAY' or
|
||||||
client['status'] == 'STALE' or
|
client['status'] == 'STALE' or
|
||||||
client['status'] == 'IN_ASSOCLIST']
|
client['status'] == 'IN_ASSOCLIST']
|
||||||
self.last_results = active_clients
|
self.last_results = active_clients
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_asuswrt_data(self):
|
def get_asuswrt_data(self):
|
||||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
"""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 logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import json
|
import json
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@@ -19,13 +17,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
_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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string
|
vol.Required(CONF_HOST): cv.string
|
||||||
})
|
})
|
||||||
@@ -46,11 +41,7 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
|
|||||||
"""Initialise the scanner."""
|
"""Initialise the scanner."""
|
||||||
_LOGGER.info("Initialising BT Home Hub 5")
|
_LOGGER.info("Initialising BT Home Hub 5")
|
||||||
self.host = config.get(CONF_HOST, '192.168.1.254')
|
self.host = config.get(CONF_HOST, '192.168.1.254')
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
|
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
|
||||||
|
|
||||||
# Test the router is accessible
|
# Test the router is accessible
|
||||||
@@ -65,17 +56,15 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""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 not initialised and not already scanned and not found.
|
if device not in self.last_results:
|
||||||
if device not in self.last_results:
|
self._update_info()
|
||||||
self._update_info()
|
|
||||||
|
|
||||||
if not self.last_results:
|
if not self.last_results:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.last_results.get(device)
|
return self.last_results.get(device)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the BT Home Hub 5 is up to date.
|
"""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:
|
if not self.success_init:
|
||||||
return False
|
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:
|
if not data:
|
||||||
_LOGGER.warning("Error scanning devices")
|
_LOGGER.warning("Error scanning devices")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.last_results = data
|
self.last_results = data
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _get_homehub_data(url):
|
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/
|
https://home-assistant.io/components/device_tracker.cisco_ios/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -14,9 +13,6 @@ from homeassistant.components.device_tracker import (
|
|||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||||
CONF_PORT
|
CONF_PORT
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -65,7 +61,6 @@ class CiscoDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
return self.last_results
|
return self.last_results
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""
|
"""
|
||||||
Ensure the information from the Cisco router is up to date.
|
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 logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -50,8 +45,6 @@ class DdWrtDeviceScanner(DeviceScanner):
|
|||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
self.mac2name = {}
|
self.mac2name = {}
|
||||||
|
|
||||||
@@ -69,68 +62,65 @@ class DdWrtDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""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 not initialised and not already scanned and not found.
|
if device not in self.mac2name:
|
||||||
if device not in self.mac2name:
|
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
data = self.get_ddwrt_data(url)
|
||||||
data = self.get_ddwrt_data(url)
|
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dhcp_leases = data.get('dhcp_leases', None)
|
dhcp_leases = data.get('dhcp_leases', None)
|
||||||
|
|
||||||
if not dhcp_leases:
|
if not dhcp_leases:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Remove leading and trailing quotes and spaces
|
# Remove leading and trailing quotes and spaces
|
||||||
cleaned_str = dhcp_leases.replace(
|
cleaned_str = dhcp_leases.replace(
|
||||||
"\"", "").replace("\'", "").replace(" ", "")
|
"\"", "").replace("\'", "").replace(" ", "")
|
||||||
elements = cleaned_str.split(',')
|
elements = cleaned_str.split(',')
|
||||||
num_clients = int(len(elements) / 5)
|
num_clients = int(len(elements) / 5)
|
||||||
self.mac2name = {}
|
self.mac2name = {}
|
||||||
for idx in range(0, num_clients):
|
for idx in range(0, num_clients):
|
||||||
# The data is a single array
|
# The data is a single array
|
||||||
# every 5 elements represents one host, the MAC
|
# every 5 elements represents one host, the MAC
|
||||||
# is the third element and the name is the first.
|
# is the third element and the name is the first.
|
||||||
mac_index = (idx * 5) + 2
|
mac_index = (idx * 5) + 2
|
||||||
if mac_index < len(elements):
|
if mac_index < len(elements):
|
||||||
mac = elements[mac_index]
|
mac = elements[mac_index]
|
||||||
self.mac2name[mac] = elements[idx * 5]
|
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):
|
def _update_info(self):
|
||||||
"""Ensure the information from the DD-WRT router is up to date.
|
"""Ensure the information from the DD-WRT router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
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)
|
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||||
data = self.get_ddwrt_data(url)
|
data = self.get_ddwrt_data(url)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
active_clients = data.get('active_wireless', None)
|
active_clients = data.get('active_wireless', None)
|
||||||
if not active_clients:
|
if not active_clients:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# The DD-WRT UI uses its own data format and then
|
# The DD-WRT UI uses its own data format and then
|
||||||
# regex's out values so this is done here too
|
# regex's out values so this is done here too
|
||||||
# Remove leading and trailing single quotes.
|
# Remove leading and trailing single quotes.
|
||||||
clean_str = active_clients.strip().strip("'")
|
clean_str = active_clients.strip().strip("'")
|
||||||
elements = clean_str.split("','")
|
elements = clean_str.split("','")
|
||||||
|
|
||||||
self.last_results.extend(item for item in elements
|
self.last_results.extend(item for item in elements
|
||||||
if _MAC_REGEX.match(item))
|
if _MAC_REGEX.match(item))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_ddwrt_data(self, url):
|
def get_ddwrt_data(self, url):
|
||||||
"""Retrieve data from DD-WRT and return parsed result."""
|
"""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/
|
https://home-assistant.io/components/device_tracker.fritz/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -13,12 +12,9 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers.
|
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 None
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Retrieve latest information from the FRITZ!Box."""
|
"""Retrieve latest information from the FRITZ!Box."""
|
||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
|
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.linksys_ap/
|
|||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
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.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
INTERFACES = 2
|
INTERFACES = 2
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
@@ -51,8 +47,6 @@ class LinksysAPDeviceScanner(object):
|
|||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
self.verify_ssl = config[CONF_VERIFY_SSL]
|
self.verify_ssl = config[CONF_VERIFY_SSL]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
# Check if the access point is accessible
|
# Check if the access point is accessible
|
||||||
@@ -76,24 +70,22 @@ class LinksysAPDeviceScanner(object):
|
|||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Check for connected devices."""
|
"""Check for connected devices."""
|
||||||
from bs4 import BeautifulSoup as BS
|
from bs4 import BeautifulSoup as BS
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info("Checking Linksys AP")
|
||||||
_LOGGER.info("Checking Linksys AP")
|
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
for interface in range(INTERFACES):
|
for interface in range(INTERFACES):
|
||||||
request = self._make_request(interface)
|
request = self._make_request(interface)
|
||||||
self.last_results.extend(
|
self.last_results.extend(
|
||||||
[x.find_all('td')[1].text
|
[x.find_all('td')[1].text
|
||||||
for x in BS(request.content, "html.parser")
|
for x in BS(request.content, "html.parser")
|
||||||
.find_all(class_='section-row')]
|
.find_all(class_='section-row')]
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _make_request(self, unit=0):
|
def _make_request(self, unit=0):
|
||||||
# No, the '&&' is not a typo - this is expected by the web interface.
|
# No, the '&&' is not a typo - this is expected by the web interface.
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
"""Support for Linksys Smart Wifi routers."""
|
"""Support for Linksys Smart Wifi routers."""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -10,9 +8,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -36,8 +32,6 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
# Check if the access point is accessible
|
# Check if the access point is accessible
|
||||||
@@ -55,48 +49,46 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
|||||||
"""Return the name (if known) of the device."""
|
"""Return the name (if known) of the device."""
|
||||||
return self.last_results.get(mac)
|
return self.last_results.get(mac)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Check for connected devices."""
|
"""Check for connected devices."""
|
||||||
with self.lock:
|
_LOGGER.info("Checking Linksys Smart Wifi")
|
||||||
_LOGGER.info("Checking Linksys Smart Wifi")
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
response = self._make_request()
|
response = self._make_request()
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Got HTTP status code %d when getting device list",
|
"Got HTTP status code %d when getting device list",
|
||||||
response.status_code)
|
response.status_code)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
result = data["responses"][0]
|
result = data["responses"][0]
|
||||||
devices = result["output"]["devices"]
|
devices = result["output"]["devices"]
|
||||||
for device in devices:
|
for device in devices:
|
||||||
macs = device["knownMACAddresses"]
|
macs = device["knownMACAddresses"]
|
||||||
if not macs:
|
if not macs:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Skipping device without known MAC address")
|
"Skipping device without known MAC address")
|
||||||
continue
|
continue
|
||||||
mac = macs[-1]
|
mac = macs[-1]
|
||||||
connections = device["connections"]
|
connections = device["connections"]
|
||||||
if not connections:
|
if not connections:
|
||||||
_LOGGER.debug("Device %s is not connected", mac)
|
_LOGGER.debug("Device %s is not connected", mac)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = None
|
name = None
|
||||||
for prop in device["properties"]:
|
for prop in device["properties"]:
|
||||||
if prop["name"] == "userDeviceName":
|
if prop["name"] == "userDeviceName":
|
||||||
name = prop["value"]
|
name = prop["value"]
|
||||||
if not name:
|
if not name:
|
||||||
name = device.get("friendlyName", device["deviceID"])
|
name = device.get("friendlyName", device["deviceID"])
|
||||||
|
|
||||||
_LOGGER.debug("Device %s is connected", mac)
|
_LOGGER.debug("Device %s is connected", mac)
|
||||||
self.last_results[mac] = name
|
self.last_results[mac] = name
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
_LOGGER.exception("Router returned unexpected response")
|
_LOGGER.exception("Router returned unexpected response")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _make_request(self):
|
def _make_request(self):
|
||||||
# Weirdly enough, this doesn't seem to require authentication
|
# Weirdly enough, this doesn't seem to require authentication
|
||||||
|
@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.luci/
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -18,9 +16,6 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -55,12 +50,8 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.refresh_token()
|
self.refresh_token()
|
||||||
|
|
||||||
self.mac2name = None
|
self.mac2name = None
|
||||||
self.success_init = self.token is not None
|
self.success_init = self.token is not None
|
||||||
|
|
||||||
@@ -75,24 +66,22 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""Return the name of the given device or None if we don't know."""
|
||||||
with self.lock:
|
if self.mac2name is None:
|
||||||
if self.mac2name is None:
|
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
||||||
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
params={'auth': self.token})
|
||||||
params={'auth': self.token})
|
if result:
|
||||||
if result:
|
hosts = [x for x in result.values()
|
||||||
hosts = [x for x in result.values()
|
if x['.type'] == 'host' and
|
||||||
if x['.type'] == 'host' and
|
'mac' in x and 'name' in x]
|
||||||
'mac' in x and 'name' in x]
|
mac2name_list = [
|
||||||
mac2name_list = [
|
(x['mac'].upper(), x['name']) for x in hosts]
|
||||||
(x['mac'].upper(), x['name']) for x in hosts]
|
self.mac2name = dict(mac2name_list)
|
||||||
self.mac2name = dict(mac2name_list)
|
else:
|
||||||
else:
|
# Error, handled in the _req_json_rpc
|
||||||
# Error, handled in the _req_json_rpc
|
return
|
||||||
return
|
return self.mac2name.get(device.upper(), None)
|
||||||
return self.mac2name.get(device.upper(), None)
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the Luci router is up to date.
|
"""Ensure the information from the Luci router is up to date.
|
||||||
|
|
||||||
@@ -101,31 +90,30 @@ class LuciDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info("Checking ARP")
|
||||||
_LOGGER.info("Checking ARP")
|
|
||||||
|
|
||||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
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
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = _req_json_rpc(url, 'net.arptable',
|
||||||
|
params={'auth': self.token})
|
||||||
|
except InvalidLuciTokenError:
|
||||||
|
_LOGGER.info("Refreshing token")
|
||||||
|
self.refresh_token()
|
||||||
return False
|
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):
|
def _req_json_rpc(url, method, *args, **kwargs):
|
||||||
"""Perform one JSON RPC operation."""
|
"""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/
|
https://home-assistant.io/components/device_tracker.mikrotik/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import (CONF_HOST,
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD,
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||||
CONF_USERNAME,
|
|
||||||
CONF_PORT)
|
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
REQUIREMENTS = ['librouteros==1.0.2']
|
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'
|
MTK_DEFAULT_API_PORT = '8728'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -54,12 +46,9 @@ class MikrotikScanner(DeviceScanner):
|
|||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.success_init = False
|
self.success_init = False
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
self.wireless_exist = None
|
self.wireless_exist = None
|
||||||
self.success_init = self.connect_to_device()
|
self.success_init = self.connect_to_device()
|
||||||
|
|
||||||
@@ -118,51 +107,48 @@ class MikrotikScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, mac):
|
def get_device_name(self, mac):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""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):
|
def _update_info(self):
|
||||||
"""Retrieve latest information from the Mikrotik box."""
|
"""Retrieve latest information from the Mikrotik box."""
|
||||||
with self.lock:
|
if self.wireless_exist:
|
||||||
if self.wireless_exist:
|
devices_tracker = 'wireless'
|
||||||
devices_tracker = 'wireless'
|
else:
|
||||||
else:
|
devices_tracker = 'ip'
|
||||||
devices_tracker = 'ip'
|
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Loading %s devices from Mikrotik (%s) ...",
|
"Loading %s devices from Mikrotik (%s) ...",
|
||||||
devices_tracker,
|
devices_tracker,
|
||||||
self.host
|
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 device_names is None and devices is None:
|
||||||
if self.wireless_exist:
|
return False
|
||||||
devices = self.client(
|
|
||||||
cmd='/interface/wireless/registration-table/getall'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
devices = device_names
|
|
||||||
|
|
||||||
if device_names is None and devices is None:
|
mac_names = {device.get('mac-address'): device.get('host-name')
|
||||||
return False
|
for device in device_names
|
||||||
|
if device.get('mac-address')}
|
||||||
|
|
||||||
mac_names = {device.get('mac-address'): device.get('host-name')
|
if self.wireless_exist:
|
||||||
for device in device_names
|
self.last_results = {
|
||||||
if device.get('mac-address')}
|
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:
|
return True
|
||||||
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
|
|
||||||
|
@@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/device_tracker.netgear/
|
https://home-assistant.io/components/device_tracker.netgear/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -15,14 +13,11 @@ from homeassistant.components.device_tracker import (
|
|||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
REQUIREMENTS = ['pynetgear==0.3.3']
|
REQUIREMENTS = ['pynetgear==0.3.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
DEFAULT_HOST = 'routerlogin.net'
|
DEFAULT_HOST = 'routerlogin.net'
|
||||||
DEFAULT_USER = 'admin'
|
DEFAULT_USER = 'admin'
|
||||||
DEFAULT_PORT = 5000
|
DEFAULT_PORT = 5000
|
||||||
@@ -56,8 +51,6 @@ class NetgearDeviceScanner(DeviceScanner):
|
|||||||
import pynetgear
|
import pynetgear
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self._api = pynetgear.Netgear(password, host, username, port)
|
self._api = pynetgear.Netgear(password, host, username, port)
|
||||||
|
|
||||||
_LOGGER.info("Logging in")
|
_LOGGER.info("Logging in")
|
||||||
@@ -85,7 +78,6 @@ class NetgearDeviceScanner(DeviceScanner):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Retrieve latest information from the Netgear router.
|
"""Retrieve latest information from the Netgear router.
|
||||||
|
|
||||||
@@ -94,12 +86,11 @@ class NetgearDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return
|
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:
|
if results is None:
|
||||||
_LOGGER.warning("Error scanning devices")
|
_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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.nmap_tracker/
|
https://home-assistant.io/components/device_tracker.nmap_tracker/
|
||||||
"""
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ import homeassistant.util.dt as dt_util
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOSTS
|
from homeassistant.const import CONF_HOSTS
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||||
|
|
||||||
@@ -29,8 +28,6 @@ CONF_HOME_INTERVAL = 'home_interval'
|
|||||||
CONF_OPTIONS = 'scan_options'
|
CONF_OPTIONS = 'scan_options'
|
||||||
DEFAULT_OPTIONS = '-F --host-timeout 5s'
|
DEFAULT_OPTIONS = '-F --host-timeout 5s'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||||
@@ -97,7 +94,6 @@ class NmapDeviceScanner(DeviceScanner):
|
|||||||
return filter_named[0]
|
return filter_named[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Scan the network for devices.
|
"""Scan the network for devices.
|
||||||
|
|
||||||
|
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.sky_hub/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -16,13 +14,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
_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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string
|
vol.Required(CONF_HOST): cv.string
|
||||||
})
|
})
|
||||||
@@ -43,11 +38,7 @@ class SkyHubDeviceScanner(DeviceScanner):
|
|||||||
"""Initialise the scanner."""
|
"""Initialise the scanner."""
|
||||||
_LOGGER.info("Initialising Sky Hub")
|
_LOGGER.info("Initialising Sky Hub")
|
||||||
self.host = config.get(CONF_HOST, '192.168.1.254')
|
self.host = config.get(CONF_HOST, '192.168.1.254')
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.url = 'http://{}/'.format(self.host)
|
self.url = 'http://{}/'.format(self.host)
|
||||||
|
|
||||||
# Test the router is accessible
|
# Test the router is accessible
|
||||||
@@ -62,17 +53,15 @@ class SkyHubDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""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 not initialised and not already scanned and not found.
|
if device not in self.last_results:
|
||||||
if device not in self.last_results:
|
self._update_info()
|
||||||
self._update_info()
|
|
||||||
|
|
||||||
if not self.last_results:
|
if not self.last_results:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.last_results.get(device)
|
return self.last_results.get(device)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the Sky Hub is up to date.
|
"""Ensure the information from the Sky Hub is up to date.
|
||||||
|
|
||||||
@@ -81,18 +70,17 @@ class SkyHubDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
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:
|
if not data:
|
||||||
_LOGGER.warning('Error scanning devices')
|
_LOGGER.warning('Error scanning devices')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.last_results = data
|
self.last_results = data
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _get_skyhub_data(url):
|
def _get_skyhub_data(url):
|
||||||
|
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.snmp/
|
|||||||
"""
|
"""
|
||||||
import binascii
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -15,7 +13,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,8 +25,6 @@ CONF_BASEOID = 'baseoid'
|
|||||||
|
|
||||||
DEFAULT_COMMUNITY = 'public'
|
DEFAULT_COMMUNITY = 'public'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||||
@@ -68,9 +63,6 @@ class SnmpScanner(DeviceScanner):
|
|||||||
privProtocol=cfg.usmAesCfb128Protocol
|
privProtocol=cfg.usmAesCfb128Protocol
|
||||||
)
|
)
|
||||||
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
# Test the router is accessible
|
# Test the router is accessible
|
||||||
@@ -90,7 +82,6 @@ class SnmpScanner(DeviceScanner):
|
|||||||
# We have no names
|
# We have no names
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the device is up to date.
|
"""Ensure the information from the device is up to date.
|
||||||
|
|
||||||
@@ -99,13 +90,12 @@ class SnmpScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
data = self.get_snmp_data()
|
||||||
data = self.get_snmp_data()
|
if not data:
|
||||||
if not data:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
self.last_results = data
|
self.last_results = data
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_snmp_data(self):
|
def get_snmp_data(self):
|
||||||
"""Fetch MAC addresses from access point via SNMP."""
|
"""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/
|
https://home-assistant.io/components/device_tracker.swisscom/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -15,9 +13,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -41,9 +36,6 @@ class SwisscomDeviceScanner(DeviceScanner):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
# Test the router is accessible.
|
# Test the router is accessible.
|
||||||
@@ -64,7 +56,6 @@ class SwisscomDeviceScanner(DeviceScanner):
|
|||||||
return client['host']
|
return client['host']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the Swisscom router is up to date.
|
"""Ensure the information from the Swisscom router is up to date.
|
||||||
|
|
||||||
@@ -73,16 +64,15 @@ class SwisscomDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info("Loading data from Swisscom Internet Box")
|
||||||
_LOGGER.info("Loading data from Swisscom Internet Box")
|
data = self.get_swisscom_data()
|
||||||
data = self.get_swisscom_data()
|
if not data:
|
||||||
if not data:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
active_clients = [client for client in data.values() if
|
active_clients = [client for client in data.values() if
|
||||||
client['status']]
|
client['status']]
|
||||||
self.last_results = active_clients
|
self.last_results = active_clients
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_swisscom_data(self):
|
def get_swisscom_data(self):
|
||||||
"""Retrieve data from Swisscom and return parsed result."""
|
"""Retrieve data from Swisscom and return parsed result."""
|
||||||
|
@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.thomson/
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import telnetlib
|
import telnetlib
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -54,9 +49,6 @@ class ThomsonDeviceScanner(DeviceScanner):
|
|||||||
self.host = config[CONF_HOST]
|
self.host = config[CONF_HOST]
|
||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
# Test the router is accessible.
|
# Test the router is accessible.
|
||||||
@@ -77,7 +69,6 @@ class ThomsonDeviceScanner(DeviceScanner):
|
|||||||
return client['host']
|
return client['host']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the THOMSON router is up to date.
|
"""Ensure the information from the THOMSON router is up to date.
|
||||||
|
|
||||||
@@ -86,17 +77,16 @@ class ThomsonDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info("Checking ARP")
|
||||||
_LOGGER.info("Checking ARP")
|
data = self.get_thomson_data()
|
||||||
data = self.get_thomson_data()
|
if not data:
|
||||||
if not data:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
# Flag C stands for CONNECTED
|
# Flag C stands for CONNECTED
|
||||||
active_clients = [client for client in data.values() if
|
active_clients = [client for client in data.values() if
|
||||||
client['status'].find('C') != -1]
|
client['status'].find('C') != -1]
|
||||||
self.last_results = active_clients
|
self.last_results = active_clients
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_thomson_data(self):
|
def get_thomson_data(self):
|
||||||
"""Retrieve data from THOMSON and return parsed result."""
|
"""Retrieve data from THOMSON and return parsed result."""
|
||||||
|
@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.tomato/
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -17,9 +15,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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'
|
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.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||||
|
|
||||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
||||||
|
|
||||||
self.success_init = self._update_tomato_info()
|
self.success_init = self._update_tomato_info()
|
||||||
@@ -76,50 +69,48 @@ class TomatoDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
return filter_named[0]
|
return filter_named[0]
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_tomato_info(self):
|
def _update_tomato_info(self):
|
||||||
"""Ensure the information from the Tomato router is up to date.
|
"""Ensure the information from the Tomato router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
self.logger.info("Scanning")
|
||||||
self.logger.info("Scanning")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.Session().send(self.req, timeout=3)
|
response = requests.Session().send(self.req, timeout=3)
|
||||||
# Calling and parsing the Tomato api here. We only need the
|
# Calling and parsing the Tomato api here. We only need the
|
||||||
# wldev and dhcpd_lease values.
|
# wldev and dhcpd_lease values.
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
|
||||||
for param, value in \
|
for param, value in \
|
||||||
self.parse_api_pattern.findall(response.text):
|
self.parse_api_pattern.findall(response.text):
|
||||||
|
|
||||||
if param == 'wldev' or param == 'dhcpd_lease':
|
if param == 'wldev' or param == 'dhcpd_lease':
|
||||||
self.last_results[param] = \
|
self.last_results[param] = \
|
||||||
json.loads(value.replace("'", '"'))
|
json.loads(value.replace("'", '"'))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif response.status_code == 401:
|
elif response.status_code == 401:
|
||||||
# Authentication error
|
# Authentication error
|
||||||
self.logger.exception((
|
self.logger.exception((
|
||||||
"Failed to authenticate, "
|
"Failed to authenticate, "
|
||||||
"please check your username and password"))
|
"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")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.ConnectionError:
|
||||||
# We get this if we could not connect to the router or
|
# We get this if we could not connect to the router or
|
||||||
# an invalid http_id was supplied.
|
# an invalid http_id was supplied.
|
||||||
self.logger.exception("Connection to the router timed out")
|
self.logger.exception("Failed to connect to the router or "
|
||||||
return False
|
"invalid http_id supplied")
|
||||||
|
return False
|
||||||
|
|
||||||
except ValueError:
|
except requests.exceptions.Timeout:
|
||||||
# If JSON decoder could not parse the response.
|
# We get this if we could not connect to the router or
|
||||||
self.logger.exception("Failed to parse response from router")
|
# an invalid http_id was supplied.
|
||||||
return False
|
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 hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
from datetime import datetime
|
||||||
from datetime import timedelta, datetime
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -18,9 +17,6 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -59,7 +55,6 @@ class TplinkDeviceScanner(DeviceScanner):
|
|||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.success_init = self._update_info()
|
self.success_init = self._update_info()
|
||||||
|
|
||||||
def scan_devices(self):
|
def scan_devices(self):
|
||||||
@@ -72,28 +67,26 @@ class TplinkDeviceScanner(DeviceScanner):
|
|||||||
"""Get firmware doesn't save the name of the wireless device."""
|
"""Get firmware doesn't save the name of the wireless device."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the TP-Link router is up to date.
|
"""Ensure the information from the TP-Link router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
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)
|
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
|
||||||
referer = 'http://{}'.format(self.host)
|
referer = 'http://{}'.format(self.host)
|
||||||
page = requests.get(
|
page = requests.get(
|
||||||
url, auth=(self.username, self.password),
|
url, auth=(self.username, self.password),
|
||||||
headers={'referer': referer}, timeout=4)
|
headers={'referer': referer}, timeout=4)
|
||||||
|
|
||||||
result = self.parse_macs.findall(page.text)
|
result = self.parse_macs.findall(page.text)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.last_results = [mac.replace("-", ":") for mac in result]
|
self.last_results = [mac.replace("-", ":") for mac in result]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Tplink2DeviceScanner(TplinkDeviceScanner):
|
class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||||
@@ -109,48 +102,46 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
|||||||
"""Get firmware doesn't save the name of the wireless device."""
|
"""Get firmware doesn't save the name of the wireless device."""
|
||||||
return self.last_results.get(device)
|
return self.last_results.get(device)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the TP-Link router is up to date.
|
"""Ensure the information from the TP-Link router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
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' \
|
url = 'http://{}/data/map_access_wireless_client_grid.json' \
|
||||||
.format(self.host)
|
.format(self.host)
|
||||||
referer = 'http://{}'.format(self.host)
|
referer = 'http://{}'.format(self.host)
|
||||||
|
|
||||||
# Router uses Authorization cookie instead of header
|
# Router uses Authorization cookie instead of header
|
||||||
# Let's create the cookie
|
# Let's create the cookie
|
||||||
username_password = '{}:{}'.format(self.username, self.password)
|
username_password = '{}:{}'.format(self.username, self.password)
|
||||||
b64_encoded_username_password = base64.b64encode(
|
b64_encoded_username_password = base64.b64encode(
|
||||||
username_password.encode('ascii')
|
username_password.encode('ascii')
|
||||||
).decode('ascii')
|
).decode('ascii')
|
||||||
cookie = 'Authorization=Basic {}' \
|
cookie = 'Authorization=Basic {}' \
|
||||||
.format(b64_encoded_username_password)
|
.format(b64_encoded_username_password)
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
url, headers={'referer': referer, 'cookie': cookie},
|
url, headers={'referer': referer, 'cookie': cookie},
|
||||||
timeout=4)
|
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
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = response.json().get('data')
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("Router didn't respond with JSON. "
|
||||||
|
"Check if credentials are correct.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.last_results = {
|
||||||
|
device['mac_addr'].replace('-', ':'): device['name']
|
||||||
|
for device in result
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Tplink3DeviceScanner(TplinkDeviceScanner):
|
class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||||
"""This class queries the Archer C9 router with version 150811 or high."""
|
"""This class queries the Archer C9 router with version 150811 or high."""
|
||||||
@@ -202,70 +193,67 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
|||||||
response.text)
|
response.text)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the TP-Link router is up to date.
|
"""Ensure the information from the TP-Link router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
if (self.stok == '') or (self.sysauth == ''):
|
||||||
if (self.stok == '') or (self.sysauth == ''):
|
self._get_auth_tokens()
|
||||||
self._get_auth_tokens()
|
|
||||||
|
|
||||||
_LOGGER.info("Loading wireless clients...")
|
_LOGGER.info("Loading wireless clients...")
|
||||||
|
|
||||||
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
|
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
|
||||||
'form=statistics').format(self.host, self.stok)
|
'form=statistics').format(self.host, self.stok)
|
||||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||||
|
|
||||||
response = requests.post(url,
|
response = requests.post(url,
|
||||||
params={'operation': 'load'},
|
params={'operation': 'load'},
|
||||||
headers={'referer': referer},
|
headers={'referer': referer},
|
||||||
cookies={'sysauth': self.sysauth},
|
cookies={'sysauth': self.sysauth},
|
||||||
timeout=5)
|
timeout=5)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json_response = response.json()
|
json_response = response.json()
|
||||||
|
|
||||||
if json_response.get('success'):
|
if json_response.get('success'):
|
||||||
result = response.json().get('data')
|
result = response.json().get('data')
|
||||||
else:
|
else:
|
||||||
if json_response.get('errorcode') == 'timeout':
|
if json_response.get('errorcode') == 'timeout':
|
||||||
_LOGGER.info("Token timed out. Relogging on next scan")
|
_LOGGER.info("Token timed out. Relogging on next scan")
|
||||||
self.stok = ''
|
self.stok = ''
|
||||||
self.sysauth = ''
|
self.sysauth = ''
|
||||||
return False
|
|
||||||
_LOGGER.error(
|
|
||||||
"An unknown error happened while fetching data")
|
|
||||||
return False
|
return False
|
||||||
except ValueError:
|
_LOGGER.error(
|
||||||
_LOGGER.error("Router didn't respond with JSON. "
|
"An unknown error happened while fetching data")
|
||||||
"Check if credentials are correct")
|
|
||||||
return False
|
return False
|
||||||
|
except ValueError:
|
||||||
if result:
|
_LOGGER.error("Router didn't respond with JSON. "
|
||||||
self.last_results = {
|
"Check if credentials are correct")
|
||||||
device['mac'].replace('-', ':'): device['mac']
|
|
||||||
for device in result
|
|
||||||
}
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.last_results = {
|
||||||
|
device['mac'].replace('-', ':'): device['mac']
|
||||||
|
for device in result
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _log_out(self):
|
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?'
|
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
|
||||||
'form=logout').format(self.host, self.stok)
|
'form=logout').format(self.host, self.stok)
|
||||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||||
|
|
||||||
requests.post(url,
|
requests.post(url,
|
||||||
params={'operation': 'write'},
|
params={'operation': 'write'},
|
||||||
headers={'referer': referer},
|
headers={'referer': referer},
|
||||||
cookies={'sysauth': self.sysauth})
|
cookies={'sysauth': self.sysauth})
|
||||||
self.stok = ''
|
self.stok = ''
|
||||||
self.sysauth = ''
|
self.sysauth = ''
|
||||||
|
|
||||||
|
|
||||||
class Tplink4DeviceScanner(TplinkDeviceScanner):
|
class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||||
@@ -318,38 +306,36 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
|||||||
_LOGGER.error("Couldn't fetch auth tokens")
|
_LOGGER.error("Couldn't fetch auth tokens")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the TP-Link router is up to date.
|
"""Ensure the information from the TP-Link router is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
if (self.credentials == '') or (self.token == ''):
|
||||||
if (self.credentials == '') or (self.token == ''):
|
self._get_auth_tokens()
|
||||||
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
|
# Check both the 2.4GHz and 5GHz client list URLs
|
||||||
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
|
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
|
||||||
url = 'http://{}/{}/userRpm/{}' \
|
url = 'http://{}/{}/userRpm/{}' \
|
||||||
.format(self.host, self.token, clients_url)
|
.format(self.host, self.token, clients_url)
|
||||||
referer = 'http://{}'.format(self.host)
|
referer = 'http://{}'.format(self.host)
|
||||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||||
|
|
||||||
page = requests.get(url, headers={
|
page = requests.get(url, headers={
|
||||||
'cookie': cookie,
|
'cookie': cookie,
|
||||||
'referer': referer
|
'referer': referer
|
||||||
})
|
})
|
||||||
mac_results.extend(self.parse_macs.findall(page.text))
|
mac_results.extend(self.parse_macs.findall(page.text))
|
||||||
|
|
||||||
if not mac_results:
|
if not mac_results:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.last_results = [mac.replace("-", ":") for mac in mac_results]
|
self.last_results = [mac.replace("-", ":") for mac in mac_results]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Tplink5DeviceScanner(TplinkDeviceScanner):
|
class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||||
@@ -365,69 +351,67 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
|
|||||||
"""Get firmware doesn't save the name of the wireless device."""
|
"""Get firmware doesn't save the name of the wireless device."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the TP-Link AP is up to date.
|
"""Ensure the information from the TP-Link AP is up to date.
|
||||||
|
|
||||||
Return boolean if scanning successful.
|
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 = {
|
header = {
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
||||||
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
|
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
|
||||||
"Accept-Encoding": "gzip, deflate",
|
"Accept-Encoding": "gzip, deflate",
|
||||||
"Content-Type": "application/x-www-form-urlencoded; "
|
"Content-Type": "application/x-www-form-urlencoded; "
|
||||||
"charset=UTF-8",
|
"charset=UTF-8",
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
"Referer": "http://" + self.host + "/",
|
"Referer": "http://" + self.host + "/",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Pragma": "no-cache",
|
"Pragma": "no-cache",
|
||||||
"Cache-Control": "no-cache"
|
"Cache-Control": "no-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
password_md5 = hashlib.md5(
|
password_md5 = hashlib.md5(
|
||||||
self.password.encode('utf')).hexdigest().upper()
|
self.password.encode('utf')).hexdigest().upper()
|
||||||
|
|
||||||
# create a session to handle cookie easier
|
# create a session to handle cookie easier
|
||||||
session = requests.session()
|
session = requests.session()
|
||||||
session.get(base_url, headers=header)
|
session.get(base_url, headers=header)
|
||||||
|
|
||||||
login_data = {"username": self.username, "password": password_md5}
|
login_data = {"username": self.username, "password": password_md5}
|
||||||
session.post(base_url, login_data, headers=header)
|
session.post(base_url, login_data, headers=header)
|
||||||
|
|
||||||
# a timestamp is required to be sent as get parameter
|
# a timestamp is required to be sent as get parameter
|
||||||
timestamp = int(datetime.now().timestamp() * 1e3)
|
timestamp = int(datetime.now().timestamp() * 1e3)
|
||||||
|
|
||||||
client_list_url = '{}/data/monitor.client.client.json'.format(
|
client_list_url = '{}/data/monitor.client.client.json'.format(
|
||||||
base_url)
|
base_url)
|
||||||
|
|
||||||
get_params = {
|
get_params = {
|
||||||
'operation': 'load',
|
'operation': 'load',
|
||||||
'_': timestamp
|
'_': 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
|
|
||||||
|
|
||||||
|
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
|
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 json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -17,12 +15,8 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
@@ -70,7 +64,6 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
self.url = 'http://{}/ubus'.format(host)
|
self.url = 'http://{}/ubus'.format(host)
|
||||||
|
|
||||||
@@ -89,33 +82,31 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
@_refresh_on_acccess_denied
|
@_refresh_on_acccess_denied
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""Return the name of the given device or None if we don't know."""
|
||||||
with self.lock:
|
if self.leasefile is None:
|
||||||
if self.leasefile is None:
|
result = _req_json_rpc(
|
||||||
result = _req_json_rpc(
|
self.url, self.session_id, 'call', 'uci', 'get',
|
||||||
self.url, self.session_id, 'call', 'uci', 'get',
|
config="dhcp", type="dnsmasq")
|
||||||
config="dhcp", type="dnsmasq")
|
if result:
|
||||||
if result:
|
values = result["values"].values()
|
||||||
values = result["values"].values()
|
self.leasefile = next(iter(values))["leasefile"]
|
||||||
self.leasefile = next(iter(values))["leasefile"]
|
else:
|
||||||
else:
|
return
|
||||||
return
|
|
||||||
|
|
||||||
if self.mac2name is None:
|
if self.mac2name is None:
|
||||||
result = _req_json_rpc(
|
result = _req_json_rpc(
|
||||||
self.url, self.session_id, 'call', 'file', 'read',
|
self.url, self.session_id, 'call', 'file', 'read',
|
||||||
path=self.leasefile)
|
path=self.leasefile)
|
||||||
if result:
|
if result:
|
||||||
self.mac2name = dict()
|
self.mac2name = dict()
|
||||||
for line in result["data"].splitlines():
|
for line in result["data"].splitlines():
|
||||||
hosts = line.split(" ")
|
hosts = line.split(" ")
|
||||||
self.mac2name[hosts[1].upper()] = hosts[3]
|
self.mac2name[hosts[1].upper()] = hosts[3]
|
||||||
else:
|
else:
|
||||||
# Error, handled in the _req_json_rpc
|
# Error, handled in the _req_json_rpc
|
||||||
return
|
return
|
||||||
|
|
||||||
return self.mac2name.get(device.upper(), None)
|
return self.mac2name.get(device.upper(), None)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
@_refresh_on_acccess_denied
|
@_refresh_on_acccess_denied
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the information from the Luci router is up to date.
|
"""Ensure the information from the Luci router is up to date.
|
||||||
@@ -125,25 +116,24 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
_LOGGER.info("Checking ARP")
|
||||||
_LOGGER.info("Checking ARP")
|
|
||||||
|
|
||||||
if not self.hostapd:
|
if not self.hostapd:
|
||||||
hostapd = _req_json_rpc(
|
hostapd = _req_json_rpc(
|
||||||
self.url, self.session_id, 'list', 'hostapd.*', '')
|
self.url, self.session_id, 'list', 'hostapd.*', '')
|
||||||
self.hostapd.extend(hostapd.keys())
|
self.hostapd.extend(hostapd.keys())
|
||||||
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
results = 0
|
results = 0
|
||||||
for hostapd in self.hostapd:
|
for hostapd in self.hostapd:
|
||||||
result = _req_json_rpc(
|
result = _req_json_rpc(
|
||||||
self.url, self.session_id, 'call', hostapd, 'get_clients')
|
self.url, self.session_id, 'call', hostapd, 'get_clients')
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
results = results + 1
|
results = results + 1
|
||||||
self.last_results.extend(result['clients'].keys())
|
self.last_results.extend(result['clients'].keys())
|
||||||
|
|
||||||
return bool(results)
|
return bool(results)
|
||||||
|
|
||||||
|
|
||||||
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||||
|
@@ -9,8 +9,7 @@ import logging
|
|||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
dispatcher_connect, dispatcher_send)
|
dispatcher_connect, dispatcher_send)
|
||||||
from homeassistant.components.volvooncall import (
|
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN
|
||||||
DATA_KEY, SIGNAL_VEHICLE_SEEN)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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/
|
https://home-assistant.io/components/device_tracker.xiaomi/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -15,12 +13,9 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_USERNAME, default='admin'): cv.string,
|
vol.Required(CONF_USERNAME, default='admin'): cv.string,
|
||||||
@@ -47,8 +42,6 @@ class XiaomiDeviceScanner(DeviceScanner):
|
|||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
self.password = config[CONF_PASSWORD]
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
self.token = _get_token(self.host, self.username, self.password)
|
self.token = _get_token(self.host, self.username, self.password)
|
||||||
|
|
||||||
@@ -62,21 +55,19 @@ class XiaomiDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
"""Return the name of the given device or None if we don't know."""
|
"""Return the name of the given device or None if we don't know."""
|
||||||
with self.lock:
|
if self.mac2name is None:
|
||||||
if self.mac2name is None:
|
result = self._retrieve_list_with_retry()
|
||||||
result = self._retrieve_list_with_retry()
|
if result:
|
||||||
if result:
|
hosts = [x for x in result
|
||||||
hosts = [x for x in result
|
if 'mac' in x and 'name' in x]
|
||||||
if 'mac' in x and 'name' in x]
|
mac2name_list = [
|
||||||
mac2name_list = [
|
(x['mac'].upper(), x['name']) for x in hosts]
|
||||||
(x['mac'].upper(), x['name']) for x in hosts]
|
self.mac2name = dict(mac2name_list)
|
||||||
self.mac2name = dict(mac2name_list)
|
else:
|
||||||
else:
|
# Error, handled in the _retrieve_list_with_retry
|
||||||
# Error, handled in the _retrieve_list_with_retry
|
return
|
||||||
return
|
return self.mac2name.get(device.upper(), None)
|
||||||
return self.mac2name.get(device.upper(), None)
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Ensure the informations from the router are up to date.
|
"""Ensure the informations from the router are up to date.
|
||||||
|
|
||||||
@@ -85,12 +76,11 @@ class XiaomiDeviceScanner(DeviceScanner):
|
|||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
result = self._retrieve_list_with_retry()
|
||||||
result = self._retrieve_list_with_retry()
|
if result:
|
||||||
if result:
|
self._store_result(result)
|
||||||
self._store_result(result)
|
return True
|
||||||
return True
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
def _retrieve_list_with_retry(self):
|
def _retrieve_list_with_retry(self):
|
||||||
"""Retrieve the device list with a retry if token is invalid.
|
"""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
|
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==1.0.1']
|
REQUIREMENTS = ['netdisco==1.1.0']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
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 = {
|
FINGERPRINTS = {
|
||||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||||
"frontend.html": "7bd9aa75b2602768e66cf7e20845d7c4",
|
"frontend.html": "c44e49b9a0d9b9e4a626b7af34ca97d0",
|
||||||
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
|
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-automation.html": "72a5c1856cece8d9246328e84185ab0b",
|
"panels/ha-panel-automation.html": "1982116c49ad26ee8d89295edc797084",
|
||||||
"panels/ha-panel-config.html": "c0e043028cfa75d6d4dc5e0de0bb6dc1",
|
"panels/ha-panel-config.html": "fafeac72f83dd6cc42218f8978f6a7af",
|
||||||
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
|
"panels/ha-panel-dev-event.html": "77784d5f0c73fcc3b29b6cc050bdf324",
|
||||||
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
|
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
|
||||||
"panels/ha-panel-dev-service.html": "ac2c50e486927dc4443e93d79f08c06e",
|
"panels/ha-panel-dev-service.html": "86a42a17f4894478b6b77bc636beafd0",
|
||||||
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
|
"panels/ha-panel-dev-state.html": "31ef6ffe3347cdda5bb0cbbc54b62cde",
|
||||||
"panels/ha-panel-dev-template.html": "82cd543177c417e5c6612e07df851e6b",
|
"panels/ha-panel-dev-template.html": "d1d76e20fe9622cddee33e67318abde8",
|
||||||
"panels/ha-panel-hassio.html": "96d563215cf7bf7b0eeaf8625bafa4ef",
|
"panels/ha-panel-hassio.html": "262d31efd9add719e0325da5cf79a096",
|
||||||
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
|
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
|
||||||
"panels/ha-panel-iframe.html": "238189f21e670b6dcfac937e5ebd7d3b",
|
"panels/ha-panel-iframe.html": "238189f21e670b6dcfac937e5ebd7d3b",
|
||||||
"panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094",
|
"panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094",
|
||||||
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
|
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
|
||||||
"panels/ha-panel-map.html": "b4923812c695dd8a69ad3da380ffe7b4",
|
"panels/ha-panel-map.html": "50501cd53eb4304e9e46eb719aa894b7",
|
||||||
"panels/ha-panel-shopping-list.html": "75602d06b41702c8093bd91c10374101",
|
"panels/ha-panel-shopping-list.html": "c04af28c6475b90cbf2cf63ba1b841d0",
|
||||||
"panels/ha-panel-zwave.html": "8c8e7844d33163f560e1f691550a8369"
|
"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
|
from sqlalchemy import and_, func
|
||||||
|
|
||||||
with session_scope(hass=hass) as session:
|
with session_scope(hass=hass) as session:
|
||||||
most_recent_state_ids = session.query(
|
if entity_ids and len(entity_ids) == 1:
|
||||||
func.max(States.state_id).label('max_state_id')
|
# Use an entirely different (and extremely fast) query if we only
|
||||||
).filter(
|
# have a single entity id
|
||||||
(States.created >= run.start) &
|
most_recent_state_ids = session.query(
|
||||||
(States.created < utc_point_in_time) &
|
States.state_id.label('max_state_id')
|
||||||
(~States.domain.in_(IGNORE_DOMAINS)))
|
).filter(
|
||||||
|
(States.created < utc_point_in_time) &
|
||||||
|
(States.entity_id.in_(entity_ids))
|
||||||
|
).order_by(
|
||||||
|
States.created.desc())
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
most_recent_state_ids = filters.apply(most_recent_state_ids,
|
most_recent_state_ids = filters.apply(most_recent_state_ids,
|
||||||
entity_ids)
|
entity_ids)
|
||||||
|
|
||||||
most_recent_state_ids = most_recent_state_ids.group_by(
|
most_recent_state_ids = most_recent_state_ids.limit(1)
|
||||||
States.entity_id).subquery()
|
|
||||||
|
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_(
|
query = session.query(States).join(most_recent_state_ids, and_(
|
||||||
States.state_id == most_recent_state_ids.c.max_state_id))
|
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:
|
if self.dev is not None:
|
||||||
_LOGGER.debug("Keyboard connected, %s", self.device_id)
|
_LOGGER.debug("Keyboard connected, %s", self.device_id)
|
||||||
else:
|
else:
|
||||||
id_folder = '/dev/input/by-id/'
|
|
||||||
device_names = [InputDevice(file_name).name
|
|
||||||
for file_name in list_devices()]
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
'Keyboard not connected, %s.\n\
|
'Keyboard not connected, %s.\n\
|
||||||
Check /dev/input/event* permissions.\
|
Check /dev/input/event* permissions.',
|
||||||
Possible device names are:\n %s.\n \
|
self.device_id
|
||||||
Possible device descriptors are %s:\n %s',
|
|
||||||
self.device_id,
|
|
||||||
device_names,
|
|
||||||
id_folder,
|
|
||||||
os.listdir(id_folder)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
threading.Thread.__init__(self)
|
||||||
self.stopped = threading.Event()
|
self.stopped = threading.Event()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@@ -33,7 +33,7 @@ import homeassistant.util.color as color_util
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
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.components import light, zha
|
||||||
from homeassistant.util.color import color_RGB_to_xy
|
from homeassistant.util.color import color_RGB_to_xy
|
||||||
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['zha']
|
DEPENDENCIES = ['zha']
|
||||||
|
|
||||||
|
DEFAULT_DURATION = 0.5
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
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
|
import bellows.zigbee.zcl.clusters as zcl_clusters
|
||||||
if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters:
|
if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters:
|
||||||
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
||||||
|
self._supported_features |= light.SUPPORT_TRANSITION
|
||||||
self._brightness = 0
|
self._brightness = 0
|
||||||
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
|
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
|
||||||
# Not sure all color lights necessarily support this directly
|
# Not sure all color lights necessarily support this directly
|
||||||
@@ -62,14 +66,15 @@ class Light(zha.Entity, light.Light):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if entity is on."""
|
"""Return true if entity is on."""
|
||||||
if self._state == 'unknown':
|
if self._state == STATE_UNKNOWN:
|
||||||
return False
|
return False
|
||||||
return bool(self._state)
|
return bool(self._state)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_on(self, **kwargs):
|
def async_turn_on(self, **kwargs):
|
||||||
"""Turn the entity on."""
|
"""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:
|
if light.ATTR_COLOR_TEMP in kwargs:
|
||||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||||
yield from self._endpoint.light_color.move_to_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:
|
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
|
self._brightness = brightness
|
||||||
# Move to level with on/off:
|
# Move to level with on/off:
|
||||||
yield from self._endpoint.level.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.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['pychromecast==0.8.1']
|
REQUIREMENTS = ['pychromecast==0.8.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@@ -216,6 +216,9 @@ class Metrics:
|
|||||||
value = state_helper.state_as_number(state)
|
value = state_helper.state_as_number(state)
|
||||||
metric.labels(**self._labels(state)).set(value)
|
metric.labels(**self._labels(state)).set(value)
|
||||||
|
|
||||||
|
def _handle_zwave(self, state):
|
||||||
|
self._battery(state)
|
||||||
|
|
||||||
|
|
||||||
class PrometheusView(HomeAssistantView):
|
class PrometheusView(HomeAssistantView):
|
||||||
"""Handle Prometheus requests."""
|
"""Handle Prometheus requests."""
|
||||||
|
@@ -433,9 +433,8 @@ class FitbitSensor(Entity):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from the Fitbit API and update the states."""
|
"""Get the latest data from the Fitbit API and update the states."""
|
||||||
if self.resource_type == 'devices/battery':
|
if self.resource_type == 'devices/battery' and self.extra:
|
||||||
response = self.client.get_devices()
|
self._state = self.extra.get('battery')
|
||||||
self._state = response[0].get('battery')
|
|
||||||
else:
|
else:
|
||||||
container = self.resource_type.replace("/", "-")
|
container = self.resource_type.replace("/", "-")
|
||||||
response = self.client.time_series(self.resource_type, period='7d')
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Lyft sensor."""
|
"""Set up the Lyft sensor."""
|
||||||
from lyft_rides.auth import ClientCredentialGrant
|
from lyft_rides.auth import ClientCredentialGrant
|
||||||
|
from lyft_rides.errors import APIError
|
||||||
|
|
||||||
auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID),
|
auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID),
|
||||||
client_secret=config.get(
|
client_secret=config.get(
|
||||||
CONF_CLIENT_SECRET),
|
CONF_CLIENT_SECRET),
|
||||||
scopes="public",
|
scopes="public",
|
||||||
is_sandbox_mode=False)
|
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)
|
wanted_product_ids = config.get(CONF_PRODUCT_IDS)
|
||||||
|
|
||||||
dev = []
|
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():
|
for product_id, product in timeandpriceest.products.items():
|
||||||
if (wanted_product_ids is not None) and \
|
if (wanted_product_ids is not None) and \
|
||||||
(product_id not in wanted_product_ids):
|
(product_id not in wanted_product_ids):
|
||||||
@@ -188,14 +195,18 @@ class LyftEstimate(object):
|
|||||||
self.end_latitude = end_latitude
|
self.end_latitude = end_latitude
|
||||||
self.end_longitude = end_longitude
|
self.end_longitude = end_longitude
|
||||||
self.products = None
|
self.products = None
|
||||||
self.__real_update()
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest product info and estimates from the Lyft API."""
|
"""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
|
from lyft_rides.client import LyftRidesClient
|
||||||
client = LyftRidesClient(self._session)
|
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
|
@asyncio.coroutine
|
||||||
def async_handle(self, intent_obj):
|
def async_handle(self, intent_obj):
|
||||||
"""Handle the intent."""
|
"""Handle the intent."""
|
||||||
|
items = intent_obj.hass.data[DOMAIN][-5:]
|
||||||
response = intent_obj.create_response()
|
response = intent_obj.create_response()
|
||||||
response.async_set_speech(
|
|
||||||
"These are the top 5 items in your shopping list: {}".format(
|
if len(items) == 0:
|
||||||
', '.join(reversed(intent_obj.hass.data[DOMAIN][-5:]))))
|
response.async_set_speech(
|
||||||
intent_obj.hass.bus.async_fire(EVENT)
|
"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
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice
|
|||||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||||
from homeassistant.helpers.event import track_time_change
|
from homeassistant.helpers.event import track_time_change
|
||||||
from homeassistant.helpers.sun import get_astral_event_date
|
from homeassistant.helpers.sun import get_astral_event_date
|
||||||
|
from homeassistant.util import slugify
|
||||||
from homeassistant.util.color import (
|
from homeassistant.util.color import (
|
||||||
color_temperature_to_rgb, color_RGB_to_xy,
|
color_temperature_to_rgb, color_RGB_to_xy,
|
||||||
color_temperature_kelvin_to_mired)
|
color_temperature_kelvin_to_mired)
|
||||||
@@ -111,7 +112,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Update lights."""
|
"""Update lights."""
|
||||||
flux.flux_update()
|
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):
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.weather import (
|
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)
|
from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN)
|
||||||
|
|
||||||
REQUIREMENTS = ["yahooweather==0.8"]
|
REQUIREMENTS = ["yahooweather==0.8"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_CONDITION = 'yahoo_condition'
|
||||||
|
|
||||||
ATTR_FORECAST_CONDITION = 'condition'
|
ATTR_FORECAST_CONDITION = 'condition'
|
||||||
ATTRIBUTION = "Weather details provided by Yahoo! Inc."
|
ATTRIBUTION = "Weather details provided by Yahoo! Inc."
|
||||||
|
|
||||||
CONF_FORECAST = 'forecast'
|
ATTR_FORECAST_TEMP_LOW = 'templow'
|
||||||
|
|
||||||
CONF_WOEID = 'woeid'
|
CONF_WOEID = 'woeid'
|
||||||
|
|
||||||
DEFAULT_NAME = 'Yweather'
|
DEFAULT_NAME = 'Yweather'
|
||||||
@@ -33,23 +37,22 @@ CONDITION_CLASSES = {
|
|||||||
'fog': [19, 20, 21, 22, 23],
|
'fog': [19, 20, 21, 22, 23],
|
||||||
'hail': [17, 18, 35],
|
'hail': [17, 18, 35],
|
||||||
'lightning': [37],
|
'lightning': [37],
|
||||||
'lightning-rainy': [38, 39],
|
'lightning-rainy': [38, 39, 47],
|
||||||
'partlycloudy': [44],
|
'partlycloudy': [44],
|
||||||
'pouring': [40, 45],
|
'pouring': [40, 45],
|
||||||
'rainy': [9, 11, 12],
|
'rainy': [9, 11, 12],
|
||||||
'snowy': [8, 13, 14, 15, 16, 41, 42, 43],
|
'snowy': [8, 13, 14, 15, 16, 41, 42, 43],
|
||||||
'snowy-rainy': [5, 6, 7, 10, 46, 47],
|
'snowy-rainy': [5, 6, 7, 10, 46],
|
||||||
'sunny': [32],
|
'sunny': [32, 33, 34],
|
||||||
'windy': [24],
|
'windy': [24],
|
||||||
'windy-variant': [],
|
'windy-variant': [],
|
||||||
'exceptional': [0, 1, 2, 3, 4, 25, 36],
|
'exceptional': [0, 1, 2, 3, 4, 25, 36],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_WOEID, default=None): cv.string,
|
vol.Optional(CONF_WOEID, default=None): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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
|
unit = hass.config.units.temperature_unit
|
||||||
woeid = config.get(CONF_WOEID)
|
woeid = config.get(CONF_WOEID)
|
||||||
forecast = config.get(CONF_FORECAST)
|
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
|
|
||||||
yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F
|
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!")
|
_LOGGER.critical("Can't retrieve weather data from Yahoo!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if forecast >= len(yahoo_api.yahoo.Forecast):
|
# create condition helper
|
||||||
_LOGGER.error("Yahoo! only support %d days forecast",
|
if DATA_CONDITION not in hass.data:
|
||||||
len(yahoo_api.yahoo.Forecast))
|
hass.data[DATA_CONDITION] = [str(x) for x in range(0, 50)]
|
||||||
return False
|
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):
|
class YahooWeatherWeather(WeatherEntity):
|
||||||
"""Representation of Yahoo! weather data."""
|
"""Representation of Yahoo! weather data."""
|
||||||
|
|
||||||
def __init__(self, weather_data, name, forecast):
|
def __init__(self, weather_data, name):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._data = weather_data
|
self._data = weather_data
|
||||||
self._forecast = forecast
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -103,9 +106,9 @@ class YahooWeatherWeather(WeatherEntity):
|
|||||||
def condition(self):
|
def condition(self):
|
||||||
"""Return the current condition."""
|
"""Return the current condition."""
|
||||||
try:
|
try:
|
||||||
return [k for k, v in CONDITION_CLASSES.items() if
|
return self.hass.data[DATA_CONDITION][int(
|
||||||
int(self._data.yahoo.Now['code']) in v][0]
|
self._data.yahoo.Now['code'])]
|
||||||
except IndexError:
|
except (ValueError, IndexError):
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -138,6 +141,11 @@ class YahooWeatherWeather(WeatherEntity):
|
|||||||
"""Return the wind speed."""
|
"""Return the wind speed."""
|
||||||
return self._data.yahoo.Wind['speed']
|
return self._data.yahoo.Wind['speed']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_bearing(self):
|
||||||
|
"""Return the wind direction."""
|
||||||
|
return self._data.yahoo.Wind['direction']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attribution(self):
|
def attribution(self):
|
||||||
"""Return the attribution."""
|
"""Return the attribution."""
|
||||||
@@ -147,19 +155,17 @@ class YahooWeatherWeather(WeatherEntity):
|
|||||||
def forecast(self):
|
def forecast(self):
|
||||||
"""Return the forecast array."""
|
"""Return the forecast array."""
|
||||||
try:
|
try:
|
||||||
forecast_condition = \
|
return [
|
||||||
[k for k, v in CONDITION_CLASSES.items() if
|
{
|
||||||
int(self._data.yahoo.Forecast[self._forecast]['code'])
|
ATTR_FORECAST_TIME: v['date'],
|
||||||
in v][0]
|
ATTR_FORECAST_TEMP:int(v['high']),
|
||||||
except IndexError:
|
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 STATE_UNKNOWN
|
||||||
|
|
||||||
return [{
|
|
||||||
ATTR_FORECAST_CONDITION: forecast_condition,
|
|
||||||
ATTR_FORECAST_TEMP:
|
|
||||||
self._data.yahoo.Forecast[self._forecast]['high'],
|
|
||||||
}]
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from Yahoo! and updates the states."""
|
"""Get the latest data from Yahoo! and updates the states."""
|
||||||
self._data.update()
|
self._data.update()
|
||||||
|
@@ -134,7 +134,7 @@ class Intent:
|
|||||||
class IntentResponse:
|
class IntentResponse:
|
||||||
"""Response to an intent."""
|
"""Response to an intent."""
|
||||||
|
|
||||||
def __init__(self, intent):
|
def __init__(self, intent=None):
|
||||||
"""Initialize an IntentResponse."""
|
"""Initialize an IntentResponse."""
|
||||||
self.intent = intent
|
self.intent = intent
|
||||||
self.speech = {}
|
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:
|
except ImportError:
|
||||||
keyring = None
|
keyring = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import credstash
|
||||||
|
except ImportError:
|
||||||
|
credstash = None
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -200,8 +205,13 @@ def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node):
|
|||||||
def _env_var_yaml(loader: SafeLineLoader,
|
def _env_var_yaml(loader: SafeLineLoader,
|
||||||
node: yaml.nodes.Node):
|
node: yaml.nodes.Node):
|
||||||
"""Load environment variables and embed it into the configuration YAML."""
|
"""Load environment variables and embed it into the configuration YAML."""
|
||||||
if node.value in os.environ:
|
args = node.value.split()
|
||||||
return os.environ[node.value]
|
|
||||||
|
# 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:
|
else:
|
||||||
_LOGGER.error("Environment variable %s not defined.", node.value)
|
_LOGGER.error("Environment variable %s not defined.", node.value)
|
||||||
raise HomeAssistantError(node.value)
|
raise HomeAssistantError(node.value)
|
||||||
@@ -257,6 +267,15 @@ def _secret_yaml(loader: SafeLineLoader,
|
|||||||
_LOGGER.debug("Secret %s retrieved from keyring", node.value)
|
_LOGGER.debug("Secret %s retrieved from keyring", node.value)
|
||||||
return pwd
|
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)
|
_LOGGER.error("Secret %s not defined", node.value)
|
||||||
raise HomeAssistantError(node.value)
|
raise HomeAssistantError(node.value)
|
||||||
|
|
||||||
|
2
pylintrc
2
pylintrc
@@ -14,6 +14,8 @@ reports=no
|
|||||||
# too-few-* - same as too-many-*
|
# too-few-* - same as too-many-*
|
||||||
# abstract-method - with intro of async there are always methods missing
|
# abstract-method - with intro of async there are always methods missing
|
||||||
|
|
||||||
|
generated-members=botocore.errorfactory
|
||||||
|
|
||||||
disable=
|
disable=
|
||||||
abstract-class-little-used,
|
abstract-class-little-used,
|
||||||
abstract-class-not-used,
|
abstract-class-not-used,
|
||||||
|
@@ -49,7 +49,7 @@ aiodns==1.1.1
|
|||||||
aiohttp_cors==0.5.3
|
aiohttp_cors==0.5.3
|
||||||
|
|
||||||
# homeassistant.components.light.lifx
|
# homeassistant.components.light.lifx
|
||||||
aiolifx==0.5.2
|
aiolifx==0.5.4
|
||||||
|
|
||||||
# homeassistant.components.light.lifx
|
# homeassistant.components.light.lifx
|
||||||
aiolifx_effects==0.1.1
|
aiolifx_effects==0.1.1
|
||||||
@@ -61,7 +61,7 @@ aiopvapi==1.4
|
|||||||
alarmdecoder==0.12.3
|
alarmdecoder==0.12.3
|
||||||
|
|
||||||
# homeassistant.components.amcrest
|
# homeassistant.components.amcrest
|
||||||
amcrest==1.2.0
|
amcrest==1.2.1
|
||||||
|
|
||||||
# homeassistant.components.media_player.anthemav
|
# homeassistant.components.media_player.anthemav
|
||||||
anthemav==1.1.8
|
anthemav==1.1.8
|
||||||
@@ -115,6 +115,9 @@ blockchain==1.3.3
|
|||||||
# homeassistant.components.tts.amazon_polly
|
# homeassistant.components.tts.amazon_polly
|
||||||
boto3==1.4.3
|
boto3==1.4.3
|
||||||
|
|
||||||
|
# homeassistant.scripts.credstash
|
||||||
|
botocore==1.4.93
|
||||||
|
|
||||||
# homeassistant.components.sensor.broadlink
|
# homeassistant.components.sensor.broadlink
|
||||||
# homeassistant.components.switch.broadlink
|
# homeassistant.components.switch.broadlink
|
||||||
broadlink==0.5
|
broadlink==0.5
|
||||||
@@ -136,6 +139,9 @@ colorlog>2.1,<3
|
|||||||
# homeassistant.components.binary_sensor.concord232
|
# homeassistant.components.binary_sensor.concord232
|
||||||
concord232==0.14
|
concord232==0.14
|
||||||
|
|
||||||
|
# homeassistant.scripts.credstash
|
||||||
|
credstash==1.13.2
|
||||||
|
|
||||||
# homeassistant.components.sensor.crimereports
|
# homeassistant.components.sensor.crimereports
|
||||||
crimereports==1.0.0
|
crimereports==1.0.0
|
||||||
|
|
||||||
@@ -404,7 +410,7 @@ myusps==1.1.2
|
|||||||
nad_receiver==0.0.6
|
nad_receiver==0.0.6
|
||||||
|
|
||||||
# homeassistant.components.discovery
|
# homeassistant.components.discovery
|
||||||
netdisco==1.0.1
|
netdisco==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.sensor.neurio_energy
|
# homeassistant.components.sensor.neurio_energy
|
||||||
neurio==0.3.1
|
neurio==0.3.1
|
||||||
@@ -503,6 +509,7 @@ py-cpuinfo==3.3.0
|
|||||||
# homeassistant.components.hdmi_cec
|
# homeassistant.components.hdmi_cec
|
||||||
pyCEC==0.4.13
|
pyCEC==0.4.13
|
||||||
|
|
||||||
|
# homeassistant.components.light.tplink
|
||||||
# homeassistant.components.switch.tplink
|
# homeassistant.components.switch.tplink
|
||||||
pyHS100==0.2.4.2
|
pyHS100==0.2.4.2
|
||||||
|
|
||||||
@@ -535,7 +542,7 @@ pybbox==0.0.5-alpha
|
|||||||
# pybluez==0.22
|
# pybluez==0.22
|
||||||
|
|
||||||
# homeassistant.components.media_player.cast
|
# homeassistant.components.media_player.cast
|
||||||
pychromecast==0.8.1
|
pychromecast==0.8.2
|
||||||
|
|
||||||
# homeassistant.components.media_player.cmus
|
# homeassistant.components.media_player.cmus
|
||||||
pycmus==0.1.0
|
pycmus==0.1.0
|
||||||
@@ -754,6 +761,9 @@ python-telegram-bot==6.1.0
|
|||||||
# homeassistant.components.sensor.twitch
|
# homeassistant.components.sensor.twitch
|
||||||
python-twitch==1.3.0
|
python-twitch==1.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.velbus
|
||||||
|
python-velbus==2.0.11
|
||||||
|
|
||||||
# homeassistant.components.media_player.vlc
|
# homeassistant.components.media_player.vlc
|
||||||
python-vlc==1.1.2
|
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'] == \
|
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
|
@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 io
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
import logging
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@@ -59,6 +60,13 @@ class TestYaml(unittest.TestCase):
|
|||||||
assert doc['password'] == "secret_password"
|
assert doc['password'] == "secret_password"
|
||||||
del os.environ["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):
|
def test_invalid_enviroment_variable(self):
|
||||||
"""Test config file with no enviroment variable sat."""
|
"""Test config file with no enviroment variable sat."""
|
||||||
conf = "password: !env_var PASSWORD"
|
conf = "password: !env_var PASSWORD"
|
||||||
@@ -372,6 +380,16 @@ class TestSecrets(unittest.TestCase):
|
|||||||
_yaml = load_yaml(self._yaml_path, yaml_str)
|
_yaml = load_yaml(self._yaml_path, yaml_str)
|
||||||
self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml)
|
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):
|
def test_secrets_logger_removed(self):
|
||||||
"""Ensure logger: debug was removed."""
|
"""Ensure logger: debug was removed."""
|
||||||
with self.assertRaises(yaml.HomeAssistantError):
|
with self.assertRaises(yaml.HomeAssistantError):
|
||||||
|
Reference in New Issue
Block a user