diff --git a/.coveragerc b/.coveragerc index 4f23fd9d8bf3e8..e97d197ca94a45 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = homeassistant/components/abode.py homeassistant/components/*/abode.py + homeassistant/components/ads/__init__.py + homeassistant/components/*/ads.py + homeassistant/components/alarmdecoder.py homeassistant/components/*/alarmdecoder.py @@ -427,6 +430,7 @@ omit = homeassistant/components/media_player/volumio.py homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha_musiccast.py + homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/mycroft.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py @@ -514,6 +518,7 @@ omit = homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py + homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/geizhals.py homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py diff --git a/CODEOWNERS b/CODEOWNERS index fe415a619db1c2..ac0f794482a320 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,7 @@ homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/sensor/airvisual.py @bachya +homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen homeassistant/components/sensor/sytadin.py @gautric diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py new file mode 100644 index 00000000000000..3d9de28ded3792 --- /dev/null +++ b/homeassistant/components/ads/__init__.py @@ -0,0 +1,217 @@ +""" +ADS Component. + +For more details about this component, please refer to the documentation. +https://home-assistant.io/components/ads/ + +""" +import os +import threading +import struct +import logging +import ctypes +from collections import namedtuple +import voluptuous as vol +from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ + EVENT_HOMEASSISTANT_STOP +from homeassistant.config import load_yaml_config_file +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyads==2.2.6'] + +_LOGGER = logging.getLogger(__name__) + +DATA_ADS = 'data_ads' + +# Supported Types +ADSTYPE_INT = 'int' +ADSTYPE_UINT = 'uint' +ADSTYPE_BYTE = 'byte' +ADSTYPE_BOOL = 'bool' + +DOMAIN = 'ads' + +# config variable names +CONF_ADS_VAR = 'adsvar' +CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' +CONF_ADS_TYPE = 'adstype' +CONF_ADS_FACTOR = 'factor' +CONF_ADS_VALUE = 'value' + +SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, + ADSTYPE_BYTE]), + vol.Required(CONF_ADS_VALUE): cv.match_all +}) + + +def setup(hass, config): + """Set up the ADS component.""" + import pyads + conf = config[DOMAIN] + + # get ads connection parameters from config + net_id = conf.get(CONF_DEVICE) + ip_address = conf.get(CONF_IP_ADDRESS) + port = conf.get(CONF_PORT) + + # create a new ads connection + client = pyads.Connection(net_id, port, ip_address) + + # add some constants to AdsHub + AdsHub.ADS_TYPEMAP = { + ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, + ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, + ADSTYPE_INT: pyads.PLCTYPE_INT, + ADSTYPE_UINT: pyads.PLCTYPE_UINT, + } + + AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL + AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE + AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT + AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT + AdsHub.ADSError = pyads.ADSError + + # connect to ads client and try to connect + try: + ads = AdsHub(client) + except pyads.pyads.ADSError: + _LOGGER.error( + 'Could not connect to ADS host (netid=%s, port=%s)', net_id, port + ) + return False + + # add ads hub to hass data collection, listen to shutdown + hass.data[DATA_ADS] = ads + hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown) + + def handle_write_data_by_name(call): + """Write a value to the connected ADS device.""" + ads_var = call.data.get(CONF_ADS_VAR) + ads_type = call.data.get(CONF_ADS_TYPE) + value = call.data.get(CONF_ADS_VALUE) + + try: + ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type]) + except pyads.ADSError as err: + _LOGGER.error(err) + + # load descriptions from services.yaml + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + hass.services.register( + DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name, + descriptions[SERVICE_WRITE_DATA_BY_NAME], + schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME + ) + + return True + + +# tuple to hold data needed for notification +NotificationItem = namedtuple( + 'NotificationItem', 'hnotify huser name plc_datatype callback' +) + + +class AdsHub: + """Representation of a PyADS connection.""" + + def __init__(self, ads_client): + """Initialize the ADS Hub.""" + self._client = ads_client + self._client.open() + + # all ADS devices are registered here + self._devices = [] + self._notification_items = {} + self._lock = threading.Lock() + + def shutdown(self, *args, **kwargs): + """Shutdown ADS connection.""" + _LOGGER.debug('Shutting down ADS') + for notification_item in self._notification_items.values(): + self._client.del_device_notification( + notification_item.hnotify, + notification_item.huser + ) + _LOGGER.debug( + 'Deleting device notification %d, %d', + notification_item.hnotify, notification_item.huser + ) + self._client.close() + + def register_device(self, device): + """Register a new device.""" + self._devices.append(device) + + def write_by_name(self, name, value, plc_datatype): + """Write a value to the device.""" + with self._lock: + return self._client.write_by_name(name, value, plc_datatype) + + def read_by_name(self, name, plc_datatype): + """Read a value from the device.""" + with self._lock: + return self._client.read_by_name(name, plc_datatype) + + def add_device_notification(self, name, plc_datatype, callback): + """Add a notification to the ADS devices.""" + from pyads import NotificationAttrib + attr = NotificationAttrib(ctypes.sizeof(plc_datatype)) + + with self._lock: + hnotify, huser = self._client.add_device_notification( + name, attr, self._device_notification_callback + ) + hnotify = int(hnotify) + + _LOGGER.debug( + 'Added Device Notification %d for variable %s', hnotify, name + ) + + self._notification_items[hnotify] = NotificationItem( + hnotify, huser, name, plc_datatype, callback + ) + + def _device_notification_callback(self, addr, notification, huser): + """Handle device notifications.""" + contents = notification.contents + + hnotify = int(contents.hNotification) + _LOGGER.debug('Received Notification %d', hnotify) + data = contents.data + + try: + notification_item = self._notification_items[hnotify] + except KeyError: + _LOGGER.debug('Unknown Device Notification handle: %d', hnotify) + return + + # parse data to desired datatype + if notification_item.plc_datatype == self.PLCTYPE_BOOL: + value = bool(struct.unpack(' None: + track_new: bool, defaults: dict, + devices: Sequence) -> None: """Initialize a device tracker.""" self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.consider_home = consider_home - self.track_new = track_new + self.track_new = defaults.get(CONF_TRACK_NEW, track_new) + self.defaults = defaults self.group = None self._is_updating = asyncio.Lock(loop=hass.loop) @@ -274,7 +285,8 @@ def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None, device = Device( self.hass, self.consider_home, self.track_new, dev_id, mac, (host_name or dev_id).replace('_', ' '), - picture=picture, icon=icon) + picture=picture, icon=icon, + hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) self.devices[dev_id] = device if mac is not None: self.mac_to_dev[mac] = device diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py new file mode 100644 index 00000000000000..319c19d7b73d86 --- /dev/null +++ b/homeassistant/components/device_tracker/meraki.py @@ -0,0 +1,116 @@ +""" +Support for the Meraki CMX location service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.meraki/ + +""" +import asyncio +import logging +import json + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY) +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER) + +CONF_VALIDATOR = 'validator' +CONF_SECRET = 'secret' +DEPENDENCIES = ['http'] +URL = '/api/meraki' +VERSION = '2.0' + + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_VALIDATOR): cv.string, + vol.Required(CONF_SECRET): cv.string +}) + + +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up an endpoint for the Meraki tracker.""" + hass.http.register_view( + MerakiView(config, async_see)) + + return True + + +class MerakiView(HomeAssistantView): + """View to handle Meraki requests.""" + + url = URL + name = 'api:meraki' + + def __init__(self, config, async_see): + """Initialize Meraki URL endpoints.""" + self.async_see = async_see + self.validator = config[CONF_VALIDATOR] + self.secret = config[CONF_SECRET] + + @asyncio.coroutine + def get(self, request): + """Meraki message received as GET.""" + return self.validator + + @asyncio.coroutine + def post(self, request): + """Meraki CMX message received.""" + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + _LOGGER.debug("Meraki Data from Post: %s", json.dumps(data)) + if not data.get('secret', False): + _LOGGER.error("secret invalid") + return self.json_message('No secret', HTTP_UNPROCESSABLE_ENTITY) + if data['secret'] != self.secret: + _LOGGER.error("Invalid Secret received from Meraki") + return self.json_message('Invalid secret', + HTTP_UNPROCESSABLE_ENTITY) + elif data['version'] != VERSION: + _LOGGER.error("Invalid API version: %s", data['version']) + return self.json_message('Invalid version', + HTTP_UNPROCESSABLE_ENTITY) + else: + _LOGGER.debug('Valid Secret') + if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'): + _LOGGER.error("Unknown Device %s", data['type']) + return self.json_message('Invalid device type', + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.debug("Processing %s", data['type']) + if len(data["data"]["observations"]) == 0: + _LOGGER.debug("No observations found") + return + self._handle(request.app['hass'], data) + + @callback + def _handle(self, hass, data): + for i in data["data"]["observations"]: + data["data"]["secret"] = "hidden" + mac = i["clientMac"] + _LOGGER.debug("clientMac: %s", mac) + attrs = {} + if i.get('os', False): + attrs['os'] = i['os'] + if i.get('manufacturer', False): + attrs['manufacturer'] = i['manufacturer'] + if i.get('ipv4', False): + attrs['ipv4'] = i['ipv4'] + if i.get('ipv6', False): + attrs['ipv6'] = i['ipv6'] + if i.get('seenTime', False): + attrs['seenTime'] = i['seenTime'] + if i.get('ssid', False): + attrs['ssid'] = i['ssid'] + hass.async_add_job(self.async_see( + mac=mac, + source_type=SOURCE_TYPE_ROUTER, + attributes=attrs + )) diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py index 633ea1b0c5e9cc..0d6645f37c11a0 100644 --- a/homeassistant/components/dominos.py +++ b/homeassistant/components/dominos.py @@ -136,6 +136,7 @@ def update_closest_store(self): def get_menu(self): """Return the products from the closest stores menu.""" + self.update_closest_store() if self.closest_store is None: _LOGGER.warning('Cannot get menu. Store may be closed') return [] diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e1121fd0c4e686..3d669ddc4d167f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171204.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20171206.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -303,7 +303,7 @@ def async_setup(hass, config): "/home-assistant-polymer", repo_path, False) hass.http.register_static_path( "/static/translations", - os.path.join(repo_path, "build-translations"), False) + os.path.join(repo_path, "build-translations/output"), False) sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js") sw_path_latest = os.path.join(repo_path, "build/service_worker.js") static_path = os.path.join(repo_path, 'hass_frontend') @@ -583,9 +583,9 @@ def _is_latest(js_option, request): family_min_version = { 'Chrome': 50, # Probably can reduce this - 'Firefox': 41, # Destructuring added in 41 + 'Firefox': 43, # Array.protopype.includes added in 43 'Opera': 40, # Probably can reduce this - 'Edge': 14, # Maybe can reduce this + 'Edge': 14, # Array.protopype.includes added in 14 'Safari': 10, # many features not supported by 9 } version = family_min_version.get(useragent.browser.family) diff --git a/homeassistant/components/light/ads.py b/homeassistant/components/light/ads.py new file mode 100644 index 00000000000000..41709a4692b6db --- /dev/null +++ b/homeassistant/components/light/ads.py @@ -0,0 +1,117 @@ +""" +Support for ADS light sources. + +For more details about this platform, please refer to the documentation. +https://home-assistant.io/components/light.ads/ + +""" +import asyncio +import logging +import voluptuous as vol +from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ + SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR, \ + CONF_ADS_VAR_BRIGHTNESS +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['ads'] +DEFAULT_NAME = 'ADS Light' +CONF_ADSVAR_BRIGHTNESS = 'adsvar_brightness' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the light platform for ADS.""" + ads_hub = hass.data.get(DATA_ADS) + + ads_var_enable = config.get(CONF_ADS_VAR) + ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS) + name = config.get(CONF_NAME) + + add_devices([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, + name)], True) + + +class AdsLight(Light): + """Representation of ADS light.""" + + def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name): + """Initialize AdsLight entity.""" + self._ads_hub = ads_hub + self._on_state = False + self._brightness = None + self._name = name + self.ads_var_enable = ads_var_enable + self.ads_var_brightness = ads_var_brightness + + @asyncio.coroutine + def async_added_to_hass(self): + """Register device notification.""" + def update_on_state(name, value): + """Handle device notifications for state.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + self._on_state = value + self.schedule_update_ha_state() + + def update_brightness(name, value): + """Handle device notification for brightness.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + self._brightness = value + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var_enable, self._ads_hub.PLCTYPE_BOOL, update_on_state + ) + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var_brightness, self._ads_hub.PLCTYPE_INT, + update_brightness + ) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light (0..255).""" + return self._brightness + + @property + def is_on(self): + """Return if light is on.""" + return self._on_state + + @property + def should_poll(self): + """Return False because entity pushes its state to HA.""" + return False + + @property + def supported_features(self): + """Flag supported features.""" + if self.ads_var_brightness is not None: + return SUPPORT_BRIGHTNESS + + def turn_on(self, **kwargs): + """Turn the light on or set a specific dimmer value.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + self._ads_hub.write_by_name(self.ads_var_enable, True, + self._ads_hub.PLCTYPE_BOOL) + + if self.ads_var_brightness is not None and brightness is not None: + self._ads_hub.write_by_name(self.ads_var_brightness, brightness, + self._ads_hub.PLCTYPE_UINT) + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._ads_hub.write_by_name(self.ads_var_enable, False, + self._ads_hub.PLCTYPE_BOOL) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index ca3da7ae165fe7..6ae44495e3e22b 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,7 +20,9 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==1.0.2'] +# Do not upgrade to 1.0.2, it breaks a bunch of stuff +# https://github.com/home-assistant/home-assistant/issues/10926 +REQUIREMENTS = ['pychromecast==0.8.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/ziggo_mediabox_xl.py b/homeassistant/components/media_player/ziggo_mediabox_xl.py new file mode 100644 index 00000000000000..1886cd751ea87f --- /dev/null +++ b/homeassistant/components/media_player/ziggo_mediabox_xl.py @@ -0,0 +1,174 @@ +""" +Support for interface with a Ziggo Mediabox XL. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.ziggo_mediabox_xl/ +""" +import logging +import socket + +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, MediaPlayerDevice, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_PLAY, SUPPORT_PAUSE) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['ziggo-mediabox-xl==1.0.0'] + +_LOGGER = logging.getLogger(__name__) + +DATA_KNOWN_DEVICES = 'ziggo_mediabox_xl_known_devices' + +SUPPORT_ZIGGO = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ziggo Mediabox XL platform.""" + from ziggo_mediabox_xl import ZiggoMediaboxXL + + hass.data[DATA_KNOWN_DEVICES] = known_devices = set() + + # Is this a manual configuration? + if config.get(CONF_HOST) is not None: + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + elif discovery_info is not None: + host = discovery_info.get('host') + name = discovery_info.get('name') + else: + _LOGGER.error("Cannot determine device") + return + + # Only add a device once, so discovered devices do not override manual + # config. + hosts = [] + ip_addr = socket.gethostbyname(host) + if ip_addr not in known_devices: + try: + mediabox = ZiggoMediaboxXL(ip_addr) + if mediabox.test_connection(): + hosts.append(ZiggoMediaboxXLDevice(mediabox, host, name)) + known_devices.add(ip_addr) + else: + _LOGGER.error("Can't connect to %s", host) + except socket.error as error: + _LOGGER.error("Can't connect to %s: %s", host, error) + else: + _LOGGER.info("Ignoring duplicate Ziggo Mediabox XL %s", host) + add_devices(hosts, True) + + +class ZiggoMediaboxXLDevice(MediaPlayerDevice): + """Representation of a Ziggo Mediabox XL Device.""" + + def __init__(self, mediabox, host, name): + """Initialize the device.""" + # Generate a configuration for the Samsung library + self._mediabox = mediabox + self._host = host + self._name = name + self._state = None + + def update(self): + """Retrieve the state of the device.""" + try: + if self._mediabox.turned_on(): + if self._state != STATE_PAUSED: + self._state = STATE_PLAYING + else: + self._state = STATE_OFF + except socket.error: + _LOGGER.error("Couldn't fetch state from %s", self._host) + + def send_keys(self, keys): + """Send keys to the device and handle exceptions.""" + try: + self._mediabox.send_keys(keys) + except socket.error: + _LOGGER.error("Couldn't send keys to %s", self._host) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def source_list(self): + """List of available sources (channels).""" + return [self._mediabox.channels()[c] + for c in sorted(self._mediabox.channels().keys())] + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ZIGGO + + def turn_on(self): + """Turn the media player on.""" + self.send_keys(['POWER']) + self._state = STATE_ON + + def turn_off(self): + """Turn off media player.""" + self.send_keys(['POWER']) + self._state = STATE_OFF + + def media_play(self): + """Send play command.""" + self.send_keys(['PLAY']) + self._state = STATE_PLAYING + + def media_pause(self): + """Send pause command.""" + self.send_keys(['PAUSE']) + self._state = STATE_PAUSED + + def media_play_pause(self): + """Simulate play pause media player.""" + self.send_keys(['PAUSE']) + if self._state == STATE_PAUSED: + self._state = STATE_PLAYING + else: + self._state = STATE_PAUSED + + def media_next_track(self): + """Channel up.""" + self.send_keys(['CHAN_UP']) + self._state = STATE_PLAYING + + def media_previous_track(self): + """Channel down.""" + self.send_keys(['CHAN_DOWN']) + self._state = STATE_PLAYING + + def select_source(self, source): + """Select the channel.""" + if str(source).isdigit(): + digits = str(source) + else: + digits = next(( + key for key, value in self._mediabox.channels().items() + if value == source), None) + if digits is None: + return + + self.send_keys(['NUM_{}'.format(digit) + for digit in str(digits)]) + self._state = STATE_PLAYING diff --git a/homeassistant/components/sensor/ads.py b/homeassistant/components/sensor/ads.py new file mode 100644 index 00000000000000..725cbb555f11a8 --- /dev/null +++ b/homeassistant/components/sensor/ads.py @@ -0,0 +1,103 @@ +""" +Support for ADS sensors. + +For more details about this platform, please refer to the documentation. +https://home-assistant.io/components/sensor.ads/ + +""" +import asyncio +import logging +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components import ads +from homeassistant.components.ads import CONF_ADS_VAR, CONF_ADS_TYPE, \ + CONF_ADS_FACTOR + + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'ADS sensor' +DEPENDENCIES = ['ads'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=''): cv.string, + vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In( + [ads.ADSTYPE_INT, ads.ADSTYPE_UINT, ads.ADSTYPE_BYTE] + ), + vol.Optional(CONF_ADS_FACTOR): cv.positive_int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up an ADS sensor device.""" + ads_hub = hass.data.get(ads.DATA_ADS) + + ads_var = config.get(CONF_ADS_VAR) + ads_type = config.get(CONF_ADS_TYPE) + name = config.get(CONF_NAME) + unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + factor = config.get(CONF_ADS_FACTOR) + + entity = AdsSensor(ads_hub, ads_var, ads_type, name, + unit_of_measurement, factor) + + add_devices([entity]) + + +class AdsSensor(Entity): + """Representation of an ADS sensor entity.""" + + def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, + factor): + """Initialize AdsSensor entity.""" + self._ads_hub = ads_hub + self._name = name + self._value = None + self._unit_of_measurement = unit_of_measurement + self.ads_var = ads_var + self.ads_type = ads_type + self.factor = factor + + @asyncio.coroutine + def async_added_to_hass(self): + """Register device notification.""" + def update(name, value): + """Handle device notifications.""" + _LOGGER.debug('Variable %s changed its value to %d', name, value) + + # if factor is set use it otherwise not + if self.factor is None: + self._value = value + else: + self._value = value / self.factor + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var, self._ads_hub.ADS_TYPEMAP[self.ads_type], update + ) + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._value + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """Return False because entity pushes its state to HA.""" + return False diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py new file mode 100755 index 00000000000000..2bc7e5b3b3a685 --- /dev/null +++ b/homeassistant/components/sensor/gearbest.py @@ -0,0 +1,127 @@ +""" +Parse prices of a item from gearbest. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.gearbest/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval +from homeassistant.const import (CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY) + +REQUIREMENTS = ['gearbest_parser==1.0.5'] +_LOGGER = logging.getLogger(__name__) + +CONF_ITEMS = 'items' + +ICON = 'mdi:coin' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2*60*60) # 2h +MIN_TIME_BETWEEN_CURRENCY_UPDATES = timedelta(seconds=12*60*60) # 12h + + +_ITEM_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_URL, 'XOR'): cv.string, + vol.Exclusive(CONF_ID, 'XOR'): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_CURRENCY): cv.string + }), cv.has_at_least_one_key(CONF_URL, CONF_ID) +) + +_ITEMS_SCHEMA = vol.Schema([_ITEM_SCHEMA]) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ITEMS): _ITEMS_SCHEMA, + vol.Required(CONF_CURRENCY): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Gearbest sensor.""" + from gearbest_parser import CurrencyConverter + currency = config.get(CONF_CURRENCY) + + sensors = [] + items = config.get(CONF_ITEMS) + + converter = CurrencyConverter() + converter.update() + + for item in items: + try: + sensors.append(GearbestSensor(converter, item, currency)) + except ValueError as exc: + _LOGGER.error(exc) + + def currency_update(event_time): + """Update currency list.""" + converter.update() + + track_time_interval(hass, + currency_update, + MIN_TIME_BETWEEN_CURRENCY_UPDATES) + + add_devices(sensors, True) + + +class GearbestSensor(Entity): + """Implementation of the sensor.""" + + def __init__(self, converter, item, currency): + """Initialize the sensor.""" + from gearbest_parser import GearbestParser + + self._name = item.get(CONF_NAME) + self._parser = GearbestParser() + self._parser.set_currency_converter(converter) + self._item = self._parser.load(item.get(CONF_ID), + item.get(CONF_URL), + item.get(CONF_CURRENCY, currency)) + if self._item is None: + raise ValueError("id and url could not be resolved") + + @property + def name(self): + """Return the name of the item.""" + return self._name if self._name is not None else self._item.name + + @property + def icon(self): + """Return the icon for the frontend.""" + return ICON + + @property + def state(self): + """Return the price of the selected product.""" + return self._item.price + + @property + def unit_of_measurement(self): + """Return the currency.""" + return self._item.currency + + @property + def entity_picture(self): + """Return the image.""" + return self._item.image + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {'name': self._item.name, + 'description': self._item.description, + 'currency': self._item.currency, + 'url': self._item.url} + return attrs + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest price from gearbest and updates the state.""" + self._item.update() diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index c4e460fdb663f2..c532c0dfd208d1 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -437,7 +437,7 @@ input_text: set_value: description: Set the value of an input text entity. fields: - entity_id: + entity_id: description: Entity id of the input text to set the new value. example: 'input_text.text1' value: @@ -448,7 +448,7 @@ input_number: set_value: description: Set the value of an input number entity. fields: - entity_id: + entity_id: description: Entity id of the input number to set the new value. example: 'input_number.threshold' value: @@ -457,13 +457,13 @@ input_number: increment: description: Increment the value of an input number entity by its stepping. fields: - entity_id: + entity_id: description: Entity id of the input number the should be incremented. example: 'input_number.threshold' decrement: description: Decrement the value of an input number entity by its stepping. fields: - entity_id: + entity_id: description: Entity id of the input number the should be decremented. example: 'input_number.threshold' diff --git a/homeassistant/components/switch/ads.py b/homeassistant/components/switch/ads.py new file mode 100644 index 00000000000000..f4abf2391e2a64 --- /dev/null +++ b/homeassistant/components/switch/ads.py @@ -0,0 +1,85 @@ +""" +Support for ADS switch platform. + +For more details about this platform, please refer to the documentation. +https://home-assistant.io/components/switch.ads/ + +""" +import asyncio +import logging +import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['ads'] +DEFAULT_NAME = 'ADS Switch' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADS_VAR): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up switch platform for ADS.""" + ads_hub = hass.data.get(DATA_ADS) + + name = config.get(CONF_NAME) + ads_var = config.get(CONF_ADS_VAR) + + add_devices([AdsSwitch(ads_hub, name, ads_var)], True) + + +class AdsSwitch(ToggleEntity): + """Representation of an Ads switch device.""" + + def __init__(self, ads_hub, name, ads_var): + """Initialize the AdsSwitch entity.""" + self._ads_hub = ads_hub + self._on_state = False + self._name = name + self.ads_var = ads_var + + @asyncio.coroutine + def async_added_to_hass(self): + """Register device notification.""" + def update(name, value): + """Handle device notification.""" + _LOGGER.debug('Variable %s changed its value to %d', + name, value) + self._on_state = value + self.schedule_update_ha_state() + + self.hass.async_add_job( + self._ads_hub.add_device_notification, + self.ads_var, self._ads_hub.PLCTYPE_BOOL, update + ) + + @property + def is_on(self): + """Return if the switch is turned on.""" + return self._on_state + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self): + """Return False because entity pushes its state to HA.""" + return False + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._ads_hub.write_by_name(self.ads_var, True, + self._ads_hub.PLCTYPE_BOOL) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._ads_hub.write_by_name(self.ads_var, False, + self._ads_hub.PLCTYPE_BOOL) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 6e8c1a6b9bb933..0772cc9277c510 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -22,9 +22,12 @@ ATTR_DAILY_CONSUMPTION = 'daily_consumption' ATTR_CURRENT = 'current' +CONF_LEDS = 'enable_leds' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LEDS, default=True): cv.boolean, }) @@ -34,17 +37,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from pyHS100 import SmartPlug host = config.get(CONF_HOST) name = config.get(CONF_NAME) + leds_on = config.get(CONF_LEDS) - add_devices([SmartPlugSwitch(SmartPlug(host), name)], True) + add_devices([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True) class SmartPlugSwitch(SwitchDevice): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug, name): + def __init__(self, smartplug, name, leds_on): """Initialize the switch.""" self.smartplug = smartplug self._name = name + self._leds_on = leds_on self._state = None self._available = True # Set up emeter cache @@ -89,6 +94,8 @@ def update(self): if self._name is None: self._name = self.smartplug.alias + self.smartplug.led = self._leds_on + if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 426893ec306794..18e14b2e91286c 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -28,7 +28,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.7.0', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.7.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) @@ -460,10 +460,11 @@ def service_handle(service): sirens_to_set.append(siren) for siren in sirens_to_set: + _man = siren.wink.device_manufacturer() if (service.service != SERVICE_SET_AUTO_SHUTOFF and service.service != SERVICE_ENABLE_SIREN and - siren.wink.device_manufacturer() != 'dome'): - _LOGGER.error("Service only valid for Dome sirens.") + (_man != 'dome' and _man != 'wink')): + _LOGGER.error("Service only valid for Dome or Wink sirens.") return if service.service == SERVICE_ENABLE_SIREN: @@ -494,10 +495,11 @@ def service_handle(service): component = EntityComponent(_LOGGER, DOMAIN, hass) sirens = [] - has_dome_siren = False + has_dome_or_wink_siren = False for siren in pywink.get_sirens(): - if siren.device_manufacturer() == "dome": - has_dome_siren = True + _man = siren.device_manufacturer() + if _man == "dome" or _man == "wink": + has_dome_or_wink_siren = True _id = siren.object_id() + siren.name() if _id not in hass.data[DOMAIN]['unique_ids']: sirens.append(WinkSirenDevice(siren, hass)) @@ -514,7 +516,7 @@ def service_handle(service): descriptions.get(SERVICE_ENABLE_SIREN), schema=ENABLED_SIREN_SCHEMA) - if has_dome_siren: + if has_dome_or_wink_siren: hass.services.register(DOMAIN, SERVICE_SET_SIREN_TONE, service_handle, diff --git a/requirements_all.txt b/requirements_all.txt index 838e8e4200804f..840ed5a834ac7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,6 +291,9 @@ gTTS-token==1.1.1 # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 +# homeassistant.components.sensor.gearbest +gearbest_parser==1.0.5 + # homeassistant.components.sensor.gitter gitterpy==0.1.6 @@ -334,7 +337,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171204.0 +home-assistant-frontend==20171206.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -600,6 +603,9 @@ pyTibber==0.2.1 # homeassistant.components.switch.dlink pyW215==0.6.0 +# homeassistant.components.ads +pyads==2.2.6 + # homeassistant.components.sensor.airvisual pyairvisual==1.0.0 @@ -626,7 +632,7 @@ pybbox==0.0.5-alpha # pybluez==0.22 # homeassistant.components.media_player.cast -pychromecast==1.0.2 +pychromecast==0.8.2 # homeassistant.components.media_player.cmus pycmus==0.1.0 @@ -889,7 +895,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.7.0 +python-wink==1.7.1 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.0.3 @@ -1179,3 +1185,6 @@ zengge==0.2 # homeassistant.components.zeroconf zeroconf==0.19.1 + +# homeassistant.components.media_player.ziggo_mediabox_xl +ziggo-mediabox-xl==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b858c8a1c0e70a..72325d6305ba12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -74,7 +74,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171204.0 +home-assistant-frontend==20171206.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 5982a6c16d80ea..63bbce2e7c6b08 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -205,6 +205,10 @@ def test_set_target_temp(self): self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) + climate.set_temperature(self.hass, None) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY) + self.assertEqual(30.0, state.attributes.get('temperature')) def test_sensor_bad_unit(self): """Test sensor that have bad unit.""" @@ -888,19 +892,22 @@ def test_custom_setup_params(hass): 'min_temp': MIN_TEMP, 'max_temp': MAX_TEMP, 'target_temp': TARGET_TEMP, + 'initial_operation_mode': STATE_OFF, }}) assert result state = hass.states.get(ENTITY) assert state.attributes.get('min_temp') == MIN_TEMP assert state.attributes.get('max_temp') == MAX_TEMP assert state.attributes.get('temperature') == TARGET_TEMP + assert state.attributes.get(climate.ATTR_OPERATION_MODE) == STATE_OFF @asyncio.coroutine def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache(hass, ( - State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20"}), + State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", + climate.ATTR_OPERATION_MODE: "off"}), )) hass.state = CoreState.starting @@ -915,3 +922,29 @@ def test_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 20) + assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off") + + +@asyncio.coroutine +def test_no_restore_state(hass): + """Ensure states are not restored on startup if not needed.""" + mock_restore_cache(hass, ( + State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", + climate.ATTR_OPERATION_MODE: "off"}), + )) + + hass.state = CoreState.starting + + yield from async_setup_component( + hass, climate.DOMAIN, {'climate': { + 'platform': 'generic_thermostat', + 'name': 'test_thermostat', + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR, + 'target_temp': 22, + 'initial_operation_mode': 'auto', + }}) + + state = hass.states.get('climate.test_thermostat') + assert(state.attributes[ATTR_TEMPERATURE] == 22) + assert(state.attributes[climate.ATTR_OPERATION_MODE] != "off") diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index b507bfea7c9176..a6827d165cd6e2 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -9,7 +9,8 @@ from homeassistant.setup import setup_component from homeassistant.components import device_tracker from homeassistant.components.device_tracker import ( - CONF_CONSIDER_HOME, CONF_TRACK_NEW) + CONF_CONSIDER_HOME, CONF_TRACK_NEW, CONF_NEW_DEVICE_DEFAULTS, + CONF_AWAY_HIDE) from homeassistant.components.device_tracker.asuswrt import ( CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, CONF_PORT, PLATFORM_SCHEMA) @@ -78,7 +79,11 @@ def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock): \ CONF_USERNAME: 'fake_user', CONF_PASSWORD: 'fake_pass', CONF_TRACK_NEW: True, - CONF_CONSIDER_HOME: timedelta(seconds=180) + CONF_CONSIDER_HOME: timedelta(seconds=180), + CONF_NEW_DEVICE_DEFAULTS: { + CONF_TRACK_NEW: True, + CONF_AWAY_HIDE: False + } } } @@ -104,7 +109,11 @@ def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): \ CONF_USERNAME: 'fake_user', CONF_PUB_KEY: FAKEFILE, CONF_TRACK_NEW: True, - CONF_CONSIDER_HOME: timedelta(seconds=180) + CONF_CONSIDER_HOME: timedelta(seconds=180), + CONF_NEW_DEVICE_DEFAULTS: { + CONF_TRACK_NEW: True, + CONF_AWAY_HIDE: False + } } } diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 704b2590f1240d..34c7ecf465db92 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -123,7 +123,7 @@ def test_track_with_duplicate_mac_dev_id(self, mock_warning): 'My device', None, None, False), device_tracker.Device(self.hass, True, True, 'your_device', 'AB:01', 'Your device', None, None, False)] - device_tracker.DeviceTracker(self.hass, False, True, devices) + device_tracker.DeviceTracker(self.hass, False, True, {}, devices) _LOGGER.debug(mock_warning.call_args_list) assert mock_warning.call_count == 1, \ "The only warning call should be duplicates (check DEBUG)" @@ -137,7 +137,7 @@ def test_track_with_duplicate_mac_dev_id(self, mock_warning): 'AB:01', 'My device', None, None, False), device_tracker.Device(self.hass, True, True, 'my_device', None, 'Your device', None, None, False)] - device_tracker.DeviceTracker(self.hass, False, True, devices) + device_tracker.DeviceTracker(self.hass, False, True, {}, devices) _LOGGER.debug(mock_warning.call_args_list) assert mock_warning.call_count == 1, \ @@ -299,7 +299,7 @@ def test_mac_vendor_lookup_on_see(self): vendor_string = 'Raspberry Pi Foundation' tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), 0, []) + self.hass, timedelta(seconds=60), 0, {}, []) with mock_aiohttp_client() as aioclient_mock: aioclient_mock.get('http://api.macvendors.com/b8:27:eb', @@ -622,7 +622,7 @@ def test_see_passive_zone_state(self): def test_see_failures(self, mock_warning): """Test that the device tracker see failures.""" tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), 0, []) + self.hass, timedelta(seconds=60), 0, {}, []) # MAC is not a string (but added) tracker.see(mac=567, host_name="Number MAC") @@ -654,7 +654,7 @@ def test_config_failure(self): def test_picture_and_icon_on_see_discovery(self): """Test that picture and icon are set in initial see.""" tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), False, []) + self.hass, timedelta(seconds=60), False, {}, []) tracker.see(dev_id=11, picture='pic_url', icon='mdi:icon') self.hass.block_till_done() config = device_tracker.load_config(self.yaml_devices, self.hass, @@ -663,6 +663,18 @@ def test_picture_and_icon_on_see_discovery(self): assert config[0].icon == 'mdi:icon' assert config[0].entity_picture == 'pic_url' + def test_default_hide_if_away_is_used(self): + """Test that default track_new is used.""" + tracker = device_tracker.DeviceTracker( + self.hass, timedelta(seconds=60), False, + {device_tracker.CONF_AWAY_HIDE: True}, []) + tracker.see(dev_id=12) + self.hass.block_till_done() + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0)) + assert len(config) == 1 + self.assertTrue(config[0].hidden) + @asyncio.coroutine def test_async_added_to_hass(hass): diff --git a/tests/components/device_tracker/test_meraki.py b/tests/components/device_tracker/test_meraki.py new file mode 100644 index 00000000000000..a739df804fd7aa --- /dev/null +++ b/tests/components/device_tracker/test_meraki.py @@ -0,0 +1,139 @@ +"""The tests the for Meraki device tracker.""" +import asyncio +import json +from unittest.mock import patch +import pytest +from homeassistant.components.device_tracker.meraki import ( + CONF_VALIDATOR, CONF_SECRET) +from homeassistant.setup import async_setup_component +import homeassistant.components.device_tracker as device_tracker +from homeassistant.const import CONF_PLATFORM +from homeassistant.components.device_tracker.meraki import URL + + +@pytest.fixture +def meraki_client(loop, hass, test_client): + """Meraki mock client.""" + assert loop.run_until_complete(async_setup_component( + hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'meraki', + CONF_VALIDATOR: 'validator', + CONF_SECRET: 'secret' + + } + })) + + with patch('homeassistant.components.device_tracker.update_config'): + yield loop.run_until_complete(test_client(hass.http.app)) + + +@asyncio.coroutine +def test_invalid_or_missing_data(meraki_client): + """Test validator with invalid or missing data.""" + req = yield from meraki_client.get(URL) + text = yield from req.text() + assert req.status == 200 + assert text == 'validator' + + req = yield from meraki_client.post(URL, data=b"invalid") + text = yield from req.json() + assert req.status == 400 + assert text['message'] == 'Invalid JSON' + + req = yield from meraki_client.post(URL, data=b"{}") + text = yield from req.json() + assert req.status == 422 + assert text['message'] == 'No secret' + + data = { + "version": "1.0", + "secret": "secret" + } + req = yield from meraki_client.post(URL, data=json.dumps(data)) + text = yield from req.json() + assert req.status == 422 + assert text['message'] == 'Invalid version' + + data = { + "version": "2.0", + "secret": "invalid" + } + req = yield from meraki_client.post(URL, data=json.dumps(data)) + text = yield from req.json() + assert req.status == 422 + assert text['message'] == 'Invalid secret' + + data = { + "version": "2.0", + "secret": "secret", + "type": "InvalidType" + } + req = yield from meraki_client.post(URL, data=json.dumps(data)) + text = yield from req.json() + assert req.status == 422 + assert text['message'] == 'Invalid device type' + + data = { + "version": "2.0", + "secret": "secret", + "type": "BluetoothDevicesSeen", + "data": { + "observations": [] + } + } + req = yield from meraki_client.post(URL, data=json.dumps(data)) + assert req.status == 200 + + +@asyncio.coroutine +def test_data_will_be_saved(hass, meraki_client): + """Test with valid data.""" + data = { + "version": "2.0", + "secret": "secret", + "type": "DevicesSeen", + "data": { + "observations": [ + { + "location": { + "lat": "51.5355157", + "lng": "21.0699035", + "unc": "46.3610585", + }, + "seenTime": "2016-09-12T16:23:13Z", + "ssid": 'ssid', + "os": 'HA', + "ipv6": '2607:f0d0:1002:51::4/64', + "clientMac": "00:26:ab:b8:a9:a4", + "seenEpoch": "147369739", + "rssi": "20", + "manufacturer": "Seiko Epson" + }, + { + "location": { + "lat": "51.5355357", + "lng": "21.0699635", + "unc": "46.3610585", + }, + "seenTime": "2016-09-12T16:21:13Z", + "ssid": 'ssid', + "os": 'HA', + "ipv4": '192.168.0.1', + "clientMac": "00:26:ab:b8:a9:a5", + "seenEpoch": "147369750", + "rssi": "20", + "manufacturer": "Seiko Epson" + } + ] + } + } + req = yield from meraki_client.post(URL, data=json.dumps(data)) + assert req.status == 200 + state_name = hass.states.get('{}.{}'.format('device_tracker', + '0026abb8a9a4')).state + assert 'home' == state_name + + state_name = hass.states.get('{}.{}'.format('device_tracker', + '0026abb8a9a5')).state + assert 'home' == state_name diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index 0e22758d07ecb8..b378118141a1d7 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -11,7 +11,8 @@ from homeassistant.setup import setup_component from homeassistant.components import device_tracker from homeassistant.components.device_tracker import ( - CONF_CONSIDER_HOME, CONF_TRACK_NEW) + CONF_CONSIDER_HOME, CONF_TRACK_NEW, CONF_AWAY_HIDE, + CONF_NEW_DEVICE_DEFAULTS) from homeassistant.components.device_tracker.unifi_direct import ( DOMAIN, CONF_PORT, PLATFORM_SCHEMA, _response_to_json, get_scanner) from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, @@ -54,7 +55,11 @@ def test_get_scanner(self, unifi_mock): \ CONF_USERNAME: 'fake_user', CONF_PASSWORD: 'fake_pass', CONF_TRACK_NEW: True, - CONF_CONSIDER_HOME: timedelta(seconds=180) + CONF_CONSIDER_HOME: timedelta(seconds=180), + CONF_NEW_DEVICE_DEFAULTS: { + CONF_TRACK_NEW: True, + CONF_AWAY_HIDE: False + } } }