diff --git a/.coveragerc b/.coveragerc index 3d32256e9fb..c8e59e55357 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,9 @@ omit = homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present + homeassistant/components/alarmdecoder.py + homeassistant/components/*/alarmdecoder.py + homeassistant/components/apcupsd.py homeassistant/components/*/apcupsd.py @@ -94,6 +97,9 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/tradfri.py + homeassistant/components/*/tradfri.py + homeassistant/components/twilio.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py @@ -159,6 +165,7 @@ omit = homeassistant/components/binary_sensor/flic.py homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/iss.py + homeassistant/components/binary_sensor/ping.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/amcrest.py @@ -175,7 +182,6 @@ omit = homeassistant/components/climate/oem.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py - homeassistant/components/config/zwave.py homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/myq.py @@ -225,15 +231,18 @@ omit = homeassistant/components/light/flux_led.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py - homeassistant/components/light/lifx.py + homeassistant/components/light/lifx/*.py homeassistant/components/light/lifx_legacy.py homeassistant/components/light/limitlessled.py + homeassistant/components/light/mystrom.py homeassistant/components/light/osramlightify.py + homeassistant/components/light/rpi_gpio_pwm.py + homeassistant/components/light/piglow.py homeassistant/components/light/tikteck.py + homeassistant/components/light/tradfri.py homeassistant/components/light/x10.py homeassistant/components/light/yeelight.py homeassistant/components/light/yeelightsunflower.py - homeassistant/components/light/piglow.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py homeassistant/components/lock/nuki.py @@ -274,6 +283,7 @@ omit = homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py + homeassistant/components/media_player/spotify.py homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/vlc.py homeassistant/components/media_player/volumio.py @@ -315,6 +325,7 @@ omit = homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py homeassistant/components/scene/hunterdouglas_powerview.py + homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/amcrest.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py @@ -375,6 +386,7 @@ omit = homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openevse.py homeassistant/components/sensor/openexchangerates.py + homeassistant/components/sensor/opensky.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py @@ -432,7 +444,7 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py - homeassistant/components/telegram_webhooks.py + homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/picotts.py @@ -442,7 +454,6 @@ omit = homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py - homeassistant/components/zwave/__init__.py homeassistant/components/zwave/util.py diff --git a/Dockerfile b/Dockerfile index 8c7ab8a9039..579229d154a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no +#ENV INSTALL_COAP_CLIENT no VOLUME /config @@ -21,7 +22,7 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet # Copy source COPY . . diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4c586d12ccd..7f8c586fd80 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -27,7 +27,8 @@ _LOGGER = logging.getLogger(__name__) ERROR_LOG_FILENAME = 'home-assistant.log' FIRST_INIT_COMPONENT = set(( - 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction')) + 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction', + 'frontend', 'history')) def from_config_dict(config: Dict[str, Any], diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py new file mode 100644 index 00000000000..f176a87827b --- /dev/null +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -0,0 +1,119 @@ +""" +Support for AlarmDecoder-based alarm control panels (Honeywell/DSC). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.components.alarm_control_panel as alarm + +from homeassistant.components.alarmdecoder import (DATA_AD, + SIGNAL_PANEL_MESSAGE) + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['alarmdecoder'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Perform the setup for AlarmDecoder alarm panels.""" + _LOGGER.debug("AlarmDecoderAlarmPanel: setup") + + device = AlarmDecoderAlarmPanel("Alarm Panel", hass) + + async_add_devices([device]) + + return True + + +class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): + """Representation of an AlarmDecoder-based alarm panel.""" + + def __init__(self, name, hass): + """Initialize the alarm panel.""" + self._display = "" + self._name = name + self._state = STATE_UNKNOWN + + _LOGGER.debug("AlarmDecoderAlarm: Setting up panel") + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) + + @callback + def _message_callback(self, message): + if message.alarm_sounding or message.fire_alarm: + if self._state != STATE_ALARM_TRIGGERED: + self._state = STATE_ALARM_TRIGGERED + self.hass.async_add_job(self.async_update_ha_state()) + elif message.armed_away: + if self._state != STATE_ALARM_ARMED_AWAY: + self._state = STATE_ALARM_ARMED_AWAY + self.hass.async_add_job(self.async_update_ha_state()) + elif message.armed_home: + if self._state != STATE_ALARM_ARMED_HOME: + self._state = STATE_ALARM_ARMED_HOME + self.hass.async_add_job(self.async_update_ha_state()) + else: + if self._state != STATE_ALARM_DISARMED: + self._state = STATE_ALARM_DISARMED + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def code_format(self): + """Regex for code format or None if no code is required.""" + return '^\\d{4,6}$' + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @asyncio.coroutine + def async_alarm_disarm(self, code=None): + """Send disarm command.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: sending %s1", + str(code)) + self.hass.data[DATA_AD].send("{!s}1".format(code)) + + @asyncio.coroutine + def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: sending %s2", + str(code)) + self.hass.data[DATA_AD].send("{!s}2".format(code)) + + @asyncio.coroutine + def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: sending %s3", + str(code)) + self.hass.data[DATA_AD].send("{!s}3".format(code)) diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py new file mode 100644 index 00000000000..ec99f2381e5 --- /dev/null +++ b/homeassistant/components/alarmdecoder.py @@ -0,0 +1,171 @@ +""" +Support for AlarmDecoder devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/alarmdecoder/ +""" +import asyncio +import logging + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['alarmdecoder==0.12.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'alarmdecoder' + +DATA_AD = 'alarmdecoder' + + +CONF_DEVICE = 'device' +CONF_DEVICE_TYPE = 'type' +CONF_DEVICE_HOST = 'host' +CONF_DEVICE_PORT = 'port' +CONF_DEVICE_PATH = 'path' +CONF_DEVICE_BAUD = 'baudrate' + +CONF_ZONES = 'zones' +CONF_ZONE_NAME = 'name' +CONF_ZONE_TYPE = 'type' + +CONF_PANEL_DISPLAY = 'panel_display' + +DEFAULT_DEVICE_TYPE = 'socket' +DEFAULT_DEVICE_HOST = 'localhost' +DEFAULT_DEVICE_PORT = 10000 +DEFAULT_DEVICE_PATH = '/dev/ttyUSB0' +DEFAULT_DEVICE_BAUD = 115200 + +DEFAULT_PANEL_DISPLAY = False + +DEFAULT_ZONE_TYPE = 'opening' + +SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message' +SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away' +SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home' +SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' + +SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' +SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' + +DEVICE_SOCKET_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'socket', + vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string, + vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port}) + +DEVICE_SERIAL_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'serial', + vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, + vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string}) + +DEVICE_USB_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'usb'}) + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_NAME): cv.string, + vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA, + DEVICE_SERIAL_SCHEMA, + DEVICE_USB_SCHEMA), + vol.Optional(CONF_PANEL_DISPLAY, + default=DEFAULT_PANEL_DISPLAY): cv.boolean, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + }), +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Common setup for AlarmDecoder devices.""" + from alarmdecoder import AlarmDecoder + from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) + + conf = config.get(DOMAIN) + + device = conf.get(CONF_DEVICE) + display = conf.get(CONF_PANEL_DISPLAY) + zones = conf.get(CONF_ZONES) + + device_type = device.get(CONF_DEVICE_TYPE) + host = DEFAULT_DEVICE_HOST + port = DEFAULT_DEVICE_PORT + path = DEFAULT_DEVICE_PATH + baud = DEFAULT_DEVICE_BAUD + + sync_connect = asyncio.Future(loop=hass.loop) + + def handle_open(device): + """Callback for a successful connection.""" + _LOGGER.info("Established a connection with the alarmdecoder.") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) + sync_connect.set_result(True) + + @callback + def stop_alarmdecoder(event): + """Callback to handle shutdown alarmdecoder.""" + _LOGGER.debug("Shutting down alarmdecoder.") + controller.close() + + @callback + def handle_message(sender, message): + """Callback to handle message from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message) + + def zone_fault_callback(sender, zone): + """Callback to handle zone fault from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone) + + def zone_restore_callback(sender, zone): + """Callback to handle zone restore from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone) + + controller = False + if device_type == 'socket': + host = device.get(CONF_DEVICE_HOST) + port = device.get(CONF_DEVICE_PORT) + controller = AlarmDecoder(SocketDevice(interface=(host, port))) + elif device_type == 'serial': + path = device.get(CONF_DEVICE_PATH) + baud = device.get(CONF_DEVICE_BAUD) + controller = AlarmDecoder(SerialDevice(interface=path)) + elif device_type == 'usb': + AlarmDecoder(USBDevice.find()) + return False + + controller.on_open += handle_open + controller.on_message += handle_message + controller.on_zone_fault += zone_fault_callback + controller.on_zone_restore += zone_restore_callback + + hass.data[DATA_AD] = controller + + controller.open(baud) + + result = yield from sync_connect + + if not result: + return False + + hass.async_add_job(async_load_platform(hass, 'alarm_control_panel', DOMAIN, + conf, config)) + + if zones: + hass.async_add_job(async_load_platform( + hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) + + if display: + hass.async_add_job(async_load_platform(hass, 'sensor', DOMAIN, + conf, config)) + + return True diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b22bd851190..8beb737ae89 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -17,9 +17,9 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, - HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS, + MATCH_ALL, URL_API, URL_API_COMPONENTS, URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, - URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES, + URL_API_EVENTS, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, __version__) from homeassistant.exceptions import TemplateError @@ -48,7 +48,6 @@ def setup(hass, config): hass.http.register_view(APIEventView) hass.http.register_view(APIServicesView) hass.http.register_view(APIDomainServicesView) - hass.http.register_view(APIEventForwardingView) hass.http.register_view(APIComponentsView) hass.http.register_view(APITemplateView) @@ -319,79 +318,6 @@ class APIDomainServicesView(HomeAssistantView): return self.json(changed_states) -class APIEventForwardingView(HomeAssistantView): - """View to handle EventForwarding requests.""" - - url = URL_API_EVENT_FORWARD - name = "api:event-forward" - event_forwarder = None - - @asyncio.coroutine - def post(self, request): - """Setup an event forwarder.""" - _LOGGER.warning('Event forwarding is deprecated. ' - 'Will be removed by 0.43') - hass = request.app['hass'] - try: - data = yield from request.json() - except ValueError: - return self.json_message("No data received.", HTTP_BAD_REQUEST) - - try: - host = data['host'] - api_password = data['api_password'] - except KeyError: - return self.json_message("No host or api_password received.", - HTTP_BAD_REQUEST) - - try: - port = int(data['port']) if 'port' in data else None - except ValueError: - return self.json_message("Invalid value received for port.", - HTTP_UNPROCESSABLE_ENTITY) - - api = rem.API(host, api_password, port) - - valid = yield from hass.loop.run_in_executor( - None, api.validate_api) - if not valid: - return self.json_message("Unable to validate API.", - HTTP_UNPROCESSABLE_ENTITY) - - if self.event_forwarder is None: - self.event_forwarder = rem.EventForwarder(hass) - - self.event_forwarder.async_connect(api) - - return self.json_message("Event forwarding setup.") - - @asyncio.coroutine - def delete(self, request): - """Remove event forwarder.""" - try: - data = yield from request.json() - except ValueError: - return self.json_message("No data received.", HTTP_BAD_REQUEST) - - try: - host = data['host'] - except KeyError: - return self.json_message("No host received.", HTTP_BAD_REQUEST) - - try: - port = int(data['port']) if 'port' in data else None - except ValueError: - return self.json_message("Invalid value received for port.", - HTTP_UNPROCESSABLE_ENTITY) - - if self.event_forwarder is not None: - api = rem.API(host, None, port) - - self.event_forwarder.async_disconnect(api) - - return self.json_message("Event forwarding cancelled.") - - class APIComponentsView(HomeAssistantView): """View to handle Components requests.""" diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 239c80523df..8625685c057 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -13,7 +13,7 @@ from homeassistant.const import ( from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['PyMata==2.13'] +REQUIREMENTS = ['PyMata==2.14'] _LOGGER = logging.getLogger(__name__) @@ -29,18 +29,25 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Setup the Arduino component.""" + """Set up the Arduino component.""" import serial + + port = config[DOMAIN][CONF_PORT] + global BOARD try: - BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT]) + BOARD = ArduinoBoard(port) except (serial.serialutil.SerialException, FileNotFoundError): - _LOGGER.exception("Your port is not accessible.") + _LOGGER.error("Your port %s is not accessible", port) return False - if BOARD.get_firmata()[1] <= 2: - _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.") - return False + try: + if BOARD.get_firmata()[1] <= 2: + _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer") + return False + except IndexError: + _LOGGER.warning("The version of the StandardFirmata sketch was not" + "detected. This may lead to side effects") def stop_arduino(event): """Stop the Arduino service.""" @@ -67,25 +74,20 @@ class ArduinoBoard(object): def set_mode(self, pin, direction, mode): """Set the mode and the direction of a given pin.""" if mode == 'analog' and direction == 'in': - self._board.set_pin_mode(pin, - self._board.INPUT, - self._board.ANALOG) + self._board.set_pin_mode( + pin, self._board.INPUT, self._board.ANALOG) elif mode == 'analog' and direction == 'out': - self._board.set_pin_mode(pin, - self._board.OUTPUT, - self._board.ANALOG) + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.ANALOG) elif mode == 'digital' and direction == 'in': - self._board.set_pin_mode(pin, - self._board.INPUT, - self._board.DIGITAL) + self._board.set_pin_mode( + pin, self._board.INPUT, self._board.DIGITAL) elif mode == 'digital' and direction == 'out': - self._board.set_pin_mode(pin, - self._board.OUTPUT, - self._board.DIGITAL) + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.DIGITAL) elif mode == 'pwm': - self._board.set_pin_mode(pin, - self._board.OUTPUT, - self._board.PWM) + self._board.set_pin_mode( + pin, self._board.OUTPUT, self._board.PWM) def get_analog_inputs(self): """Get the values from the pins.""" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 66c7c763cc9..5c3d944aad4 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -263,15 +263,23 @@ class AutomationEntity(ToggleEntity): @asyncio.coroutine def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" - enable_automation = DEFAULT_INITIAL_STATE - if self._initial_state is not None: enable_automation = self._initial_state + _LOGGER.debug("Automation %s initial state %s from config " + "initial_state", self.entity_id, enable_automation) else: state = yield from async_get_last_state(self.hass, self.entity_id) if state: enable_automation = state.state == STATE_ON self._last_triggered = state.attributes.get('last_triggered') + _LOGGER.debug("Automation %s initial state %s from recorder " + "last state %s", self.entity_id, + enable_automation, state) + else: + enable_automation = DEFAULT_INITIAL_STATE + _LOGGER.debug("Automation %s initial state %s from default " + "initial state", self.entity_id, + enable_automation) if not enable_automation: return diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py new file mode 100644 index 00000000000..e6292128710 --- /dev/null +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -0,0 +1,123 @@ +""" +Support for AlarmDecoder zone states- represented as binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import BinarySensorDevice + +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_OPEN, STATE_CLOSED) +from homeassistant.components.alarmdecoder import (ZONE_SCHEMA, + CONF_ZONES, + CONF_ZONE_NAME, + CONF_ZONE_TYPE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE) + + +DEPENDENCIES = ['alarmdecoder'] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup AlarmDecoder binary sensor devices.""" + configured_zones = discovery_info[CONF_ZONES] + + devices = [] + + for zone_num in configured_zones: + device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + zone_type = device_config_data[CONF_ZONE_TYPE] + zone_name = device_config_data[CONF_ZONE_NAME] + device = AlarmDecoderBinarySensor(hass, + zone_num, + zone_name, + zone_type) + devices.append(device) + + async_add_devices(devices) + + return True + + +class AlarmDecoderBinarySensor(BinarySensorDevice): + """Representation of an AlarmDecoder binary sensor.""" + + def __init__(self, hass, zone_number, zone_name, zone_type): + """Initialize the binary_sensor.""" + self._zone_number = zone_number + self._zone_type = zone_type + self._state = 0 + self._name = zone_name + self._type = zone_type + + _LOGGER.debug('AlarmDecoderBinarySensor: Setup up zone: ' + zone_name) + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) + + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback) + + @property + def state(self): + """Return the state of the binary sensor.""" + if self._type == 'opening': + return STATE_OPEN if self.is_on else STATE_CLOSED + + return STATE_ON if self.is_on else STATE_OFF + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def icon(self): + """Icon for device by its type.""" + if "window" in self._name.lower(): + return "mdi:window-open" if self.is_on else "mdi:window-closed" + + if self._type == 'smoke': + return "mdi:fire" + + return None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state == 1 + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return self._zone_type + + @callback + def _fault_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 1 + self.hass.async_add_job(self.async_update_ha_state()) + + @callback + def _restore_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 0 + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py index eedcfb6060e..2079d6a1ce8 100644 --- a/homeassistant/components/binary_sensor/flic.py +++ b/homeassistant/components/binary_sensor/flic.py @@ -1,4 +1,9 @@ -"""Contains functionality to use flic buttons as a binary sensor.""" +""" +Support to use flic buttons as a binary sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.flic/ +""" import logging import threading @@ -11,39 +16,40 @@ from homeassistant.const import ( from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) - REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4'] _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 3 -CLICK_TYPE_SINGLE = "single" -CLICK_TYPE_DOUBLE = "double" -CLICK_TYPE_HOLD = "hold" +CLICK_TYPE_SINGLE = 'single' +CLICK_TYPE_DOUBLE = 'double' +CLICK_TYPE_HOLD = 'hold' CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD] -CONF_IGNORED_CLICK_TYPES = "ignored_click_types" +CONF_IGNORED_CLICK_TYPES = 'ignored_click_types' -EVENT_NAME = "flic_click" -EVENT_DATA_NAME = "button_name" -EVENT_DATA_ADDRESS = "button_address" -EVENT_DATA_TYPE = "click_type" -EVENT_DATA_QUEUED_TIME = "queued_time" +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 5551 + +EVENT_NAME = 'flic_click' +EVENT_DATA_NAME = 'button_name' +EVENT_DATA_ADDRESS = 'button_address' +EVENT_DATA_TYPE = 'click_type' +EVENT_DATA_QUEUED_TIME = 'queued_time' -# Validation of the user's configuration PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default='localhost'): cv.string, - vol.Optional(CONF_PORT, default=5551): cv.port, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list, - [vol.In(CLICK_TYPES)]) + vol.Optional(CONF_IGNORED_CLICK_TYPES): + vol.All(cv.ensure_list, [vol.In(CLICK_TYPES)]) }) def setup_platform(hass, config, add_entities, discovery_info=None): - """Setup the flic platform.""" + """Set up the flic platform.""" import pyflic # Initialize flic client responsible for @@ -55,11 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: client = pyflic.FlicClient(host, port) except ConnectionRefusedError: - _LOGGER.error("Failed to connect to flic server.") + _LOGGER.error("Failed to connect to flic server") return def new_button_callback(address): - """Setup newly verified button as device in home assistant.""" + """Set up newly verified button as device in Home Assistant.""" setup_button(hass, config, add_entities, client, address) client.on_new_verified_button = new_button_callback @@ -74,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_info_callback(items): """Add entities for already verified buttons.""" - addresses = items["bd_addr_of_verified_buttons"] or [] + addresses = items['bd_addr_of_verified_buttons'] or [] for address in addresses: setup_button(hass, config, add_entities, client, address) @@ -83,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def start_scanning(config, add_entities, client): - """Start a new flic client for scanning & connceting to new buttons.""" + """Start a new flic client for scanning and connecting to new buttons.""" import pyflic scan_wizard = pyflic.ScanWizard() @@ -91,10 +97,10 @@ def start_scanning(config, add_entities, client): def scan_completed_callback(scan_wizard, result, address, name): """Restart scan wizard to constantly check for new buttons.""" if result == pyflic.ScanWizardResult.WizardSuccess: - _LOGGER.info("Found new button (%s)", address) + _LOGGER.info("Found new button %s", address) elif result != pyflic.ScanWizardResult.WizardFailedTimeout: - _LOGGER.warning("Failed to connect to button (%s). Reason: %s", - address, result) + _LOGGER.warning( + "Failed to connect to button %s. Reason: %s", address, result) # Restart scan wizard start_scanning(config, add_entities, client) @@ -108,7 +114,7 @@ def setup_button(hass, config, add_entities, client, address): timeout = config.get(CONF_TIMEOUT) ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES) button = FlicButton(hass, client, address, timeout, ignored_click_types) - _LOGGER.info("Connected to button (%s)", address) + _LOGGER.info("Connected to button %s", address) add_entities([button]) @@ -161,7 +167,7 @@ class FlicButton(BinarySensorDevice): @property def name(self): """Return the name of the device.""" - return "flic_%s" % self.address.replace(":", "") + return 'flic_{}'.format(self.address.replace(':', '')) @property def address(self): @@ -181,21 +187,21 @@ class FlicButton(BinarySensorDevice): @property def device_state_attributes(self): """Return device specific state attributes.""" - return {"address": self.address} + return {'address': self.address} def _queued_event_check(self, click_type, time_diff): """Generate a log message and returns true if timeout exceeded.""" time_string = "{:d} {}".format( - time_diff, "second" if time_diff == 1 else "seconds") + time_diff, 'second' if time_diff == 1 else 'seconds') if time_diff > self._timeout: _LOGGER.warning( - "Queued %s dropped for %s. Time in queue was %s.", + "Queued %s dropped for %s. Time in queue was %s", click_type, self.address, time_string) return True else: _LOGGER.info( - "Queued %s allowed for %s. Time in queue was %s.", + "Queued %s allowed for %s. Time in queue was %s", click_type, self.address, time_string) return False @@ -227,8 +233,8 @@ class FlicButton(BinarySensorDevice): EVENT_DATA_TYPE: hass_click_type }) - def _connection_status_changed(self, channel, - connection_status, disconnect_reason): + def _connection_status_changed( + self, channel, connection_status, disconnect_reason): """Remove device, if button disconnects.""" import pyflic diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py new file mode 100644 index 00000000000..cee5b81f00d --- /dev/null +++ b/homeassistant/components/binary_sensor/ping.py @@ -0,0 +1,130 @@ +""" +Tracks the latency of a host by sending ICMP echo requests (ping). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.ping/ +""" +import logging +import subprocess +import re +import sys +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME, CONF_HOST + +_LOGGER = logging.getLogger(__name__) + +ATTR_ROUND_TRIP_TIME_AVG = 'round_trip_time_avg' +ATTR_ROUND_TRIP_TIME_MAX = 'round_trip_time_max' +ATTR_ROUND_TRIP_TIME_MDEV = 'round_trip_time_mdev' +ATTR_ROUND_TRIP_TIME_MIN = 'round_trip_time_min' + +CONF_PING_COUNT = 'count' + +DEFAULT_NAME = 'Ping Binary sensor' +DEFAULT_PING_COUNT = 5 +DEFAULT_SENSOR_CLASS = 'connectivity' + +SCAN_INTERVAL = timedelta(minutes=5) + +PING_MATCHER = re.compile( + r'(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)\/(?P\d+.\d+)') + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): cv.positive_int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ping Binary sensor.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + count = config.get(CONF_PING_COUNT) + + add_devices([PingBinarySensor(name, PingData(host, count))], True) + + +class PingBinarySensor(BinarySensorDevice): + """Representation of a Ping Binary sensor.""" + + def __init__(self, name, ping): + """Initialize the Ping Binary sensor.""" + self._name = name + self.ping = ping + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEFAULT_SENSOR_CLASS + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.ping.available + + @property + def device_state_attributes(self): + """Return the state attributes of the ICMP checo request.""" + if self.ping.data is not False: + return { + ATTR_ROUND_TRIP_TIME_AVG: self.ping.data['avg'], + ATTR_ROUND_TRIP_TIME_MAX: self.ping.data['max'], + ATTR_ROUND_TRIP_TIME_MDEV: self.ping.data['mdev'], + ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], + } + + def update(self): + """Get the latest data.""" + self.ping.update() + + +class PingData(object): + """The Class for handling the data retrieval.""" + + def __init__(self, host, count): + """Initialize the data object.""" + self._ip_address = host + self._count = count + self.data = {} + self.available = False + + if sys.platform == 'win32': + self._ping_cmd = [ + 'ping', '-n', str(self._count), '-w 1000', self._ip_address] + else: + self._ping_cmd = [ + 'ping', '-n', '-q', '-c', str(self._count), '-W1', + self._ip_address] + + def ping(self): + """Send ICMP echo request and return details if success.""" + pinger = subprocess.Popen( + self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + out = pinger.communicate() + match = PING_MATCHER.search(str(out).split('\n')[-1]) + rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() + return { + 'min': rtt_min, + 'avg': rtt_avg, + 'max': rtt_max, + 'mdev': rtt_mdev} + except (subprocess.CalledProcessError, AttributeError): + return False + + def update(self): + """Retrieve the latest details from the host.""" + self.data = self.ping() + self.available = bool(self.data) diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 07deea02f6e..58bd411d758 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo.discovery as discovery if discovery_info is not None: - location = discovery_info[2] - mac = discovery_info[3] + location = discovery_info['ssdp_description'] + mac = discovery_info['mac_address'] device = discovery.device_from_description(location, mac) if device: @@ -40,12 +40,14 @@ class WemoBinarySensor(BinarySensorDevice): wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) - def _update_callback(self, _device, _params): - """Called by the wemo device callback to update state.""" + def _update_callback(self, _device, _type, _params): + """Called by the Wemo device callback to update state.""" _LOGGER.info( 'Subscription update for %s', _device) - self.update() + updated = self.wemo.subscription_update(_type, _params) + self._update(force_update=(not updated)) + if not hasattr(self, 'hass'): return self.schedule_update_ha_state() @@ -72,7 +74,11 @@ class WemoBinarySensor(BinarySensorDevice): def update(self): """Update WeMo state.""" + self._update(force_update=True) + + def _update(self, force_update=True): try: - self._state = self.wemo.get_state(True) - except AttributeError: - _LOGGER.warning('Could not update status for %s', self.name) + self._state = self.wemo.get_state(force_update) + except AttributeError as err: + _LOGGER.warning('Could not update status for %s (%s)', + self.name, err) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index a50cdc859a7..72d3120c77a 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_web) -REQUIREMENTS = ['amcrest==1.1.8'] +REQUIREMENTS = ['amcrest==1.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py new file mode 100755 index 00000000000..39fbdc7fd9f --- /dev/null +++ b/homeassistant/components/camera/mqtt.py @@ -0,0 +1,74 @@ +""" +Camera that loads a picture from an MQTT topic. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.mqtt/ +""" + +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_NAME +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_TOPIC = 'topic' + +DEFAULT_NAME = 'MQTT Camera' + +DEPENDENCIES = ['mqtt'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Camera.""" + topic = config[CONF_TOPIC] + + async_add_devices([MqttCamera(config[CONF_NAME], topic)]) + + +class MqttCamera(Camera): + """MQTT camera.""" + + def __init__(self, name, topic): + """Initialize Local File Camera component.""" + super().__init__() + + self._name = name + self._topic = topic + self._qos = 0 + self._last_image = None + + @asyncio.coroutine + def async_camera_image(self): + """Return image response.""" + return self._last_image + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + def async_added_to_hass(self): + """Subscribe mqtt events. + + This method must be run in the event loop and returns a coroutine. + """ + @callback + def message_received(topic, payload, qos): + """A new MQTT message has been received.""" + self._last_image = payload + + return mqtt.async_subscribe( + self.hass, self._topic, message_received, self._qos, None) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py new file mode 100644 index 00000000000..d6eafc36859 --- /dev/null +++ b/homeassistant/components/camera/neato.py @@ -0,0 +1,65 @@ +""" +Camera that loads a picture from a local file. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.neato/ +""" +import logging + +from datetime import timedelta +from homeassistant.components.camera import Camera +from homeassistant.components.neato import ( + NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Camera.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + if 'maps' in robot.traits: + dev.append(NeatoCleaningMap(hass, robot)) + _LOGGER.debug('Adding robots for cleaning maps %s', dev) + add_devices(dev, True) + + +class NeatoCleaningMap(Camera): + """Neato cleaning map for last clean.""" + + def __init__(self, hass, robot): + """Initialize Neato cleaning map.""" + super().__init__() + self.robot = robot + self._robot_name = self.robot.name + ' Cleaning Map' + self._robot_serial = self.robot.serial + self.neato = hass.data[NEATO_LOGIN] + self._image_url = None + self._image = None + + def camera_image(self): + """Return image response.""" + self.update() + return self._image + + @Throttle(timedelta(seconds=10)) + def update(self): + """Check the contents of the map list.""" + self.neato.update_robots() + image_url = None + map_data = self.hass.data[NEATO_MAP_DATA] + image_url = map_data[self._robot_serial]['maps'][0]['url'] + if image_url == self._image_url: + _LOGGER.debug('The map image_url is the same as old') + return + image = self.neato.download_map(image_url) + self._image = image.read() + self._image_url = image_url + + @property + def name(self): + """Return the name of this camera.""" + return self._robot_name diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index be38d468f2d..e419564a887 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -101,11 +101,17 @@ class ZoneMinderCamera(MjpegCamera): status_response = zoneminder.get_state( 'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id ) + if not status_response: _LOGGER.warning('Could not get status for monitor %i', self._monitor_id) return + if status_response['success'] is False: + _LOGGER.warning('Alarm status API call failed for monitor %i', + self._monitor_id) + return + self._is_recording = status_response['status'] == ZM_STATE_ALARM @property diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index d912f9914b5..3376815b9d5 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -180,7 +180,7 @@ class Configurator(object): # field validation goes here? - callback(call.data.get(ATTR_FIELDS, {})) + self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 3f26da183b5..f860e52de95 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -20,13 +20,13 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -def get_device(values, node_config, **kwargs): +def get_device(hass, values, node_config, **kwargs): """Create zwave entity device.""" invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS) if (values.primary.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0): - return ZwaveRollershutter(values, invert_buttons) + return ZwaveRollershutter(hass, values, invert_buttons) elif (values.primary.command_class in [ zwave.const.COMMAND_CLASS_SWITCH_BINARY, zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]): @@ -37,10 +37,11 @@ def get_device(values, node_config, **kwargs): class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): """Representation of an Zwave roller shutter.""" - def __init__(self, values, invert_buttons): + def __init__(self, hass, values, invert_buttons): """Initialize the zwave rollershutter.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) # pylint: disable=no-member + self._network = hass.data[zwave.ZWAVE_NETWORK] self._open_id = None self._close_id = None self._current_position = None @@ -90,11 +91,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def open_cover(self, **kwargs): """Move the roller shutter up.""" - zwave.NETWORK.manager.pressButton(self._open_id) + self._network.manager.pressButton(self._open_id) def close_cover(self, **kwargs): """Move the roller shutter down.""" - zwave.NETWORK.manager.pressButton(self._close_id) + self._network.manager.pressButton(self._close_id) def set_cover_position(self, position, **kwargs): """Move the roller shutter to a specific position.""" @@ -102,7 +103,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def stop_cover(self, **kwargs): """Stop the roller shutter.""" - zwave.NETWORK.manager.releaseButton(self._open_id) + self._network.manager.releaseButton(self._open_id) class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice): diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index e03cb72ea44..6212567f3d7 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -159,13 +159,13 @@ def async_setup(hass, config): tasks2.append(group.Group.async_create_group(hass, 'living room', [ lights[1], switches[0], 'input_select.living_room_preset', - 'rollershutter.living_room_window', media_players[1], + 'cover.living_room_window', media_players[1], 'scene.romantic_lights'])) tasks2.append(group.Group.async_create_group(hass, 'bedroom', [ lights[0], switches[1], media_players[0], 'input_slider.noise_allowance'])) tasks2.append(group.Group.async_create_group(hass, 'kitchen', [ - lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])) + lights[2], 'cover.kitchen_window', 'lock.kitchen_door'])) tasks2.append(group.Group.async_create_group(hass, 'doors', [ 'lock.front_door', 'lock.kitchen_door', 'garage_door.right_garage_door', 'garage_door.left_garage_door'])) @@ -176,8 +176,8 @@ def async_setup(hass, config): 'device_tracker.demo_paulus'])) tasks2.append(group.Group.async_create_group(hass, 'downstairs', [ 'group.living_room', 'group.kitchen', - 'scene.romantic_lights', 'rollershutter.kitchen_window', - 'rollershutter.living_room_window', 'group.doors', + 'scene.romantic_lights', 'cover.kitchen_window', + 'cover.living_room_window', 'group.doors', 'thermostat.ecobee', ], view=True)) diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 3b4612edf6c..53d49fd38d9 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -4,19 +4,20 @@ Support for the Automatic platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.automatic/ """ +import asyncio from datetime import timedelta import logging -import re -import requests import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, ATTR_ATTRIBUTES) from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_utc_time_change -from homeassistant.util import datetime as dt_util +from homeassistant.helpers.event import async_track_time_interval + +REQUIREMENTS = ['aioautomatic==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -24,129 +25,101 @@ CONF_CLIENT_ID = 'client_id' CONF_SECRET = 'secret' CONF_DEVICES = 'devices' -SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip' - -ATTR_ACCESS_TOKEN = 'access_token' -ATTR_EXPIRES_IN = 'expires_in' -ATTR_RESULTS = 'results' -ATTR_VEHICLE = 'vehicle' -ATTR_ENDED_AT = 'ended_at' -ATTR_END_LOCATION = 'end_location' - -URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/' -URL_VEHICLES = 'https://api.automatic.com/vehicle/' -URL_TRIPS = 'https://api.automatic.com/trip/' - -_VEHICLE_ID_REGEX = re.compile( - (URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/')) +DEFAULT_TIMEOUT = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_SECRET): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_DEVICES, default=None): vol.All( + cv.ensure_list, [cv.string]) }) -def setup_scanner(hass, config: dict, see, discovery_info=None): +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" + import aioautomatic + + client = aioautomatic.Client( + client_id=config[CONF_CLIENT_ID], + client_secret=config[CONF_SECRET], + client_session=async_get_clientsession(hass), + request_kwargs={'timeout': DEFAULT_TIMEOUT}) try: - AutomaticDeviceScanner(hass, config, see) - except requests.HTTPError as err: + session = yield from client.create_session_from_password( + config[CONF_USERNAME], config[CONF_PASSWORD]) + data = AutomaticData(hass, session, config[CONF_DEVICES], async_see) + except aioautomatic.exceptions.AutomaticError as err: _LOGGER.error(str(err)) return False + yield from data.update() return True -class AutomaticDeviceScanner(object): - """A class representing an Automatic device.""" +class AutomaticData(object): + """A class representing an Automatic cloud service connection.""" - def __init__(self, hass, config: dict, see) -> None: + def __init__(self, hass, session, devices, async_see): """Initialize the automatic device scanner.""" self.hass = hass - self._devices = config.get(CONF_DEVICES, None) - self._access_token_payload = { - 'username': config.get(CONF_USERNAME), - 'password': config.get(CONF_PASSWORD), - 'client_id': config.get(CONF_CLIENT_ID), - 'client_secret': config.get(CONF_SECRET), - 'grant_type': 'password', - 'scope': SCOPE - } - self._headers = None - self._token_expires = dt_util.now() - self.last_results = {} - self.last_trips = {} - self.see = see + self.devices = devices + self.session = session + self.async_see = async_see - self._update_info() + async_track_time_interval(hass, self.update, timedelta(seconds=30)) - track_utc_time_change(self.hass, self._update_info, - second=range(0, 60, 30)) - - def _update_headers(self): - """Get the access token from automatic.""" - if self._headers is None or self._token_expires <= dt_util.now(): - resp = requests.post( - URL_AUTHORIZE, - data=self._access_token_payload) - - resp.raise_for_status() - - json = resp.json() - - access_token = json[ATTR_ACCESS_TOKEN] - self._token_expires = dt_util.now() + timedelta( - seconds=json[ATTR_EXPIRES_IN]) - self._headers = { - 'Authorization': 'Bearer {}'.format(access_token) - } - - def _update_info(self, now=None) -> None: + @asyncio.coroutine + def update(self, now=None): """Update the device info.""" + import aioautomatic + _LOGGER.debug('Updating devices %s', now) - self._update_headers() - response = requests.get(URL_VEHICLES, headers=self._headers) + try: + vehicles = yield from self.session.get_vehicles() + except aioautomatic.exceptions.AutomaticError as err: + _LOGGER.error(str(err)) + return False - response.raise_for_status() + for vehicle in vehicles: + name = vehicle.display_name + if name is None: + name = ' '.join(filter(None, ( + str(vehicle.year), vehicle.make, vehicle.model))) - self.last_results = [item for item in response.json()[ATTR_RESULTS] - if self._devices is None or item[ - 'display_name'] in self._devices] + if self.devices is not None and name not in self.devices: + continue - response = requests.get(URL_TRIPS, headers=self._headers) + self.hass.async_add_job(self.update_vehicle(vehicle, name)) - if response.status_code == 200: - for trip in response.json()[ATTR_RESULTS]: - vehicle_id = _VEHICLE_ID_REGEX.match( - trip[ATTR_VEHICLE]).group(1) - if vehicle_id not in self.last_trips: - self.last_trips[vehicle_id] = trip - elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[ - ATTR_ENDED_AT]: - self.last_trips[vehicle_id] = trip + @asyncio.coroutine + def update_vehicle(self, vehicle, name): + """Updated the specified vehicle's data.""" + import aioautomatic - for vehicle in self.last_results: - dev_id = vehicle.get('id') - host_name = vehicle.get('display_name') + kwargs = { + 'dev_id': vehicle.id, + 'host_name': name, + 'mac': vehicle.id, + ATTR_ATTRIBUTES: { + 'fuel_level': vehicle.fuel_level_percent, + } + } - attrs = { - 'fuel_level': vehicle.get('fuel_level_percent') - } + trips = [] + try: + # Get the most recent trip for this vehicle + trips = yield from self.session.get_trips( + vehicle=vehicle.id, limit=1) + except aioautomatic.exceptions.AutomaticError as err: + _LOGGER.error(str(err)) - kwargs = { - 'dev_id': dev_id, - 'host_name': host_name, - 'mac': dev_id, - ATTR_ATTRIBUTES: attrs - } + if trips: + end_location = trips[0].end_location + kwargs['gps'] = (end_location.lat, end_location.lon) + kwargs['gps_accuracy'] = end_location.accuracy_m - if dev_id in self.last_trips: - end_location = self.last_trips[dev_id][ATTR_END_LOCATION] - kwargs['gps'] = (end_location['lat'], end_location['lon']) - kwargs['gps_accuracy'] = end_location['accuracy_m'] - - self.see(**kwargs) + yield from self.async_see(**kwargs) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 255440a18e1..668ee6dd8a0 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -22,20 +22,20 @@ URL = '/api/locative' def setup_scanner(hass, config, see, discovery_info=None): - """Setup an endpoint for the Locative application.""" + """Set up an endpoint for the Locative application.""" hass.http.register_view(LocativeView(see)) return True class LocativeView(HomeAssistantView): - """View to handle locative requests.""" + """View to handle Locative requests.""" url = URL name = 'api:locative' def __init__(self, see): - """Initialize Locative url endpoints.""" + """Initialize Locative URL endpoints.""" self.see = see @asyncio.coroutine @@ -52,7 +52,6 @@ class LocativeView(HomeAssistantView): return res @asyncio.coroutine - # pylint: disable=too-many-return-statements def _handle(self, hass, data): """Handle locative request.""" if 'latitude' not in data or 'longitude' not in data: diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py new file mode 100644 index 00000000000..da85055ba96 --- /dev/null +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -0,0 +1,85 @@ +""" +Support for GPS tracking MQTT enabled devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mqtt_json/ +""" +import asyncio +import json +import logging + +import voluptuous as vol + +import homeassistant.components.mqtt as mqtt +from homeassistant.core import callback +from homeassistant.components.mqtt import CONF_QOS +from homeassistant.components.device_tracker import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_DEVICES, ATTR_GPS_ACCURACY, ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_BATTERY_LEVEL) + +DEPENDENCIES = ['mqtt'] + +_LOGGER = logging.getLogger(__name__) + +GPS_JSON_PAYLOAD_SCHEMA = vol.Schema({ + vol.Required(ATTR_LATITUDE): vol.Coerce(float), + vol.Required(ATTR_LONGITUDE): vol.Coerce(float), + vol.Optional(ATTR_GPS_ACCURACY, default=None): vol.Coerce(int), + vol.Optional(ATTR_BATTERY_LEVEL, default=None): vol.Coerce(str), +}, extra=vol.ALLOW_EXTRA) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, +}) + + +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Setup the MQTT tracker.""" + devices = config[CONF_DEVICES] + qos = config[CONF_QOS] + + dev_id_lookup = {} + + @callback + def async_tracker_message_received(topic, payload, qos): + """MQTT message received.""" + dev_id = dev_id_lookup[topic] + + try: + data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload)) + except vol.MultipleInvalid: + _LOGGER.error('Skipping update for following data ' + 'because of missing or malformatted data: %s', + payload) + return + except ValueError: + _LOGGER.error('Error parsing JSON payload: %s', payload) + return + + kwargs = _parse_see_args(dev_id, data) + hass.async_add_job( + async_see(**kwargs)) + + for dev_id, topic in devices.items(): + dev_id_lookup[topic] = dev_id + yield from mqtt.async_subscribe( + hass, topic, async_tracker_message_received, qos) + + return True + + +def _parse_see_args(dev_id, data): + """Parse the payload location parameters, into the format see expects.""" + kwargs = { + 'gps': (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), + 'dev_id': dev_id + } + + if ATTR_GPS_ACCURACY in data: + kwargs[ATTR_GPS_ACCURACY] = data[ATTR_GPS_ACCURACY] + if ATTR_BATTERY_LEVEL in data: + kwargs['battery'] = data[ATTR_BATTERY_LEVEL] + return kwargs diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 04537dd6e4d..ee9a4d19d37 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -1,15 +1,8 @@ """ -Tracks devices by sending a ICMP ping. +Tracks devices by sending a ICMP echo request (ping). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.ping/ - -device_tracker: - - platform: ping - count: 2 - hosts: - host_one: pc.local - host_two: 192.168.2.25 """ import logging import subprocess @@ -18,14 +11,12 @@ from datetime import timedelta import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER) from homeassistant.helpers.event import track_point_in_utc_time from homeassistant import util from homeassistant import const -import homeassistant.helpers.config_validation as cv - -DEPENDENCIES = [] _LOGGER = logging.getLogger(__name__) @@ -37,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -class Host: +class Host(object): """Host object with ping detection.""" def __init__(self, ip_address, dev_id, hass, config): @@ -53,8 +44,10 @@ class Host: self.ip_address] def ping(self): - """Send ICMP ping and return True if success.""" - pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE) + """Send an ICMP echo request and return True if success.""" + pinger = subprocess.Popen(self._ping_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) try: pinger.communicate() return pinger.returncode == 0 @@ -70,7 +63,7 @@ class Host: return True failed += 1 - _LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed) + _LOGGER.debug("No response from %s failed=%d", self.ip_address, failed) def setup_scanner(hass, config, see, discovery_info=None): diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 891e34ee8a9..01b25cf0a87 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -10,6 +10,7 @@ import asyncio import json from datetime import timedelta import logging +import os import voluptuous as vol @@ -20,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==0.9.2'] +REQUIREMENTS = ['netdisco==1.0.0rc3'] DOMAIN = 'discovery' @@ -28,11 +29,15 @@ SCAN_INTERVAL = timedelta(seconds=300) SERVICE_NETGEAR = 'netgear_router' SERVICE_WEMO = 'belkin_wemo' SERVICE_HASS_IOS_APP = 'hass_ios' +SERVICE_IKEA_TRADFRI = 'ikea_tradfri' +SERVICE_HASSIO = 'hassio' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), SERVICE_WEMO: ('wemo', None), + SERVICE_IKEA_TRADFRI: ('tradfri', None), + SERVICE_HASSIO: ('hassio', None), 'philips_hue': ('light', 'hue'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), @@ -45,10 +50,10 @@ SERVICE_HANDLERS = { 'denonavr': ('media_player', 'denonavr'), 'samsung_tv': ('media_player', 'samsungtv'), 'yeelight': ('light', 'yeelight'), - 'flux_led': ('light', 'flux_led'), 'apple_tv': ('media_player', 'apple_tv'), 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), + 'bose_soundtouch': ('media_player', 'soundtouch'), } CONF_IGNORE = 'ignore' @@ -122,6 +127,10 @@ def async_setup(hass, config): """Schedule the first discovery when Home Assistant starts up.""" async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow()) + # discovery local services + if 'HASSIO' in os.environ: + hass.async_add_job(new_service_found(SERVICE_HASSIO, {})) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) return True diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 44563480cdb..c3bf78c7711 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,18 +3,19 @@ FINGERPRINTS = { "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", "core.js": "5d08475f03adb5969bd31855d5ca0cfd", - "frontend.html": "feaf3e9453eca239f29eb10e7645a84f", - "mdi.html": "989f02c51eba561dc32b9ecc628a84b3", + "frontend.html": "8264c0ee8dafb09785ec7b934795d3b1", + "mdi.html": "d86ee142ae2476f49384bfe866a2885e", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3", - "panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326", + "panels/ha-panel-config.html": "0b42cb4e709ce35ad2666ffeca6f9b14", + "panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", - "panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0", - "panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4", - "panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae", - "panels/ha-panel-history.html": "6945cebe5d8075aae560d2c4246b063f", + "panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", + "panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", + "panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", + "panels/ha-panel-history.html": "be115906882752d220199abbaddc53e5", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", - "panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71", - "panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6", + "panels/ha-panel-logbook.html": "bf29de0c586a598113c6cc09ead12b00", + "panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4", + "panels/ha-panel-zwave.html": "f52d0c001f48e0c7b33a172f3a71b547", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/compatibility.js.gz b/homeassistant/components/frontend/www_static/compatibility.js.gz index 77ca6c1bdac..f28e90335de 100644 Binary files a/homeassistant/components/frontend/www_static/compatibility.js.gz and b/homeassistant/components/frontend/www_static/compatibility.js.gz differ diff --git a/homeassistant/components/frontend/www_static/core.js.gz b/homeassistant/components/frontend/www_static/core.js.gz index 514c4f83bf6..e9b52fa52c7 100644 Binary files a/homeassistant/components/frontend/www_static/core.js.gz and b/homeassistant/components/frontend/www_static/core.js.gz differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index ce38aab6821..59d17b1c94b 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,7 +1,7 @@ - \ No newline at end of file +}()); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index f41ff60ac76..ccc0ffb90ff 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index abbdc6f0555..3fdba359865 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit abbdc6f055524c5d3ed0bb50e35400fed40d573f +Subproject commit 3fdba359865823805e8ea756c8500d3913976158 diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index afb3f013acf..8a4662d482f 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index 638f13140e4..78f55cb811f 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html index 62a9e31885b..bbe911e4959 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html @@ -1,4 +1,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz index 4af3bed93ba..3d3ac6f70aa 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz index c35637c53a8..50b91dba116 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index b650277a15b..887d95325d6 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index c094f8d9fb0..5ac8c8c977c 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html index 1cc4b362708..37227fb4d52 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz index b9dea8501fe..bc975d427c0 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html index 12bfbb395cb..d905cc92a78 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index d663a683324..6ec343e910f 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html index 5d1f6163383..507856d98ce 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz index 4d7d1ec9bad..50006b9bb42 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html index 3cdf5654da8..d3a23fba566 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz index 728de3ce443..49588c57be3 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html index 013f27cf9b9..dbc34a17448 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -1,8 +1,8 @@ - \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz new file mode 100644 index 00000000000..5696f89a832 Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 8f2d6161e73..a72ef49fd22 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","e908e36c8b741daeb2387aab993051a0"],["/frontend/panels/dev-event-1f169700c2345785855b1d7919d12326.html","3975cb76bc0b7b1e26416ca6d1d8e716"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-0fe8e6acdccf2dc3d1ae657b2c7f2df0.html","e1f927a20bc1e8ee0dde85e8173539bc"],["/frontend/panels/dev-state-48d37db4a1d6708314ded1d624d0f4d4.html","ca4fa5299da2a6c7e595cef38a5cee31"],["/frontend/panels/dev-template-6f353392d68574fbc5af188bca44d0ae.html","16e3e7de4dcb93b9cb05486adef915a3"],["/frontend/panels/map-e3c7a54f90dd4269d7e53cdcd96514c6.html","aa36ba71ab2ff533f7fd3d0a4fddbdeb"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-5d08475f03adb5969bd31855d5ca0cfd.js","1cd99ba798bfcff9768c9d2bb2f58a7c"],["/static/frontend-feaf3e9453eca239f29eb10e7645a84f.html","286783bbce5966d43fe56c50cbc338b7"],["/static/mdi-4921d26f29dc148c3e8bd5bcd8ce5822.html","14b01c4c4c7a06d156acd83862082ab3"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n 0: + transition = int(1000 * random.uniform(period/2, period)) + + if ATTR_BRIGHTNESS in kwargs: + brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS]) + else: + brightness = light.effect_data.color[2] + + hsbk = [ + int(65535/359*lhue), + int(random.uniform(0.8, 1.0)*65535), + brightness, + 4000, + ] + light.device.set_color(hsbk, None, transition) + + # Adjust the next light so the full spread is used + if len(self.lights) > 1: + lhue = (lhue + spread/(len(self.lights)-1)) % 360 + + yield from asyncio.sleep(period) + + def from_poweroff_hsbk(self, light, **kwargs): + """Start from a random hue.""" + return [random.randint(0, 65535), 65535, 0, 4000] + + +class LIFXEffectStop(LIFXEffect): + """A no-op effect, but starting it will stop an existing effect.""" + + def __init__(self, hass, lights): + """Initialize the stop effect.""" + super(LIFXEffectStop, self).__init__(hass, lights) + self.name = SERVICE_EFFECT_STOP + + @asyncio.coroutine + def async_perform(self, **kwargs): + """Do nothing.""" + yield None diff --git a/homeassistant/components/light/lifx/services.yaml b/homeassistant/components/light/lifx/services.yaml new file mode 100644 index 00000000000..1b34c54f253 --- /dev/null +++ b/homeassistant/components/light/lifx/services.yaml @@ -0,0 +1,99 @@ +lifx_effect_breathe: + description: Run a breathe effect by fading to a color and back. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.kitchen' + + brightness: + description: Number between 0..255 indicating brightness when the effect peaks + example: 120 + + color_name: + description: A human readable color name + example: 'red' + + rgb_color: + description: Color for the fade in RGB-format + example: '[255, 100, 100]' + + period: + description: Duration of the effect in seconds (default 1.0) + example: 3 + + cycles: + description: Number of times the effect should run (default 1.0) + example: 2 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_pulse: + description: Run a flash effect by changing to a color and back. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.kitchen' + + brightness: + description: Number between 0..255 indicating brightness of the temporary color + example: 120 + + color_name: + description: A human readable color name + example: 'red' + + rgb_color: + description: The temporary color in RGB-format + example: '[255, 100, 100]' + + period: + description: Duration of the effect in seconds (default 1.0) + example: 3 + + cycles: + description: Number of times the effect should run (default 1.0) + example: 2 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_colorloop: + description: Run an effect with looping colors. + + fields: + entity_id: + description: Name(s) of entities to run the effect on + example: 'light.disco1, light.disco2, light.disco3' + + brightness: + description: Number between 0..255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light + example: 120 + + period: + description: Duration between color changes (deafult 60) + example: 180 + + change: + description: Hue movement per period, in degrees on a color wheel (default 20) + example: 45 + + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (default 30) + example: 0 + + power_on: + description: Powered off lights are temporarily turned on during the effect (default True) + example: False + +lifx_effect_stop: + description: Stop a running effect. + + fields: + entity_id: + description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. + example: 'light.bedroom' diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py index 36742950b9c..5322fd79489 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/light/lutron.py @@ -14,12 +14,9 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Lutron lights.""" - area_devs = {} devs = [] for (area_name, device) in hass.data[LUTRON_DEVICES]['light']: - dev = LutronLight(hass, area_name, device, - hass.data[LUTRON_CONTROLLER]) - area_devs.setdefault(area_name, []).append(dev) + dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER]) devs.append(dev) add_devices(devs, True) @@ -39,10 +36,10 @@ def to_hass_level(level): class LutronLight(LutronDevice, Light): """Representation of a Lutron Light, including dimmable.""" - def __init__(self, hass, area_name, lutron_device, controller): + def __init__(self, area_name, lutron_device, controller): """Initialize the light.""" self._prev_brightness = None - LutronDevice.__init__(self, hass, area_name, lutron_device, controller) + LutronDevice.__init__(self, area_name, lutron_device, controller) @property def supported_features(self): diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py index e77501e8ce4..705886855b0 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -42,7 +42,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light): def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs and self._device_type == "WallDimmer": + if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] else: brightness = 255 diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py new file mode 100644 index 00000000000..e29535d60e6 --- /dev/null +++ b/homeassistant/components/light/mystrom.py @@ -0,0 +1,121 @@ +""" +Support for myStrom Wifi bulbs. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.mystrom/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-mystrom==0.3.8'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'myStrom bulb' + +SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the myStrom Light platform.""" + from pymystrom import MyStromBulb + from pymystrom.exceptions import MyStromConnectionError + + host = config.get(CONF_HOST) + mac = config.get(CONF_MAC) + name = config.get(CONF_NAME) + + bulb = MyStromBulb(host, mac) + try: + if bulb.get_status()['type'] != 'rgblamp': + _LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac) + return False + except MyStromConnectionError: + _LOGGER.warning("myStrom bulb not online") + + add_devices([MyStromLight(bulb, name)], True) + + +class MyStromLight(Light): + """Representation of the myStrom WiFi Bulb.""" + + def __init__(self, bulb, name): + """Initialize the light.""" + self._bulb = bulb + self._name = name + self._state = None + self._available = False + self._brightness = 0 + self._rgb_color = [0, 0, 0] + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_MYSTROM + + @property + def brightness(self): + """Brightness of the light.""" + return self._brightness + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def is_on(self): + """Return true if light is on.""" + return self._state['on'] if self._state is not None else STATE_UNKNOWN + + def turn_on(self, **kwargs): + """Turn on the light.""" + from pymystrom.exceptions import MyStromConnectionError + + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + + try: + if not self.is_on: + self._bulb.set_on() + if brightness is not None: + self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255)) + except MyStromConnectionError: + _LOGGER.warning("myStrom bulb not online") + + def turn_off(self, **kwargs): + """Turn off the bulb.""" + from pymystrom.exceptions import MyStromConnectionError + + try: + self._bulb.set_off() + except MyStromConnectionError: + _LOGGER.warning("myStrom bulb not online") + + def update(self): + """Fetch new state data for this light.""" + from pymystrom.exceptions import MyStromConnectionError + + try: + self._state = self._bulb.get_status() + self._brightness = int(self._bulb.get_brightness()) * 255 / 100 + self._available = True + except MyStromConnectionError: + _LOGGER.warning("myStrom bulb not online") + self._available = False diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py new file mode 100644 index 00000000000..c8ba110c58a --- /dev/null +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -0,0 +1,213 @@ +""" +Support for LED lights that can be controlled using PWM. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.pwm/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.components.light import ( + Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pwmled==1.1.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_LEDS = 'leds' +CONF_DRIVER = 'driver' +CONF_PINS = 'pins' +CONF_FREQUENCY = 'frequency' +CONF_ADDRESS = 'address' + +CONF_DRIVER_GPIO = 'gpio' +CONF_DRIVER_PCA9685 = 'pca9685' +CONF_DRIVER_TYPES = [CONF_DRIVER_GPIO, CONF_DRIVER_PCA9685] + +CONF_LED_TYPE_SIMPLE = 'simple' +CONF_LED_TYPE_RGB = 'rgb' +CONF_LED_TYPE_RGBW = 'rgbw' +CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] + +DEFAULT_COLOR = [255, 255, 255] + +SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) +SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [ + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), + vol.Required(CONF_PINS): vol.All(cv.ensure_list, + [cv.positive_int]), + vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), + vol.Optional(CONF_FREQUENCY): cv.positive_int, + vol.Optional(CONF_ADDRESS): cv.byte + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the pwm lights.""" + from pwmled.led import SimpleLed + from pwmled.led.rgb import RgbLed + from pwmled.led.rgbw import RgbwLed + from pwmled.driver.gpio import GpioDriver + from pwmled.driver.pca9685 import Pca9685Driver + + leds = [] + for led_conf in config[CONF_LEDS]: + driver_type = led_conf[CONF_DRIVER] + pins = led_conf[CONF_PINS] + opt_args = {} + if CONF_FREQUENCY in led_conf: + opt_args['freq'] = led_conf[CONF_FREQUENCY] + if driver_type == CONF_DRIVER_GPIO: + driver = GpioDriver(pins, **opt_args) + elif driver_type == CONF_DRIVER_PCA9685: + if CONF_ADDRESS in led_conf: + opt_args['address'] = led_conf[CONF_ADDRESS] + driver = Pca9685Driver(pins, **opt_args) + else: + _LOGGER.error("Invalid driver type.") + return + + name = led_conf[CONF_NAME] + led_type = led_conf[CONF_TYPE] + if led_type == CONF_LED_TYPE_SIMPLE: + led = PwmSimpleLed(SimpleLed(driver), name) + elif led_type == CONF_LED_TYPE_RGB: + led = PwmRgbLed(RgbLed(driver), name) + elif led_type == CONF_LED_TYPE_RGBW: + led = PwmRgbLed(RgbwLed(driver), name) + else: + _LOGGER.error("Invalid led type.") + return + leds.append(led) + + add_devices(leds) + + +class PwmSimpleLed(Light): + """Representation of a simple on-color pwm led.""" + + def __init__(self, led, name): + """Initialize led.""" + self._led = led + self._name = name + self._is_on = False + self._brightness = 255 + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the group.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness property.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SIMPLE_LED + + def turn_on(self, **kwargs): + """Turn on a led.""" + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_TRANSITION in kwargs: + transition_time = kwargs[ATTR_TRANSITION] + self._led.transition( + transition_time, + is_on=True, + brightness=_from_hass_brightness(self._brightness)) + else: + self._led.set(is_on=True, + brightness=_from_hass_brightness(self._brightness)) + + self._is_on = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn off a led.""" + if self.is_on: + if ATTR_TRANSITION in kwargs: + transition_time = kwargs[ATTR_TRANSITION] + self._led.transition(transition_time, is_on=False) + else: + self._led.off() + + self._is_on = False + self.schedule_update_ha_state() + + +class PwmRgbLed(PwmSimpleLed): + """Representation of a rgb(w) pwm led.""" + + def __init__(self, led, name): + """Initialize led.""" + super().__init__(led, name) + self._color = DEFAULT_COLOR + + @property + def rgb_color(self): + """Return the color property.""" + return self._color + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_RGB_LED + + def turn_on(self, **kwargs): + """Turn on a led.""" + if ATTR_RGB_COLOR in kwargs: + self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_TRANSITION in kwargs: + transition_time = kwargs[ATTR_TRANSITION] + self._led.transition( + transition_time, + is_on=True, + brightness=_from_hass_brightness(self._brightness), + color=_from_hass_color(self._color)) + else: + self._led.set(is_on=True, + brightness=_from_hass_brightness(self._brightness), + color=_from_hass_color(self._color)) + + self._is_on = True + self.schedule_update_ha_state() + + +def _from_hass_brightness(brightness): + """Convert Home Assistant brightness units to percentage.""" + return brightness / 255 + + +def _from_hass_color(color): + """Convert Home Assistant RGB list to Color tuple.""" + from pwmled import Color + return Color(*tuple(color)) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py new file mode 100644 index 00000000000..9c0284c22f6 --- /dev/null +++ b/homeassistant/components/light/tradfri.py @@ -0,0 +1,135 @@ +"""Support for the IKEA Tradfri platform.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) +from homeassistant.components.light import \ + PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA +from homeassistant.components.tradfri import KEY_GATEWAY +from homeassistant.util import color as color_util + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tradfri'] +PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA +IKEA = 'IKEA of Sweden' +ALLOWED_TEMPERATURES = { + IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'} +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the IKEA Tradfri Light platform.""" + if discovery_info is None: + return + + gateway_id = discovery_info['gateway'] + gateway = hass.data[KEY_GATEWAY][gateway_id] + devices = gateway.get_devices() + lights = [dev for dev in devices if dev.has_light_control] + add_devices(Tradfri(light) for light in lights) + + +class Tradfri(Light): + """The platform class required by hass.""" + + def __init__(self, light): + """Initialize a Light.""" + self._light = light + + # Caching of LightControl and light object + self._light_control = light.light_control + self._light_data = light.light_control.lights[0] + self._name = light.name + self._rgb_color = None + self._features = SUPPORT_BRIGHTNESS + + if self._light_data.hex_color is not None: + if self._light.device_info.manufacturer == IKEA: + self._features |= SUPPORT_COLOR_TEMP + else: + self._features |= SUPPORT_RGB_COLOR + + self._ok_temps = ALLOWED_TEMPERATURES.get( + self._light.device_info.manufacturer) + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def is_on(self): + """Return true if light is on.""" + return self._light_data.state + + @property + def brightness(self): + """Brightness of the light (an integer in the range 1-255).""" + return self._light_data.dimmer + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + if (self._light_data.hex_color is None or + self.supported_features & SUPPORT_COLOR_TEMP == 0 or + not self._ok_temps): + return None + + kelvin = next(( + kelvin for kelvin, hex_color in self._ok_temps.items() + if hex_color == self._light_data.hex_color), None) + if kelvin is None: + _LOGGER.error( + 'unexpected color temperature found for %s: %s', + self.name, self._light_data.hex_color) + return + return color_util.color_temperature_kelvin_to_mired(kelvin) + + @property + def rgb_color(self): + """RGB color of the light.""" + return self._rgb_color + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + return self._light_control.set_state(False) + + def turn_on(self, **kwargs): + """ + Instruct the light to turn on. + + After adding "self._light_data.hexcolor is not None" + for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. + """ + if ATTR_BRIGHTNESS in kwargs: + self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS]) + else: + self._light_control.set_state(True) + + if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: + self._light.light_control.set_hex_color( + color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])) + + elif ATTR_COLOR_TEMP in kwargs and \ + self._light_data.hex_color is not None and self._ok_temps: + kelvin = color_util.color_temperature_mired_to_kelvin( + kwargs[ATTR_COLOR_TEMP]) + # find closest allowed kelvin temp from user input + kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin)) + self._light_control.set_hex_color(self._ok_temps[kelvin]) + + def update(self): + """Fetch new state data for this light.""" + self._light.update() + + # Handle Hue lights paired with the gatway + if self._light_data.hex_color is not None: + self._rgb_color = color_util.rgb_hex_to_rgb_list( + self._light_data.hex_color) diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 41143246931..02106511fe2 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -30,8 +30,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import pywemo.discovery as discovery if discovery_info is not None: - location = discovery_info[2] - mac = discovery_info[3] + location = discovery_info['ssdp_description'] + mac = discovery_info['mac_address'] device = discovery.device_from_description(location, mac) if device: diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 676775aace5..9a3e2e34fcc 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -128,11 +128,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) + network = hass.data[zwave.ZWAVE_NETWORK] def set_usercode(service): """Set the usercode to index X on the lock.""" node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = zwave.NETWORK.nodes[node_id] + lock_node = network.nodes[node_id] code_slot = service.data.get(ATTR_CODE_SLOT) usercode = service.data.get(ATTR_USERCODE) @@ -151,7 +152,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def get_usercode(service): """Get a usercode at index X on the lock.""" node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = zwave.NETWORK.nodes[node_id] + lock_node = network.nodes[node_id] code_slot = service.data.get(ATTR_CODE_SLOT) for value in lock_node.get_values( @@ -164,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def clear_usercode(service): """Set usercode to slot X on the lock.""" node_id = service.data.get(zwave.const.ATTR_NODE_ID) - lock_node = zwave.NETWORK.nodes[node_id] + lock_node = network.nodes[node_id] code_slot = service.data.get(ATTR_CODE_SLOT) data = '' diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index a428ea10cfa..3bd1682bc41 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -4,6 +4,7 @@ Component for interacting with a Lutron RadioRA 2 system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lutron/ """ +import asyncio import logging from homeassistant.helpers import discovery @@ -50,16 +51,19 @@ def setup(hass, base_config): class LutronDevice(Entity): """Representation of a Lutron device entity.""" - def __init__(self, hass, area_name, lutron_device, controller): + def __init__(self, area_name, lutron_device, controller): """Initialize the device.""" self._lutron_device = lutron_device self._controller = controller self._area_name = area_name - self.hass = hass - self.object_id = '{} {}'.format(area_name, lutron_device.name) - - self._controller.subscribe(self._lutron_device, self._update_callback) + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + self.hass.async_add_job( + self._controller.subscribe, self._lutron_device, + self._update_callback + ) def _update_callback(self, _device): """Callback invoked by pylutron when the device state changes.""" @@ -68,7 +72,7 @@ class LutronDevice(Entity): @property def name(self): """Return the name of the device.""" - return self._lutron_device.name + return "{} {}".format(self._area_name, self._lutron_device.name) @property def should_poll(self): diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 416c9f7fd00..1e19083e959 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -10,15 +10,13 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_HOST, - CONF_USERNAME, - CONF_PASSWORD) +from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity REQUIREMENTS = ['https://github.com/gurumitts/' - 'pylutron-caseta/archive/v0.2.5.zip#' - 'pylutron-caseta==v0.2.5'] + 'pylutron-caseta/archive/v0.2.6.zip#' + 'pylutron-caseta==v0.2.6'] _LOGGER = logging.getLogger(__name__) @@ -28,9 +26,7 @@ DOMAIN = 'lutron_caseta' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_HOST): cv.string }) }, extra=vol.ALLOW_EXTRA) @@ -41,9 +37,7 @@ def setup(hass, base_config): config = base_config.get(DOMAIN) hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge( - hostname=config[CONF_HOST], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD] + hostname=config[CONF_HOST] ) if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected(): _LOGGER.error("Unable to connect to Lutron smartbridge at %s", @@ -71,14 +65,17 @@ class LutronCasetaDevice(Entity): self._device_id = device["device_id"] self._device_type = device["type"] self._device_name = device["name"] + self._device_zone = device["zone"] self._state = None self._smartbridge = bridge @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" - self._smartbridge.add_subscriber(self._device_id, - self._update_callback) + self.hass.async_add_job( + self._smartbridge.add_subscriber, self._device_id, + self._update_callback + ) def _update_callback(self): self.schedule_update_ha_state() @@ -91,7 +88,8 @@ class LutronCasetaDevice(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - attr = {'Lutron Integration ID': self._device_id} + attr = {'Device ID': self._device_id, + 'Zone ID': self._device_zone} return attr @property diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 7a865cd2d8a..580870cf375 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -51,7 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if discovery_info is not None: name = discovery_info['name'] host = discovery_info['host'] - login_id = discovery_info['hsgid'] + login_id = discovery_info['properties']['hG'] start_off = False else: name = config.get(CONF_NAME) @@ -145,6 +145,8 @@ class AppleTvDevice(MediaPlayerDevice): @callback def playstatus_update(self, updater, playing): """Print what is currently playing when it changes.""" + self._playing = playing + if self.state == STATE_IDLE: self._artwork_hash = None elif self._has_playing_media_changed(playing): @@ -153,7 +155,6 @@ class AppleTvDevice(MediaPlayerDevice): self._artwork_hash = hashlib.md5( base.encode('utf-8')).hexdigest() - self._playing = playing self.hass.async_add_job(self.async_update_ha_state()) def _has_playing_media_changed(self, new_playing): diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index d6e7261ec4f..fbfc207c59a 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/aparraga/braviarc/archive/0.3.6.zip' - '#braviarc==0.3.6'] + 'https://github.com/aparraga/braviarc/archive/0.3.7.zip' + '#braviarc==0.3.7'] BRAVIA_CONFIG_FILE = 'bravia.conf' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index c2aa8a0bcca..148cdee1d48 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -51,11 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hosts = [] - if discovery_info and discovery_info in KNOWN_HOSTS: - return + if discovery_info: + host = (discovery_info.get('host'), discovery_info.get('port')) - elif discovery_info: - hosts = [discovery_info] + if host in KNOWN_HOSTS: + return + + hosts = [host] elif CONF_HOST in config: hosts = [(config.get(CONF_HOST), DEFAULT_PORT)] diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index eba2d031158..5e91a5418e2 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -64,8 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.info("Denon receiver at host %s initialized", host) # 2. option: discovery using netdisco if discovery_info is not None: - host = discovery_info[0] - name = discovery_info[1] + host = discovery_info.get('host') + name = discovery_info.get('name') # Check if host not in cache, append it and save for later starting if host not in cache: cache.add(host) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 035fe6d9cd6..ef3333d4da3 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -37,14 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the DirecTV platform.""" hosts = [] - if discovery_info and discovery_info in KNOWN_HOSTS: - return + if discovery_info: + host = discovery_info.get('host') + + if host in KNOWN_HOSTS: + return - if discovery_info is not None: hosts.append([ - 'DirecTV_' + discovery_info[1], - discovery_info[0], - DEFAULT_PORT + 'DirecTV_' + discovery_info.get('serial', ''), + host, DEFAULT_PORT ]) elif CONF_HOST in config: diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index 8bd36b4535c..f46d0657604 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -47,7 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is not None: add_devices( - [FSAPIDevice(discovery_info, DEFAULT_PASSWORD)], + [FSAPIDevice(discovery_info['ssdp_description'], + DEFAULT_PASSWORD)], update_before_add=True) return True diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py index b74af4a4ddb..f9c20c4d4ea 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -9,9 +9,9 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PLAY_MEDIA, - SUPPORT_PLAY, SUPPORT_NEXT_TRACK, PLATFORM_SCHEMA, MediaPlayerDevice) + MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PAUSE, + SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, SUPPORT_NEXT_TRACK, + PLATFORM_SCHEMA, MediaPlayerDevice) from homeassistant.const import ( STATE_IDLE, CONF_NAME, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv @@ -20,14 +20,13 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['gstreamer-player==1.0.0'] +REQUIREMENTS = ['gstreamer-player==1.1.0'] DOMAIN = 'gstreamer' CONF_PIPELINE = 'pipeline' -SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_SEEK | SUPPORT_STOP | \ - SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_NEXT_TRACK +SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PAUSE |\ + SUPPORT_PLAY_MEDIA | SUPPORT_NEXT_TRACK PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, @@ -61,7 +60,6 @@ class GstreamerDevice(MediaPlayerDevice): self._state = STATE_IDLE self._volume = None self._duration = None - self._position = None self._uri = None self._title = None self._artist = None @@ -72,16 +70,11 @@ class GstreamerDevice(MediaPlayerDevice): self._state = self._player.state self._volume = self._player.volume self._duration = self._player.duration - self._position = self._player.position self._uri = self._player.uri self._title = self._player.title self._album = self._player.album self._artist = self._player.artist - def mute_volume(self, mute): - """Send the mute command.""" - self._player.mute() - def set_volume_level(self, volume): """Set the volume level.""" self._player.volume = volume @@ -93,9 +86,13 @@ class GstreamerDevice(MediaPlayerDevice): return self._player.queue(media_id) - def media_seek(self, position): - """Seek.""" - self._player.position = position + def media_play(self): + """Play.""" + self._player.play() + + def media_pause(self): + """Pause.""" + self._player.pause() def media_next_track(self): """Next track.""" @@ -121,11 +118,6 @@ class GstreamerDevice(MediaPlayerDevice): """Return the volume level.""" return self._volume - @property - def is_volume_muted(self): - """Volume muted.""" - return self._volume == 0 - @property def supported_features(self): """Flag media player features that are supported.""" @@ -141,11 +133,6 @@ class GstreamerDevice(MediaPlayerDevice): """Duration of current playing media in seconds.""" return self._duration - @property - def media_position(self): - """Position of current playing media in seconds.""" - return self._position - @property def media_title(self): """Media title.""" diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index a137a332f7e..e10886d6916 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -8,6 +8,7 @@ import asyncio from functools import wraps import logging import urllib +import re import aiohttp import voluptuous as vol @@ -17,7 +18,7 @@ from homeassistant.components.media_player import ( SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, - MEDIA_TYPE_PLAYLIST) + MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, @@ -76,6 +77,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ cv.boolean, }) +SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' +SERVICE_SET_SHUFFLE = 'kodi_set_shuffle' + +ATTR_MEDIA_TYPE = 'media_type' +ATTR_MEDIA_NAME = 'media_name' +ATTR_MEDIA_ARTIST_NAME = 'artist_name' +ATTR_MEDIA_ID = 'media_id' + +MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required('shuffle_on'): cv.boolean, +}) + +MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_MEDIA_TYPE): cv.string, + vol.Optional(ATTR_MEDIA_ID): cv.string, + vol.Optional(ATTR_MEDIA_NAME): cv.string, + vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, +}) + +SERVICE_TO_METHOD = { + SERVICE_ADD_MEDIA: { + 'method': 'async_add_media_to_playlist', + 'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA}, + SERVICE_SET_SHUFFLE: { + 'method': 'async_set_shuffle', + 'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA}, +} + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -103,6 +132,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices([entity], update_before_add=True) + @asyncio.coroutine + def async_service_handler(service): + """Map services to methods on MediaPlayerDevice.""" + method = SERVICE_TO_METHOD.get(service.service) + if not method: + return + + params = {key: value for key, value in service.data.items() + if key != 'entity_id'} + + yield from getattr(entity, method['method'])(**params) + + update_tasks = [] + if entity.should_poll: + update_coro = entity.async_update_ha_state(True) + update_tasks.append(update_coro) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service].get( + 'schema', MEDIA_PLAYER_SCHEMA) + hass.services.async_register( + DOMAIN, service, async_service_handler, + description=None, schema=schema) + def cmd(func): """Decorator to catch command exceptions.""" @@ -593,6 +649,139 @@ class KodiDevice(MediaPlayerDevice): if media_type == "CHANNEL": return self.server.Player.Open( {"item": {"channelid": int(media_id)}}) + elif media_type == "PLAYLIST": + return self.server.Player.Open( + {"item": {"playlistid": int(media_id)}}) else: return self.server.Player.Open( {"item": {"file": str(media_id)}}) + + @asyncio.coroutine + def async_set_shuffle(self, shuffle_on): + """Set shuffle mode, for the first player.""" + if len(self._players) < 1: + raise RuntimeError("Error: No active player.") + yield from self.server.Player.SetShuffle( + {"playerid": self._players[0]['playerid'], "shuffle": shuffle_on}) + + @asyncio.coroutine + def async_add_media_to_playlist( + self, media_type, media_id=None, media_name='', artist_name=''): + """Add a media to default playlist (i.e. playlistid=0). + + First the media type must be selected, then + the media can be specified in terms of id or + name and optionally artist name. + All the albums of an artist can be added with + media_name="ALL" + """ + if media_type == "SONG": + if media_id is None: + media_id = yield from self.async_find_song( + media_name, artist_name) + + yield from self.server.Playlist.Add( + {"playlistid": 0, "item": {"songid": int(media_id)}}) + + elif media_type == "ALBUM": + if media_id is None: + if media_name == "ALL": + yield from self.async_add_all_albums(artist_name) + return + + media_id = yield from self.async_find_album( + media_name, artist_name) + + yield from self.server.Playlist.Add( + {"playlistid": 0, "item": {"albumid": int(media_id)}}) + else: + raise RuntimeError("Unrecognized media type.") + + @asyncio.coroutine + def async_add_all_albums(self, artist_name): + """Add all albums of an artist to default playlist (i.e. playlistid=0). + + The artist is specified in terms of name. + """ + artist_id = yield from self.async_find_artist(artist_name) + + albums = yield from self.async_get_albums(artist_id) + + for alb in albums['albums']: + yield from self.server.Playlist.Add( + {"playlistid": 0, "item": {"albumid": int(alb['albumid'])}}) + + @asyncio.coroutine + def async_clear_playlist(self): + """Clear default playlist (i.e. playlistid=0).""" + return self.server.Playlist.Clear({"playlistid": 0}) + + @asyncio.coroutine + def async_get_artists(self): + """Get artists list.""" + return (yield from self.server.AudioLibrary.GetArtists()) + + @asyncio.coroutine + def async_get_albums(self, artist_id=None): + """Get albums list.""" + if artist_id is None: + return (yield from self.server.AudioLibrary.GetAlbums()) + else: + return (yield from self.server.AudioLibrary.GetAlbums( + {"filter": {"artistid": int(artist_id)}})) + + @asyncio.coroutine + def async_find_artist(self, artist_name): + """Find artist by name.""" + artists = yield from self.async_get_artists() + out = self._find( + artist_name, [a['artist'] for a in artists['artists']]) + return artists['artists'][out[0][0]]['artistid'] + + @asyncio.coroutine + def async_get_songs(self, artist_id=None): + """Get songs list.""" + if artist_id is None: + return (yield from self.server.AudioLibrary.GetSongs()) + else: + return (yield from self.server.AudioLibrary.GetSongs( + {"filter": {"artistid": int(artist_id)}})) + + @asyncio.coroutine + def async_find_song(self, song_name, artist_name=''): + """Find song by name and optionally artist name.""" + artist_id = None + if artist_name != '': + artist_id = yield from self.async_find_artist(artist_name) + + songs = yield from self.async_get_songs(artist_id) + if songs['limits']['total'] == 0: + return None + + out = self._find(song_name, [a['label'] for a in songs['songs']]) + return songs['songs'][out[0][0]]['songid'] + + @asyncio.coroutine + def async_find_album(self, album_name, artist_name=''): + """Find album by name and optionally artist name.""" + artist_id = None + if artist_name != '': + artist_id = yield from self.async_find_artist(artist_name) + + albums = yield from self.async_get_albums(artist_id) + out = self._find(album_name, [a['label'] for a in albums['albums']]) + return albums['albums'][out[0][0]]['albumid'] + + @staticmethod + def _find(key_word, words): + key_word = key_word.split(' ') + patt = [re.compile( + '(^| )' + k + '( |$)', re.IGNORECASE) for k in key_word] + + out = [[i, 0] for i in range(len(words))] + for i in range(len(words)): + mtc = [p.search(words[i]) for p in patt] + rate = [m is not None for m in mtc].count(True) + out[i][1] = rate + + return sorted(out, key=lambda out: out[1], reverse=True) diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index af58b4cb654..c70822381a4 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -31,21 +31,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Openhome Platform.""" from openhomedevice.Device import Device - if discovery_info: - _LOGGER.info('Openhome device found, (%s)', discovery_info[0]) - device = Device(discovery_info[1]) - - # if device has already been discovered - if device.Uuid() in [x.unique_id for x in DEVICES]: - return True - - device = OpenhomeDevice(hass, device) - - add_devices([device], True) - DEVICES.append(device) - + if not discovery_info: return True + name = discovery_info.get('name') + description = discovery_info.get('ssdp_description') + _LOGGER.info('Openhome device found, (%s)', name) + device = Device(description) + + # if device has already been discovered + if device.Uuid() in [x.unique_id for x in DEVICES]: + return True + + device = OpenhomeDevice(hass, device) + + add_devices([device], True) + DEVICES.append(device) + return True diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 30672069558..a5f9979d2d4 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -51,11 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info: _LOGGER.debug('%s', discovery_info) - vals = discovery_info.split(':') - if len(vals) > 1: - port = vals[1] - - host = vals[0] + host = discovery_info.get('host') + port = discovery_info.get('port') remote = RemoteControl(host, port) add_devices([PanasonicVieraTVDevice(mac, name, remote)]) return True diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 66d4f02c526..ee8ae4840b2 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -39,7 +39,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.loader import get_component - REQUIREMENTS = ['plexapi==2.0.2'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) @@ -102,7 +101,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # Via discovery elif discovery_info is not None: # Parse discovery data - host = urlparse(discovery_info[1]).netloc + host = discovery_info.get('host') _LOGGER.info('Discovered PLEX server: %s', host) if host in _CONFIGURING: @@ -265,6 +264,7 @@ class PlexClient(MediaPlayerDevice): self._machine_identifier = None self._make = '' self._media_content_id = None + self._media_content_rating = None self._media_content_type = None self._media_duration = None self._media_image_url = None @@ -274,6 +274,7 @@ class PlexClient(MediaPlayerDevice): self._previous_volume_level = 1 # Used in fake muting self._session = None self._session_type = None + self._session_username = None self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely @@ -343,6 +344,8 @@ class PlexClient(MediaPlayerDevice): self._session.viewOffset) self._media_content_id = self._convert_na_to_none( self._session.ratingKey) + self._media_content_rating = self._convert_na_to_none( + self._session.contentRating) else: self._media_position = None self._media_content_id = None @@ -354,6 +357,8 @@ class PlexClient(MediaPlayerDevice): self._session.player.machineIdentifier) self._name = self._convert_na_to_none(self._session.player.title) self._player_state = self._session.player.state + self._session_username = self._convert_na_to_none( + self._session.username) self._make = self._convert_na_to_none(self._session.player.device) else: self._is_player_available = False @@ -786,7 +791,7 @@ class PlexClient(MediaPlayerDevice): src['library_name']).get(src['artist_name']).album( src['album_name']).get(src['track_name']) elif media_type == 'EPISODE': - media = self._get_episode( + media = self._get_tv_media( src['library_name'], src['show_name'], src['season_number'], src['episode_number']) elif media_type == 'PLAYLIST': @@ -795,18 +800,31 @@ class PlexClient(MediaPlayerDevice): media = self.device.server.library.section( src['library_name']).get(src['video_name']) - if media: - self._client_play_media(media, shuffle=src['shuffle']) + import plexapi.playlist + if (media and media_type == 'EPISODE' and + isinstance(media, plexapi.playlist.Playlist)): + # delete episode playlist after being loaded into a play queue + self._client_play_media(media=media, delete=True, + shuffle=src['shuffle']) + elif media: + self._client_play_media(media=media, shuffle=src['shuffle']) - def _get_episode(self, library_name, show_name, season_number, - episode_number): - """Find TV episode and return a Plex media object.""" + def _get_tv_media(self, library_name, show_name, season_number, + episode_number): + """Find TV media and return a Plex media object.""" target_season = None target_episode = None - seasons = self.device.server.library.section(library_name).get( - show_name).seasons() - for season in seasons: + show = self.device.server.library.section(library_name).get( + show_name) + + if not season_number: + playlist_name = "{} - {} Episodes".format( + self.entity_id, show_name) + return self.device.server.createPlaylist( + playlist_name, show.episodes()) + + for season in show.seasons(): if int(season.seasonNumber) == int(season_number): target_season = season break @@ -817,6 +835,12 @@ class PlexClient(MediaPlayerDevice): str(season_number).zfill(2), str(episode_number).zfill(2)) else: + if not episode_number: + playlist_name = "{} - {} Season {} Episodes".format( + self.entity_id, show_name, str(season_number)) + return self.device.server.createPlaylist( + playlist_name, target_season.episodes()) + for episode in target_season.episodes(): if int(episode.index) == int(episode_number): target_episode = episode @@ -830,7 +854,7 @@ class PlexClient(MediaPlayerDevice): return target_episode - def _client_play_media(self, media, **params): + def _client_play_media(self, media, delete=False, **params): """Instruct Plex client to play a piece of media.""" if not (self.device and 'playback' in self._device_protocol_capabilities): @@ -838,10 +862,16 @@ class PlexClient(MediaPlayerDevice): return import plexapi.playqueue - server_url = media.server.baseurl.split(':') playqueue = plexapi.playqueue.PlayQueue.create(self.device.server, media, **params) + + # delete dynamic playlists used to build playqueue (ex. play tv season) + if delete: + media.delete() + self._local_client_control_fix() + + server_url = self.device.server.baseurl.split(':') self.device.sendCommand('playback/playMedia', **dict({ 'machineIdentifier': self.device.server.machineIdentifier, @@ -854,3 +884,13 @@ class PlexClient(MediaPlayerDevice): 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, }, **params)) + + @property + def device_state_attributes(self): + """Return the scene state attributes.""" + attr = {} + attr['media_content_rating'] = self._media_content_rating + attr['session_username'] = self._session_username + attr['media_library_name'] = self._app_name + + return attr diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index a33f331b737..59e6a19bff8 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -42,11 +42,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Roku platform.""" hosts = [] - if discovery_info and discovery_info in KNOWN_HOSTS: - return + if discovery_info: + host = discovery_info[0] - if discovery_info is not None: - _LOGGER.debug('Discovered Roku: %s', discovery_info[0]) + if host in KNOWN_HOSTS: + return + + _LOGGER.debug('Discovered Roku: %s', host) hosts.append(discovery_info[0]) elif CONF_HOST in config: diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index b71e37fda19..c99ad49577c 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -58,7 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mac = config.get(CONF_MAC) timeout = config.get(CONF_TIMEOUT) elif discovery_info is not None: - tv_name, model, host = discovery_info + tv_name = discovery_info.get('name') + model = discovery_info.get('model_name') + host = discovery_info.get('host') name = "{} ({})".format(tv_name, model) port = DEFAULT_PORT timeout = DEFAULT_TIMEOUT diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index fa26e1613dc..e23616a47a9 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -222,30 +222,39 @@ soundtouch_play_everywhere: description: Play on all Bose Soundtouch devices fields: - entity_id: - description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + master: + description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices example: 'media_player.soundtouch_home' soundtouch_create_zone: description: Create a multi-room zone fields: - entity_id: - description: Name of entites that will coordinate the multi-room zone. Platform dependent. + master: + description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the new zone + example: 'media_player.soundtouch_bedroom' soundtouch_add_zone_slave: description: Add a slave to a multi-room zone fields: - entity_id: - description: Name of entites that will be added to the multi-room zone. Platform dependent. + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the existing zone + example: 'media_player.soundtouch_bedroom' soundtouch_remove_zone_slave: description: Remove a slave from the multi-room zone fields: - entity_id: - description: Name of entites that will be remove from the multi-room zone. Platform dependent. + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to remove from the existing zone + example: 'media_player.soundtouch_bedroom' diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 0fc402caf6c..00cc52ab3a1 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): soco.config.EVENT_ADVERTISE_IP = advertise_addr if discovery_info: - player = soco.SoCo(discovery_info) + player = soco.SoCo(discovery_info.get('host')) # if device allready exists by config if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]: @@ -292,7 +292,7 @@ class SonosDevice(MediaPlayerDevice): @asyncio.coroutine def async_added_to_hass(self): """Subscribe sonos events.""" - self.hass.loop.run_in_executor(None, self._subscribe_to_player_events) + self.hass.async_add_job(self._subscribe_to_player_events) @property def should_poll(self): diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 90a870a8c65..fb2e02494df 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) -REQUIREMENTS = ['libsoundtouch==0.1.0'] +REQUIREMENTS = ['libsoundtouch==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -29,33 +29,33 @@ MAP_STATUS = { "PLAY_STATE": STATE_PLAYING, "BUFFERING_STATE": STATE_PLAYING, "PAUSE_STATE": STATE_PAUSED, - "STOp_STATE": STATE_OFF + "STOP_STATE": STATE_OFF } +DATA_SOUNDTOUCH = "soundtouch" + SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({ - 'master': cv.entity_id, + vol.Required('master'): cv.entity_id }) SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('slaves'): cv.entity_ids }) SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('slaves'): cv.entity_ids }) SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({ - 'master': cv.entity_id, - 'slaves': cv.entity_ids + vol.Required('master'): cv.entity_id, + vol.Required('slaves'): cv.entity_ids }) DEFAULT_NAME = 'Bose Soundtouch' DEFAULT_PORT = 8090 -DEVICES = [] - SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ @@ -70,180 +70,99 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Bose Soundtouch platform.""" - name = config.get(CONF_NAME) + if DATA_SOUNDTOUCH not in hass.data: + hass.data[DATA_SOUNDTOUCH] = [] - remote_config = { - 'name': 'HomeAssistant', - 'description': config.get(CONF_NAME), - 'id': 'ha.component.soundtouch', - 'port': config.get(CONF_PORT), - 'host': config.get(CONF_HOST) - } + if discovery_info: + # Discovery + host = discovery_info["host"] + port = int(discovery_info["port"]) - soundtouch_device = SoundTouchDevice(name, remote_config) - DEVICES.append(soundtouch_device) - add_devices([soundtouch_device]) + # if device already exists by config + if host in [device.config['host'] for device in + hass.data[DATA_SOUNDTOUCH]]: + return + + remote_config = { + 'id': 'ha.component.soundtouch', + 'host': host, + 'port': port + } + soundtouch_device = SoundTouchDevice(None, remote_config) + hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) + add_devices([soundtouch_device]) + else: + # Config + name = config.get(CONF_NAME) + remote_config = { + 'id': 'ha.component.soundtouch', + 'port': config.get(CONF_PORT), + 'host': config.get(CONF_HOST) + } + soundtouch_device = SoundTouchDevice(name, remote_config) + hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) + add_devices([soundtouch_device]) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) + def service_handle(service): + """Internal func for applying a service.""" + master_device_id = service.data.get('master') + slaves_ids = service.data.get('slaves') + slaves = [] + if slaves_ids: + slaves = [device for device in hass.data[DATA_SOUNDTOUCH] if + device.entity_id in slaves_ids] + + master = next([device for device in hass.data[DATA_SOUNDTOUCH] if + device.entity_id == master_device_id].__iter__(), None) + + if master is None: + _LOGGER.warning("Unable to find master with entity_id:" + str( + master_device_id)) + return + + if service.service == SERVICE_PLAY_EVERYWHERE: + slaves = [d for d in hass.data[DATA_SOUNDTOUCH] if + d.entity_id != master_device_id] + master.create_zone(slaves) + elif service.service == SERVICE_CREATE_ZONE: + master.create_zone(slaves) + elif service.service == SERVICE_REMOVE_ZONE_SLAVE: + master.remove_zone_slave(slaves) + elif service.service == SERVICE_ADD_ZONE_SLAVE: + master.add_zone_slave(slaves) + hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE, - play_everywhere_service, + service_handle, descriptions.get(SERVICE_PLAY_EVERYWHERE), schema=SOUNDTOUCH_PLAY_EVERYWHERE) hass.services.register(DOMAIN, SERVICE_CREATE_ZONE, - create_zone_service, + service_handle, descriptions.get(SERVICE_CREATE_ZONE), schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE, - remove_zone_slave, + service_handle, descriptions.get(SERVICE_REMOVE_ZONE_SLAVE), schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA) hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE, - add_zone_slave, + service_handle, descriptions.get(SERVICE_ADD_ZONE_SLAVE), schema=SOUNDTOUCH_ADD_ZONE_SCHEMA) -def play_everywhere_service(service): - """ - Create a zone (multi-room) and play on all devices. - - :param service: Home Assistant service with 'master' data set - - :Example: - - - service: media_player.soundtouch_play_everywhere - data: - master: media_player.soundtouch_living_room - - """ - master_device_id = service.data.get('master') - slaves = [d for d in DEVICES if d.entity_id != master_device_id] - master = next([device for device in DEVICES if - device.entity_id == master_device_id].__iter__(), None) - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id:" + str(master_device_id)) - elif not slaves: - _LOGGER.warning("Unable to create zone without slaves") - else: - _LOGGER.info( - "Creating zone with master " + str(master.device.config.name)) - master.device.create_zone([slave.device for slave in slaves]) - - -def create_zone_service(service): - """ - Create a zone (multi-room) on a master and play on specified slaves. - - At least one master and one slave must be specified - - :param service: Home Assistant service with 'master' and 'slaves' data set - - :Example: - - - service: media_player.soundtouch_create_zone - data: - master: media_player.soundtouch_living_room - slaves: - - media_player.soundtouch_room - - media_player.soundtouch_kitchen - - """ - master_device_id = service.data.get('master') - slaves_ids = service.data.get('slaves') - slaves = [device for device in DEVICES if device.entity_id in slaves_ids] - master = next([device for device in DEVICES if - device.entity_id == master_device_id].__iter__(), None) - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id:" + master_device_id) - elif not slaves: - _LOGGER.warning("Unable to create zone without slaves") - else: - _LOGGER.info( - "Creating zone with master " + str(master.device.config.name)) - master.device.create_zone([slave.device for slave in slaves]) - - -def add_zone_slave(service): - """ - Add slave(s) to and existing zone (multi-room). - - Zone must already exist and slaves array can not be empty. - - :param service: Home Assistant service with 'master' and 'slaves' data set - - :Example: - - - service: media_player.soundtouch_add_zone_slave - data: - master: media_player.soundtouch_living_room - slaves: - - media_player.soundtouch_room - - """ - master_device_id = service.data.get('master') - slaves_ids = service.data.get('slaves') - slaves = [device for device in DEVICES if device.entity_id in slaves_ids] - master = next([device for device in DEVICES if - device.entity_id == master_device_id].__iter__(), None) - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id:" + str(master_device_id)) - elif not slaves: - _LOGGER.warning("Unable to find slaves to add") - else: - _LOGGER.info( - "Adding slaves to zone with master " + str( - master.device.config.name)) - master.device.add_zone_slave([slave.device for slave in slaves]) - - -def remove_zone_slave(service): - """ - Remove slave(s) from and existing zone (multi-room). - - Zone must already exist and slaves array can not be empty. - Note: If removing last slave, the zone will be deleted and you'll have to - create a new one. You will not be able to add a new slave anymore - - :param service: Home Assistant service with 'master' and 'slaves' data set - - :Example: - - - service: media_player.soundtouch_remove_zone_slave - data: - master: media_player.soundtouch_living_room - slaves: - - media_player.soundtouch_room - - """ - master_device_id = service.data.get('master') - slaves_ids = service.data.get('slaves') - slaves = [device for device in DEVICES if device.entity_id in slaves_ids] - master = next([device for device in DEVICES if - device.entity_id == master_device_id].__iter__(), None) - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id:" + master_device_id) - elif not slaves: - _LOGGER.warning("Unable to find slaves to remove") - else: - _LOGGER.info("Removing slaves from zone with master " + - str(master.device.config.name)) - master.device.remove_zone_slave([slave.device for slave in slaves]) - - class SoundTouchDevice(MediaPlayerDevice): """Representation of a SoundTouch Bose device.""" def __init__(self, name, config): """Create Soundtouch Entity.""" from libsoundtouch import soundtouch_device - self._name = name self._device = soundtouch_device(config['host'], config['port']) + if name is None: + self._name = self._device.config.name + else: + self._name = name self._status = self._device.status() self._volume = self._device.volume() self._config = config @@ -297,7 +216,7 @@ class SoundTouchDevice(MediaPlayerDevice): self._status = self._device.status() def turn_on(self): - """Turn the media player on.""" + """Turn on media player.""" self._device.power_on() self._status = self._device.status() @@ -392,3 +311,52 @@ class SoundTouchDevice(MediaPlayerDevice): self._device.select_preset(preset) else: _LOGGER.warning("Unable to find preset with id " + str(media_id)) + + def create_zone(self, slaves): + """ + Create a zone (multi-room) and play on selected devices. + + :param slaves: slaves on which to play + + """ + if not slaves: + _LOGGER.warning("Unable to create zone without slaves") + else: + _LOGGER.info( + "Creating zone with master " + str(self.device.config.name)) + self.device.create_zone([slave.device for slave in slaves]) + + def remove_zone_slave(self, slaves): + """ + Remove slave(s) from and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + Note: If removing last slave, the zone will be deleted and you'll have + to create a new one. You will not be able to add a new slave anymore + + :param slaves: slaves to remove from the zone + + """ + if not slaves: + _LOGGER.warning("Unable to find slaves to remove") + else: + _LOGGER.info("Removing slaves from zone with master " + + str(self.device.config.name)) + self.device.remove_zone_slave([slave.device for slave in slaves]) + + def add_zone_slave(self, slaves): + """ + Add slave(s) to and existing zone (multi-room). + + Zone must already exist and slaves array can not be empty. + + :param slaves:slaves to add + + """ + if not slaves: + _LOGGER.warning("Unable to find slaves to add") + else: + _LOGGER.info( + "Adding slaves to zone with master " + str( + self.device.config.name)) + self.device.add_zone_slave([slave.device for slave in slaves]) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py new file mode 100644 index 00000000000..ce4230bf92c --- /dev/null +++ b/homeassistant/components/media_player/spotify.py @@ -0,0 +1,286 @@ +""" +Support for interacting with Spotify Connect. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.spotify/ +""" + +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.loader import get_component +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET, + SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv + + +COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2' +REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/' + 'archive/%s.zip#spotipy==2.4.4' % COMMIT] + +DEPENDENCIES = ['http'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SPOTIFY = SUPPORT_VOLUME_SET | SUPPORT_PAUSE | SUPPORT_PLAY |\ + SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_SELECT_SOURCE |\ + SUPPORT_PLAY_MEDIA + +SCOPE = 'user-read-playback-state user-modify-playback-state' +DEFAULT_CACHE_PATH = '.spotify-token-cache' +AUTH_CALLBACK_PATH = '/api/spotify' +AUTH_CALLBACK_NAME = 'api:spotify' +ICON = 'mdi:spotify' +DEFAULT_NAME = 'Spotify' +DOMAIN = 'spotify' +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_CACHE_PATH = 'cache_path' +CONFIGURATOR_LINK_NAME = 'Link Spotify account' +CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully' +CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \ + 'click the link, login, and authorize:' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_CACHE_PATH): cv.string +}) + +SCAN_INTERVAL = timedelta(seconds=30) + + +def request_configuration(hass, config, add_devices, oauth): + """Request Spotify authorization.""" + configurator = get_component('configurator') + hass.data[DOMAIN] = configurator.request_config( + hass, DEFAULT_NAME, lambda _: None, + link_name=CONFIGURATOR_LINK_NAME, + link_url=oauth.get_authorize_url(), + description=CONFIGURATOR_DESCRIPTION, + submit_caption=CONFIGURATOR_SUBMIT_CAPTION) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Spotify platform.""" + import spotipy.oauth2 + callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH) + cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) + oauth = spotipy.oauth2.SpotifyOAuth( + config.get(CONF_CLIENT_ID), config.get(CONF_CLIENT_SECRET), + callback_url, scope=SCOPE, + cache_path=cache) + token_info = oauth.get_cached_token() + if not token_info: + _LOGGER.info('no token; requesting authorization') + hass.http.register_view(SpotifyAuthCallbackView( + config, add_devices, oauth)) + request_configuration(hass, config, add_devices, oauth) + return + if hass.data.get(DOMAIN): + configurator = get_component('configurator') + configurator.request_done(hass.data.get(DOMAIN)) + del hass.data[DOMAIN] + player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME)) + add_devices([player], True) + + +class SpotifyAuthCallbackView(HomeAssistantView): + """Spotify Authorization Callback View.""" + + requires_auth = False + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + + def __init__(self, config, add_devices, oauth): + """Initialize.""" + self.config = config + self.add_devices = add_devices + self.oauth = oauth + + @callback + def get(self, request): + """Receive authorization token.""" + hass = request.app['hass'] + self.oauth.get_access_token(request.GET['code']) + hass.async_add_job(setup_platform, hass, self.config, self.add_devices) + + +class SpotifyMediaPlayer(MediaPlayerDevice): + """Representation of a Spotify controller.""" + + def __init__(self, oauth, name): + """Initialize.""" + self._name = name + self._oauth = oauth + self._album = None + self._title = None + self._artist = None + self._uri = None + self._image_url = None + self._state = STATE_UNKNOWN + self._current_device = None + self._devices = None + self._volume = None + self._player = None + self._token_info = self._oauth.get_cached_token() + + def refresh_spotify_instance(self): + """Fetch a new spotify instance.""" + import spotipy + token_refreshed = False + need_token = (self._token_info is None or + self._oauth.is_token_expired(self._token_info)) + if need_token: + new_token = \ + self._oauth.refresh_access_token( + self._token_info['refresh_token']) + self._token_info = new_token + token_refreshed = True + if self._player is None or token_refreshed: + self._player = \ + spotipy.Spotify(auth=self._token_info.get('access_token')) + + def update(self): + """Update state and attributes.""" + self.refresh_spotify_instance() + # Available devices + devices = self._player.devices().get('devices') + if devices is not None: + self._devices = {device.get('name'): device.get('id') + for device in devices} + # Current playback state + current = self._player.current_playback() + if current is None: + self._state = STATE_IDLE + return + # Track metadata + item = current.get('item') + if item: + self._album = item.get('album').get('name') + self._title = item.get('name') + self._artist = ', '.join([artist.get('name') + for artist in item.get('artists')]) + self._uri = current.get('uri') + self._image_url = item.get('album').get('images')[0].get('url') + # Playing state + self._state = STATE_PAUSED + if current.get('is_playing'): + self._state = STATE_PLAYING + device = current.get('device') + if device is None: + self._state = STATE_IDLE + else: + if device.get('volume_percent'): + self._volume = device.get('volume_percent') / 100 + if device.get('name'): + self._current_device = device.get('name') + + def set_volume_level(self, volume): + """Set the volume level.""" + self._player.volume(int(volume * 100)) + + def media_next_track(self): + """Skip to next track.""" + self._player.next_track() + + def media_previous_track(self): + """Skip to previous track.""" + self._player.previous_track() + + def media_play(self): + """Start or resume playback.""" + self._player.start_playback() + + def media_pause(self): + """Pause playback.""" + self._player.pause_playback() + + def select_source(self, source): + """Select playback device.""" + self._player.transfer_playback(self._devices[source]) + + def play_media(self, media_type, media_id, **kwargs): + """Play media.""" + kwargs = {} + if media_type == MEDIA_TYPE_MUSIC: + kwargs['uris'] = [media_id] + elif media_type == MEDIA_TYPE_PLAYLIST: + kwargs['context_uri'] = media_id + else: + _LOGGER.error('media type %s is not supported', media_type) + return + if not media_id.startswith('spotify:'): + _LOGGER.error('media id must be spotify uri') + return + self._player.start_playback(**kwargs) + + @property + def name(self): + """Name.""" + return self._name + + @property + def icon(self): + """Icon.""" + return ICON + + @property + def state(self): + """Playback state.""" + return self._state + + @property + def volume_level(self): + """Device volume.""" + return self._volume + + @property + def source_list(self): + """Playback devices.""" + return list(self._devices.keys()) + + @property + def source(self): + """Current playback device.""" + return self._current_device + + @property + def media_content_id(self): + """Media URL.""" + return self._uri + + @property + def media_image_url(self): + """Media image url.""" + return self._image_url + + @property + def media_artist(self): + """Media artist.""" + return self._artist + + @property + def media_album_name(self): + """Media album.""" + return self._album + + @property + def media_title(self): + """Media title.""" + return self._title + + @property + def supported_features(self): + """Media player features that are supported.""" + return SUPPORT_SPOTIFY diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index eec8eeb8177..40c48f55215 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -29,8 +29,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 9000 TIMEOUT = 10 -KNOWN_DEVICES = [] - SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ @@ -71,18 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host, port, error) return False - # Combine it with port to allow multiple servers at the same host - key = "{}:{}".format(ipaddr, port) - - # Only add a media server once - if key in KNOWN_DEVICES: - return False - KNOWN_DEVICES.append(key) - _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) - if lms is False: - return False players = yield from lms.create_players() async_add_devices(players) @@ -173,6 +161,11 @@ class SqueezeBoxDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def unique_id(self): + """Return an unique ID.""" + return self._id + @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index fe029af163e..c1b51e2d32a 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.webostv/ """ import logging +import asyncio from datetime import timedelta from urllib.parse import urlparse @@ -24,9 +25,7 @@ from homeassistant.const import ( from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' - '/archive/v0.1.4.zip' - '#pylgtv==0.1.4', +REQUIREMENTS = ['pylgtv==0.1.6', 'websockets==3.2', 'wakeonlan==0.2.2'] @@ -101,7 +100,8 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices): _LOGGER.warning( "Connected to LG webOS TV %s but not paired", host) return - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): _LOGGER.error("Unable to connect to host %s", host) return else: @@ -198,7 +198,8 @@ class LgWebOSDevice(MediaPlayerDevice): app = self._app_list[source['appId']] self._source_list[app['title']] = app - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): self._state = STATE_OFF @property @@ -240,7 +241,10 @@ class LgWebOSDevice(MediaPlayerDevice): def media_image_url(self): """Image url of current playing media.""" if self._current_source_id in self._app_list: - return self._app_list[self._current_source_id]['largeIcon'] + icon = self._app_list[self._current_source_id]['largeIcon'] + if not icon.startswith('http'): + icon = self._app_list[self._current_source_id]['icon'] + return icon return None @property @@ -256,7 +260,8 @@ class LgWebOSDevice(MediaPlayerDevice): self._state = STATE_OFF try: self._client.power_off() - except (OSError, ConnectionClosed): + except (OSError, ConnectionClosed, TypeError, + asyncio.TimeoutError): pass def turn_on(self): diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 928d15b5950..243b96220c9 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zone_ignore = config.get(CONF_ZONE_IGNORE) if discovery_info is not None: - name = discovery_info[0] - model = discovery_info[1] - ctrl_url = discovery_info[2] - desc_url = discovery_info[3] + name = discovery_info.get('name') + model = discovery_info.get('model_name') + ctrl_url = discovery_info.get('control_url') + desc_url = discovery_info.get('description_url') if ctrl_url in hass.data[KNOWN]: _LOGGER.info("%s already manually configured", ctrl_url) return diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 1f1363f2060..3ed9fff9cf0 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -158,6 +158,15 @@ class ModbusHub(object): count, **kwargs) + def read_input_registers(self, unit, address, count): + """Read input registers.""" + with self._lock: + kwargs = {'unit': unit} if unit else {} + return self._client.read_input_registers( + address, + count, + **kwargs) + def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e53209e3b4c..2b6774939da 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,7 +28,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA -REQUIREMENTS = ['paho-mqtt==1.2.1'] +REQUIREMENTS = ['paho-mqtt==1.2.2'] _LOGGER = logging.getLogger(__name__) @@ -201,7 +201,8 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None): @asyncio.coroutine -def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): +def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS, + encoding='utf-8'): """Subscribe to an MQTT topic.""" @callback def async_mqtt_topic_subscriber(dp_topic, dp_payload, dp_qos): @@ -209,7 +210,21 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): if not _match_topic(topic, dp_topic): return - hass.async_run_job(msg_callback, dp_topic, dp_payload, dp_qos) + if encoding is not None: + try: + payload = dp_payload.decode(encoding) + _LOGGER.debug("Received message on %s: %s", + dp_topic, payload) + except (AttributeError, UnicodeDecodeError): + _LOGGER.error("Illegal payload encoding %s from " + "MQTT topic: %s, Payload: %s", + encoding, dp_topic, dp_payload) + return + else: + _LOGGER.debug("Received binary message on %s", dp_topic) + payload = dp_payload + + hass.async_run_job(msg_callback, dp_topic, payload, dp_qos) async_remove = async_dispatcher_connect( hass, SIGNAL_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber) @@ -218,10 +233,12 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): return async_remove -def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): +def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS, + encoding='utf-8'): """Subscribe to an MQTT topic.""" async_remove = run_coroutine_threadsafe( - async_subscribe(hass, topic, msg_callback, qos), + async_subscribe(hass, topic, msg_callback, + qos, encoding), hass.loop ).result() @@ -372,16 +389,16 @@ def async_setup(hass, config): payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE) qos = call.data[ATTR_QOS] retain = call.data[ATTR_RETAIN] - try: - if payload_template is not None: + if payload_template is not None: + try: payload = \ template.Template(payload_template, hass).async_render() - except template.jinja2.TemplateError as exc: - _LOGGER.error( - "Unable to publish to '%s': rendering payload template of " - "'%s' failed because %s", - msg_topic, payload_template, exc) - return + except template.jinja2.TemplateError as exc: + _LOGGER.error( + "Unable to publish to '%s': rendering payload template of " + "'%s' failed because %s", + msg_topic, payload_template, exc) + return yield from hass.data[DATA_MQTT].async_publish( msg_topic, payload, qos, retain) @@ -564,18 +581,10 @@ class MQTT(object): def _mqtt_on_message(self, _mqttc, _userdata, msg): """Message received callback.""" - try: - payload = msg.payload.decode('utf-8') - except (AttributeError, UnicodeDecodeError): - _LOGGER.error("Illegal utf-8 unicode payload from " - "MQTT topic: %s, Payload: %s", msg.topic, - msg.payload) - else: - _LOGGER.info("Received message on %s: %s", msg.topic, payload) - dispatcher_send( - self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, payload, - msg.qos - ) + dispatcher_send( + self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, msg.payload, + msg.qos + ) def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos): """Unsubscribe successful callback.""" diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index e0b36721f74..67716c6a2e5 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,12 +17,13 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip' - '#pybotvac==0.0.1'] +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.3.zip' + '#pybotvac==0.0.3'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' NEATO_LOGIN = 'neato_login' +NEATO_MAP_DATA = 'neato_map_data' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -80,7 +81,7 @@ ALERTS = { def setup(hass, config): - """Setup the Verisure component.""" + """Setup the Neato component.""" from pybotvac import Account hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account) @@ -89,7 +90,7 @@ def setup(hass, config): _LOGGER.debug('Failed to login to Neato API') return False hub.update_robots() - for component in ('sensor', 'switch'): + for component in ('camera', 'sensor', 'switch'): discovery.load_platform(hass, component, DOMAIN, {}, config) return True @@ -108,6 +109,7 @@ class NeatoHub(object): domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps def login(self): """Login to My Neato.""" @@ -126,3 +128,9 @@ class NeatoHub(object): _LOGGER.debug('Running HUB.update_robots %s', self._hass.data[NEATO_ROBOTS]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + def download_map(self, url): + """Download a new map image.""" + map_image_data = self.my_neato.get_map_image(url) + return map_image_data diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index f2ef64a9ea0..4db12a5b8d8 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==3.6.5'] +REQUIREMENTS = ['sendgrid==4.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index e82971e0064..0e91fc8698a 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -14,8 +14,7 @@ from homeassistant.components.notify import ( ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON) -REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.4.zip' - '#pylgtv==0.1.4'] +REQUIREMENTS = ['pylgtv==0.1.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index 7084339ab42..5bf629ed37f 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -8,6 +8,7 @@ import asyncio import logging import aiohttp +from aiohttp import hdrs import async_timeout import voluptuous as vol @@ -31,6 +32,8 @@ SUPPORT_REST_METHODS = [ 'delete', ] +CONF_CONTENT_TYPE = 'content_type' + COMMAND_SCHEMA = vol.Schema({ vol.Required(CONF_URL): cv.template, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): @@ -39,6 +42,7 @@ COMMAND_SCHEMA = vol.Schema({ vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, vol.Optional(CONF_PAYLOAD): cv.template, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), + vol.Optional(CONF_CONTENT_TYPE): cv.string }) CONFIG_SCHEMA = vol.Schema({ @@ -72,6 +76,11 @@ def async_setup(hass, config): template_payload = command_config[CONF_PAYLOAD] template_payload.hass = hass + headers = None + if CONF_CONTENT_TYPE in command_config: + content_type = command_config[CONF_CONTENT_TYPE] + headers = {hdrs.CONTENT_TYPE: content_type} + @asyncio.coroutine def async_service_handler(service): """Execute a shell command service.""" @@ -86,7 +95,8 @@ def async_setup(hass, config): request = yield from getattr(websession, method)( template_url.async_render(variables=service.data), data=payload, - auth=auth + auth=auth, + headers=headers ) if request.status < 400: diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py new file mode 100644 index 00000000000..f600510d406 --- /dev/null +++ b/homeassistant/components/scene/lifx_cloud.py @@ -0,0 +1,97 @@ +""" +Support for LIFX Cloud scenes. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/scene.lifx_cloud/ +""" +import asyncio +import logging + +import voluptuous as vol + +import aiohttp +import async_timeout + +from homeassistant.components.scene import Scene +from homeassistant.const import (CONF_PLATFORM, CONF_TOKEN, CONF_TIMEOUT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import (async_get_clientsession) + +_LOGGER = logging.getLogger(__name__) + +LIFX_API_URL = 'https://api.lifx.com/v1/{0}' +DEFAULT_TIMEOUT = 10 + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'lifx_cloud', + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, +}) + + +# pylint: disable=unused-argument +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the scenes stored in the LIFX Cloud.""" + token = config.get(CONF_TOKEN) + timeout = config.get(CONF_TIMEOUT) + + headers = { + "Authorization": "Bearer %s" % token, + } + + url = LIFX_API_URL.format('scenes') + + try: + httpsession = async_get_clientsession(hass) + with async_timeout.timeout(timeout, loop=hass.loop): + scenes_resp = yield from httpsession.get(url, headers=headers) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.exception("Error on %s", url) + return False + + status = scenes_resp.status + if status == 200: + data = yield from scenes_resp.json() + devices = [] + for scene in data: + devices.append(LifxCloudScene(hass, headers, timeout, scene)) + async_add_devices(devices) + return True + elif status == 401: + _LOGGER.error("Unauthorized (bad token?) on %s", url) + return False + else: + _LOGGER.error("HTTP error %d on %s", scenes_resp.status, url) + return False + + +class LifxCloudScene(Scene): + """Representation of a LIFX Cloud scene.""" + + def __init__(self, hass, headers, timeout, scene_data): + """Initialize the scene.""" + self.hass = hass + self._headers = headers + self._timeout = timeout + self._name = scene_data["name"] + self._uuid = scene_data["uuid"] + + @property + def name(self): + """Return the name of the scene.""" + return self._name + + @asyncio.coroutine + def async_activate(self): + """Activate the scene.""" + url = LIFX_API_URL.format('scenes/scene_id:%s/activate' % self._uuid) + + try: + httpsession = async_get_clientsession(self.hass) + with async_timeout.timeout(self._timeout, loop=self.hass.loop): + yield from httpsession.put(url, headers=self._headers) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.exception("Error on %s", url) diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py new file mode 100644 index 00000000000..88246cc0bc2 --- /dev/null +++ b/homeassistant/components/sensor/alarmdecoder.py @@ -0,0 +1,75 @@ +""" +Support for AlarmDecoder Sensors (Shows Panel Display). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE) + +from homeassistant.const import (STATE_UNKNOWN) + +DEPENDENCIES = ['alarmdecoder'] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Perform the setup for AlarmDecoder sensor devices.""" + _LOGGER.debug("AlarmDecoderSensor: async_setup_platform") + + device = AlarmDecoderSensor(hass) + + async_add_devices([device]) + + +class AlarmDecoderSensor(Entity): + """Representation of an AlarmDecoder keypad.""" + + def __init__(self, hass): + """Initialize the alarm panel.""" + self._display = "" + self._state = STATE_UNKNOWN + self._icon = 'mdi:alarm-check' + self._name = 'Alarm Panel Display' + + _LOGGER.debug("AlarmDecoderSensor: Setting up panel") + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) + + @callback + def _message_callback(self, message): + if self._display != message.text: + self._display = message.text + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._display + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 2d05372220b..40556fbe5ad 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -19,7 +19,7 @@ import homeassistant.loader as loader from requests.exceptions import HTTPError, ConnectTimeout -REQUIREMENTS = ['amcrest==1.1.8'] +REQUIREMENTS = ['amcrest==1.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 0bf68e68b0d..d2e148b8204 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -116,6 +116,7 @@ class ArwnSensor(Entity): """Update the sensor with the most recent event.""" self.event = {} self.event.update(event) + self.hass.async_add_job(self.async_update_ha_state()) @property def state(self): diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index a86d28a1a06..e26d12469d8 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -13,13 +13,13 @@ from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['py-cpuinfo==3.0.0'] +REQUIREMENTS = ['py-cpuinfo==3.2.0'] _LOGGER = logging.getLogger(__name__) ATTR_BRAND = 'Brand' ATTR_HZ = 'GHz Advertised' -ATTR_VENDOR = 'Vendor ID' +ATTR_ARCH = 'arch' DEFAULT_NAME = 'CPU speed' ICON = 'mdi:pulse' @@ -67,7 +67,7 @@ class CpuSpeedSensor(Entity): """Return the state attributes.""" if self.info is not None: return { - ATTR_VENDOR: self.info['vendor_id'], + ATTR_ARCH: self.info['arch'], ATTR_BRAND: self.info['brand'], ATTR_HZ: round(self.info['hz_advertised_raw'][0]/10**9, 2) } diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index b9c3f82cdc5..84669a57000 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -153,10 +153,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from protocol.wait_closed() if hass.state != CoreState.stopping: + # unexpected disconnect if transport: # remove listerer stop_listerer() + # reflect disconnect state in devices state by setting an + # empty telegram resulting in `unkown` states + update_entities_telegram({}) + # throttle reconnect attempts yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL], loop=hass.loop) @@ -207,7 +212,7 @@ class DSMREntity(Entity): if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) else: - if value: + if value is not None: return value else: return STATE_UNKNOWN diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index 9cbeb753b2b..2e1b8e6a6a0 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_date import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fedexdeliverymanager==1.0.1'] +REQUIREMENTS = ['fedexdeliverymanager==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 70876cca1c2..5deb00db67b 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -153,7 +153,7 @@ class FidoSensor(Entity): def update(self): """Get the latest data from Fido and update the state.""" self.fido_data.update() - if self._name == 'balance': + if self.type == 'balance': if self.fido_data.data.get(self.type) is not None: self._state = round(self.fido_data.data[self.type], 2) else: diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index ba963e44b6c..3173eec4285 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -14,7 +14,8 @@ SENSOR_TYPES = { "state": ["Battery State", None] } -DEFAULT_ICON = "mdi:battery" +DEFAULT_ICON_LEVEL = "mdi:battery" +DEFAULT_ICON_STATE = "mdi:power-plug" def setup_platform(hass, config, add_devices, discovery_info=None): @@ -56,12 +57,12 @@ class IOSSensor(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return "sensor_ios_battery_{}_{}".format(self.type, self._device_name) + device_id = self._device[ios.ATTR_DEVICE_ID] + return "sensor_ios_battery_{}_{}".format(self.type, device_id) @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement @property def device_state_attributes(self): @@ -83,28 +84,44 @@ class IOSSensor(Entity): battery_state = device_battery[ios.ATTR_BATTERY_STATE] battery_level = device_battery[ios.ATTR_BATTERY_LEVEL] rounded_level = round(battery_level, -1) - returning_icon = DEFAULT_ICON + returning_icon_level = DEFAULT_ICON_LEVEL if battery_state == ios.ATTR_BATTERY_STATE_FULL: - returning_icon = DEFAULT_ICON + returning_icon_level = DEFAULT_ICON_LEVEL + if battery_state == ios.ATTR_BATTERY_STATE_CHARGING: + returning_icon_state = DEFAULT_ICON_STATE + else: + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_CHARGING: # Why is MDI missing 10, 50, 70? if rounded_level in (20, 30, 40, 60, 80, 90, 100): - returning_icon = "{}-charging-{}".format(DEFAULT_ICON, - str(rounded_level)) + returning_icon_level = "{}-charging-{}".format( + DEFAULT_ICON_LEVEL, str(rounded_level)) + returning_icon_state = DEFAULT_ICON_STATE else: - returning_icon = "{}-charging".format(DEFAULT_ICON) + returning_icon_level = "{}-charging".format( + DEFAULT_ICON_LEVEL) + returning_icon_state = DEFAULT_ICON_STATE elif battery_state == ios.ATTR_BATTERY_STATE_UNPLUGGED: if rounded_level < 10: - returning_icon = "{}-outline".format(DEFAULT_ICON) - elif battery_level == 100: - returning_icon = DEFAULT_ICON + returning_icon_level = "{}-outline".format( + DEFAULT_ICON_LEVEL) + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) + elif battery_level > 95: + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) + returning_icon_level = "{}-outline".format( + DEFAULT_ICON_LEVEL) else: - returning_icon = "{}-{}".format(DEFAULT_ICON, - str(rounded_level)) + returning_icon_level = "{}-{}".format(DEFAULT_ICON_LEVEL, + str(rounded_level)) + returning_icon_state = "{}-off".format(DEFAULT_ICON_STATE) elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: - returning_icon = "{}-unknown".format(DEFAULT_ICON) + returning_icon_level = "{}-unknown".format(DEFAULT_ICON_LEVEL) + returning_icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) - return returning_icon + if self.type == "state": + return returning_icon_state + else: + return returning_icon_level def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index d612ca5cf26..33ffc769991 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -34,8 +34,6 @@ ATTR_TO_PROPERTY = [ CONF_ENTITY_IDS = 'entity_ids' CONF_ROUND_DIGITS = 'round_digits' -DEFAULT_NAME = 'Min/Max/Avg Sensor' - ICON = 'mdi:calculator' SENSOR_TYPES = { @@ -47,7 +45,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TYPE, default=SENSOR_TYPES[ATTR_MAX_VALUE]): vol.All(cv.string, vol.In(SENSOR_TYPES.values())), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_IDS): cv.entity_ids, vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int), }) @@ -67,6 +65,39 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return True +def calc_min(sensor_values): + """Calculate min value, honoring unknown states.""" + val = STATE_UNKNOWN + for sval in sensor_values: + if sval != STATE_UNKNOWN: + if val == STATE_UNKNOWN or val > sval: + val = sval + return val + + +def calc_max(sensor_values): + """Calculate max value, honoring unknown states.""" + val = STATE_UNKNOWN + for sval in sensor_values: + if sval != STATE_UNKNOWN: + if val == STATE_UNKNOWN or val < sval: + val = sval + return val + + +def calc_mean(sensor_values, round_digits): + """Calculate mean value, honoring unknown states.""" + val = 0 + count = 0 + for sval in sensor_values: + if sval != STATE_UNKNOWN: + val += sval + count += 1 + if count == 0: + return STATE_UNKNOWN + return round(val/count, round_digits) + + class MinMaxSensor(Entity): """Representation of a min/max sensor.""" @@ -76,10 +107,15 @@ class MinMaxSensor(Entity): self._entity_ids = entity_ids self._sensor_type = sensor_type self._round_digits = round_digits - self._name = '{} {}'.format( - name, next(v for k, v in SENSOR_TYPES.items() - if self._sensor_type == v)) + + if name: + self._name = name + else: + self._name = '{} sensor'.format( + next(v for k, v in SENSOR_TYPES.items() + if self._sensor_type == v)).capitalize() self._unit_of_measurement = None + self._unit_of_measurement_mismatch = False self.min_value = self.max_value = self.mean = STATE_UNKNOWN self.count_sensors = len(self._entity_ids) self.states = {} @@ -89,6 +125,8 @@ class MinMaxSensor(Entity): def async_min_max_sensor_state_listener(entity, old_state, new_state): """Called when the sensor changes state.""" if new_state.state is None or new_state.state in STATE_UNKNOWN: + self.states[entity] = STATE_UNKNOWN + hass.async_add_job(self.async_update_ha_state, True) return if self._unit_of_measurement is None: @@ -97,8 +135,11 @@ class MinMaxSensor(Entity): if self._unit_of_measurement != new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT): - _LOGGER.warning("Units of measurement do not match") - return + _LOGGER.warning( + "Units of measurement do not match for entity %s", + self.entity_id) + self._unit_of_measurement_mismatch = True + try: self.states[entity] = float(new_state.state) except ValueError: @@ -118,12 +159,16 @@ class MinMaxSensor(Entity): @property def state(self): """Return the state of the sensor.""" + if self._unit_of_measurement_mismatch: + return STATE_UNKNOWN return getattr(self, next( k for k, v in SENSOR_TYPES.items() if self._sensor_type == v)) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" + if self._unit_of_measurement_mismatch: + return "ERR" return self._unit_of_measurement @property @@ -150,10 +195,6 @@ class MinMaxSensor(Entity): """Get the latest data and updates the states.""" sensor_values = [self.states[k] for k in self._entity_ids if k in self.states] - if len(sensor_values) == self.count_sensors: - self.min_value = min(sensor_values) - self.max_value = max(sensor_values) - self.mean = round(sum(sensor_values) / self.count_sensors, - self._round_digits) - else: - self.min_value = self.max_value = self.mean = STATE_UNKNOWN + self.min_value = calc_min(sensor_values) + self.max_value = calc_max(sensor_values) + self.mean = calc_mean(sensor_values, self._round_digits) diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 5b30f52d926..f1449e5df06 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.modbus/ """ import logging +import struct import voluptuous as vol @@ -24,16 +25,28 @@ CONF_REGISTER = 'register' CONF_REGISTERS = 'registers' CONF_SCALE = 'scale' CONF_SLAVE = 'slave' +CONF_DATA_TYPE = 'data_type' +CONF_REGISTER_TYPE = 'register_type' + +REGISTER_TYPE_HOLDING = 'holding' +REGISTER_TYPE_INPUT = 'input' + +DATA_TYPE_INT = 'int' +DATA_TYPE_FLOAT = 'float' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_REGISTERS): [{ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, + vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): + vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), vol.Optional(CONF_COUNT, default=1): cv.positive_int, vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): + vol.In([DATA_TYPE_INT, DATA_TYPE_FLOAT]), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string }] }) @@ -47,10 +60,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): register.get(CONF_NAME), register.get(CONF_SLAVE), register.get(CONF_REGISTER), + register.get(CONF_REGISTER_TYPE), register.get(CONF_UNIT_OF_MEASUREMENT), register.get(CONF_COUNT), register.get(CONF_SCALE), register.get(CONF_OFFSET), + register.get(CONF_DATA_TYPE), register.get(CONF_PRECISION))) add_devices(sensors) @@ -58,17 +73,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ModbusRegisterSensor(Entity): """Modbus resgister sensor.""" - def __init__(self, name, slave, register, unit_of_measurement, count, - scale, offset, precision): + def __init__(self, name, slave, register, register_type, + unit_of_measurement, count, scale, offset, data_type, + precision): """Initialize the modbus register sensor.""" self._name = name self._slave = int(slave) if slave else None self._register = int(register) + self._register_type = register_type self._unit_of_measurement = unit_of_measurement self._count = int(count) self._scale = scale self._offset = offset self._precision = precision + self._data_type = data_type self._value = None @property @@ -88,16 +106,28 @@ class ModbusRegisterSensor(Entity): def update(self): """Update the state of the sensor.""" - result = modbus.HUB.read_holding_registers( - self._slave, - self._register, - self._count) + if self._register_type == REGISTER_TYPE_INPUT: + result = modbus.HUB.read_input_registers( + self._slave, + self._register, + self._count) + else: + result = modbus.HUB.read_holding_registers( + self._slave, + self._register, + self._count) val = 0 if not result: _LOGGER.error("No response from modbus slave %s register %s", self._slave, self._register) return - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) + if self._data_type == DATA_TYPE_FLOAT: + byte_string = b''.join( + [x.to_bytes(2, byteorder='big') for x in result.registers] + ) + val = struct.unpack(">f", byte_string)[0] + elif self._data_type == DATA_TYPE_INT: + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) self._value = format( self._scale * val + self._offset, '.{}f'.format(self._precision)) diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py index 7043da53222..c2f8c2be71f 100644 --- a/homeassistant/components/sensor/mvglive.py +++ b/homeassistant/components/sensor/mvglive.py @@ -10,146 +10,170 @@ from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ( + CONF_NAME, ATTR_ATTRIBUTION, STATE_UNKNOWN + ) REQUIREMENTS = ['PyMVGLive==1.1.3'] _LOGGER = logging.getLogger(__name__) -CONF_BUS = 'bus' -CONF_DEST = 'destination' -CONF_LINE = 'line' -CONF_OFFSET = 'offset' -CONF_SBAHN = 'sbahn' +CONF_NEXT_DEPARTURE = 'nextdeparture' + CONF_STATION = 'station' -CONF_TRAM = 'tram' -CONF_UBAHN = 'ubahn' +CONF_DESTINATIONS = 'destinations' +CONF_DIRECTIONS = 'directions' +CONF_LINES = 'lines' +CONF_PRODUCTS = 'products' +CONF_TIMEOFFSET = 'timeoffset' -ICON = 'mdi:bus' +ICONS = { + 'U-Bahn': 'mdi:subway', + 'Tram': 'mdi:tram', + 'Bus': 'mdi:bus', + 'S-Bahn': 'mdi:train', + 'SEV': 'mdi:checkbox-blank-circle-outline', + '-': 'mdi:clock' +} +ATTRIBUTION = "Data provided by MVG-live.de" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) +SCAN_INTERVAL = timedelta(seconds=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STATION): cv.string, - vol.Optional(CONF_DEST, default=None): cv.string, - vol.Optional(CONF_LINE, default=None): cv.string, - vol.Optional(CONF_OFFSET, default=0): cv.positive_int, - vol.Optional(CONF_UBAHN, default=True): cv.boolean, - vol.Optional(CONF_TRAM, default=True): cv.boolean, - vol.Optional(CONF_BUS, default=True): cv.boolean, - vol.Optional(CONF_SBAHN, default=True): cv.boolean, + vol.Optional(CONF_NEXT_DEPARTURE): [{ + vol.Required(CONF_STATION): cv.string, + vol.Optional(CONF_DESTINATIONS, default=['']): cv.ensure_list_csv, + vol.Optional(CONF_DIRECTIONS, default=['']): cv.ensure_list_csv, + vol.Optional(CONF_LINES, default=['']): cv.ensure_list_csv, + vol.Optional(CONF_PRODUCTS, + default=['U-Bahn', 'Tram', + 'Bus', 'S-Bahn']): cv.ensure_list_csv, + vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int, + vol.Optional(CONF_NAME): cv.string}] }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MVG Live Sensor.""" - station = config.get(CONF_STATION) - destination = config.get(CONF_DEST) - line = config.get(CONF_LINE) - offset = config.get(CONF_OFFSET) - ubahn = config.get(CONF_UBAHN) - tram = config.get(CONF_TRAM) - bus = config.get(CONF_BUS) - sbahn = config.get(CONF_SBAHN) - - add_devices([MVGLiveSensor( - station, destination, line, offset, ubahn, tram, bus, sbahn)], True) + """Get the MVGLive sensor.""" + sensors = [] + for nextdeparture in config.get(CONF_NEXT_DEPARTURE): + sensors.append( + MVGLiveSensor( + nextdeparture.get(CONF_STATION), + nextdeparture.get(CONF_DESTINATIONS), + nextdeparture.get(CONF_DIRECTIONS), + nextdeparture.get(CONF_LINES), + nextdeparture.get(CONF_PRODUCTS), + nextdeparture.get(CONF_TIMEOFFSET), + nextdeparture.get(CONF_NAME))) + add_devices(sensors, True) +# pylint: disable=too-few-public-methods class MVGLiveSensor(Entity): """Implementation of an MVG Live sensor.""" - def __init__(self, station, destination, line, - offset, ubahn, tram, bus, sbahn): + def __init__(self, station, destinations, directions, + lines, products, timeoffset, name): """Initialize the sensor.""" self._station = station - self._destination = destination - self._line = line - self.data = MVGLiveData(station, destination, line, - offset, ubahn, tram, bus, sbahn) + self._name = name + self.data = MVGLiveData(station, destinations, directions, + lines, products, timeoffset) self._state = STATE_UNKNOWN + self._icon = ICONS['-'] @property def name(self): """Return the name of the sensor.""" - # e.g. - # 'Hauptbahnhof (S1)' - # 'Hauptbahnhof-Marienplatz' - # 'Hauptbahnhof-Marienplatz (S1)' - namestr = self._station - if self._destination: - namestr = '{}-{}'.format(namestr, self._destination) - if self._line: - namestr = '{} ({})'.format(namestr, self._line) - return namestr - - @property - def icon(self): - """Return the icon for the frontend.""" - return ICON + if self._name: + return self._name + else: + return self._station @property def state(self): - """Return the departure time of the next train.""" + """Return the next departure time.""" return self._state @property def state_attributes(self): """Return the state attributes.""" - return self.data.nextdeparture + return self.data.departures + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return "min" def update(self): """Get the latest data and update the state.""" self.data.update() - if not self.data.nextdeparture: + if not self.data.departures: self._state = '-' + self._icon = ICONS['-'] else: - self._state = self.data.nextdeparture.get('time', '-') + self._state = self.data.departures.get('time', '-') + self._icon = ICONS[self.data.departures.get('product', '-')] class MVGLiveData(object): """Pull data from the mvg-live.de web page.""" - def __init__(self, station, destination, line, - offset, ubahn, tram, bus, sbahn): + def __init__(self, station, destinations, directions, + lines, products, timeoffset): """Initialize the sensor.""" import MVGLive self._station = station - self._destination = destination - self._line = line - self._offset = offset - self._ubahn = ubahn - self._tram = tram - self._bus = bus - self._sbahn = sbahn + self._destinations = destinations + self._directions = directions + self._lines = lines + self._products = products + self._timeoffset = timeoffset + self._include_ubahn = True if 'U-Bahn' in self._products else False + self._include_tram = True if 'Tram' in self._products else False + self._include_bus = True if 'Bus' in self._products else False + self._include_sbahn = True if 'S-Bahn' in self._products else False self.mvg = MVGLive.MVGLive() - self.nextdeparture = {} + self.departures = {} - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the connection data.""" try: - _departures = self.mvg.getlivedata( - station=self._station, ubahn=self._ubahn, tram=self._tram, - bus=self._bus, sbahn=self._sbahn) + _departures = self.mvg.getlivedata(station=self._station, + ubahn=self._include_ubahn, + tram=self._include_tram, + bus=self._include_bus, + sbahn=self._include_sbahn) except ValueError: - self.nextdeparture = {} + self.departures = {} _LOGGER.warning("Returned data not understood.") return for _departure in _departures: # find the first departure meeting the criteria - if not _departure['destination'].startswith(self._destination): + if ('' not in self._destinations[:1] and + _departure['destination'] not in self._destinations): continue - elif _departure['time'] < self._offset: + elif ('' not in self._directions[:1] and + _departure['direction'] not in self._directions): + continue + elif ('' not in self._lines[:1] and + _departure['linename'] not in self._lines): + continue + elif _departure['time'] < self._timeoffset: continue # now select the relevant data - _nextdep = {} + _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} for k in ['destination', 'linename', 'time', 'direction', 'product']: _nextdep[k] = _departure.get(k, '') _nextdep['time'] = int(_nextdep['time']) - self.nextdeparture = _nextdep + self.departures = _nextdep break diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py index ca5cff1d24a..7c33e481069 100644 --- a/homeassistant/components/sensor/neato.py +++ b/homeassistant/components/sensor/neato.py @@ -8,9 +8,12 @@ import logging import requests from homeassistant.helpers.entity import Entity from homeassistant.components.neato import ( - NEATO_ROBOTS, NEATO_LOGIN, ACTION, ERRORS, MODE, ALERTS) + NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) _LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + SENSOR_TYPE_STATUS = 'status' SENSOR_TYPE_BATTERY = 'battery' @@ -19,12 +22,17 @@ SENSOR_TYPES = { SENSOR_TYPE_BATTERY: ['Battery'] } +ATTR_CLEAN_START = 'clean_start' +ATTR_CLEAN_STOP = 'clean_stop' +ATTR_CLEAN_AREA = 'clean_area' +ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start' +ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end' +ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' +ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Neato sensor platform.""" - if not hass.data['neato_robots']: - return False - dev = [] for robot in hass.data[NEATO_ROBOTS]: for type_name in SENSOR_TYPES: @@ -42,22 +50,37 @@ class NeatoConnectedSensor(Entity): self.robot = robot self.neato = hass.data[NEATO_LOGIN] self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0] - self._state = self.robot.state - self._battery_state = None self._status_state = None + try: + self._state = self.robot.state + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + self._state = None + _LOGGER.warning('Neato connection error: %s', ex) + self._mapdata = hass.data[NEATO_MAP_DATA] + self.clean_time_start = None + self.clean_time_stop = None + self.clean_area = None + self.clean_battery_start = None + self.clean_battery_end = None + self.clean_suspension_charge_count = None + self.clean_suspension_time = None + self._battery_state = None def update(self): """Update the properties of sensor.""" _LOGGER.debug('Update of sensor') self.neato.update_robots() - if not self._state: - return + self._mapdata = self.hass.data[NEATO_MAP_DATA] try: self._state = self.robot.state - except requests.exceptions.HTTPError as ex: + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: self._state = None self._status_state = 'Offline' - _LOGGER.debug('Neato connection issue: %s', ex) + _LOGGER.warning('Neato connection error: %s', ex) + return + if not self._state: return _LOGGER.debug('self._state=%s', self._state) if self.type == SENSOR_TYPE_STATUS: @@ -82,6 +105,27 @@ class NeatoConnectedSensor(Entity): self._status_state = ERRORS.get(self._state['error']) if self.type == SENSOR_TYPE_BATTERY: self._battery_state = self._state['details']['charge'] + if self._mapdata is None: + return + self.clean_time_start = ( + (self._mapdata[self.robot.serial]['maps'][0]['start_at'] + .strip('Z')) + .replace('T', ' ')) + self.clean_time_stop = ( + (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z')) + .replace('T', ' ')) + self.clean_area = ( + self._mapdata[self.robot.serial]['maps'][0]['cleaned_area']) + self.clean_suspension_charge_count = ( + self._mapdata[self.robot.serial]['maps'][0] + ['suspended_cleaning_charging_count']) + self.clean_suspension_time = ( + self._mapdata[self.robot.serial]['maps'][0] + ['time_in_suspended_cleaning']) + self.clean_battery_start = ( + self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start']) + self.clean_battery_end = ( + self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end']) @property def unit_of_measurement(self): @@ -109,3 +153,25 @@ class NeatoConnectedSensor(Entity): def name(self): """Return the name of the sensor.""" return self._robot_name + + @property + def device_state_attributes(self): + """Return the device specific attributes.""" + data = {} + if self.type is SENSOR_TYPE_STATUS: + if self.clean_time_start: + data[ATTR_CLEAN_START] = self.clean_time_start + if self.clean_time_stop: + data[ATTR_CLEAN_STOP] = self.clean_time_stop + if self.clean_area: + data[ATTR_CLEAN_AREA] = self.clean_area + if self.clean_suspension_charge_count: + data[ATTR_CLEAN_SUSP_COUNT] = ( + self.clean_suspension_charge_count) + if self.clean_suspension_time: + data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time + if self.clean_battery_start: + data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start + if self.clean_battery_end: + data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + return data diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py new file mode 100644 index 00000000000..17e5f1f351c --- /dev/null +++ b/homeassistant/components/sensor/opensky.py @@ -0,0 +1,141 @@ +""" +Sensor for the Open Sky Network. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.opensky/ +""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + LENGTH_KILOMETERS, LENGTH_METERS) +from homeassistant.helpers.entity import Entity +from homeassistant.util import distance as util_distance +from homeassistant.util import location as util_location +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds +DOMAIN = 'opensky' +EVENT_OPENSKY_ENTRY = '{}_entry'.format(DOMAIN) +EVENT_OPENSKY_EXIT = '{}_exit'.format(DOMAIN) +CONF_RADIUS = 'radius' +ATTR_SENSOR = 'sensor' +ATTR_STATES = 'states' +ATTR_ON_GROUND = 'on_ground' +ATTR_CALLSIGN = 'callsign' +OPENSKY_ATTRIBUTION = "Information provided by the OpenSky Network "\ + "(https://opensky-network.org)" +OPENSKY_API_URL = 'https://opensky-network.org/api/states/all' +OPENSKY_API_FIELDS = [ + 'icao24', ATTR_CALLSIGN, 'origin_country', 'time_position', + 'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, 'altitude', + ATTR_ON_GROUND, 'velocity', 'heading', 'vertical_rate', 'sensors'] + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_NAME): cv.string, + vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Open Sky platform.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + add_devices([OpenSkySensor( + hass, config.get(CONF_NAME, DOMAIN), latitude, longitude, + config.get(CONF_RADIUS))], True) + + +class OpenSkySensor(Entity): + """Open Sky Network Sensor.""" + + def __init__(self, hass, name, latitude, longitude, radius): + """Initialize the sensor.""" + self._session = requests.Session() + self._latitude = latitude + self._longitude = longitude + self._radius = util_distance.convert( + radius, LENGTH_KILOMETERS, LENGTH_METERS) + self._state = 0 + self._hass = hass + self._name = name + self._previously_tracked = 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 + + def _handle_boundary(self, callsigns, event): + """Handle flights crossing region boundary.""" + for callsign in callsigns: + data = { + ATTR_CALLSIGN: callsign, + ATTR_SENSOR: self._name + } + self._hass.bus.fire(event, data) + + def update(self): + """Update device state.""" + currently_tracked = set() + states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES) + for state in states: + data = dict(zip(OPENSKY_API_FIELDS, state)) + missing_location = ( + data.get(ATTR_LONGITUDE) is None or + data.get(ATTR_LATITUDE) is None) + if missing_location: + continue + if data.get(ATTR_ON_GROUND): + continue + distance = util_location.distance( + self._latitude, self._longitude, + data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE)) + if distance is None or distance > self._radius: + continue + callsign = data[ATTR_CALLSIGN].strip() + if callsign == '': + continue + currently_tracked.add(callsign) + if self._previously_tracked is not None: + entries = currently_tracked - self._previously_tracked + exits = self._previously_tracked - currently_tracked + self._handle_boundary(entries, EVENT_OPENSKY_ENTRY) + self._handle_boundary(exits, EVENT_OPENSKY_EXIT) + self._state = len(currently_tracked) + self._previously_tracked = currently_tracked + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION + } + + @property + def unit_of_measurement(self): + """Unit of measurement.""" + return 'flights' + + @property + def icon(self): + """Icon.""" + return 'mdi:airplane' diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index b37cb432461..e57c59e41d6 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv import voluptuous as vol -REQUIREMENTS = ['qnapstats==0.2.3'] +REQUIREMENTS = ['qnapstats==0.2.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 76dbbe4ed39..5035e2464b3 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change from homeassistant.helpers.restore_state import async_get_last_state -REQUIREMENTS = ['speedtest-cli==1.0.3'] +REQUIREMENTS = ['speedtest-cli==1.0.4'] _LOGGER = logging.getLogger(__name__) _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+' diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 4b402d32792..fee12c31acf 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.2.1'] +REQUIREMENTS = ['psutil==5.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index 08617e824ec..2830b8c98e9 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['uber_rides==0.2.7'] +REQUIREMENTS = ['uber_rides==0.4.1'] _LOGGER = logging.getLogger(__name__) @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_START_LONGITUDE): cv.longitude, vol.Optional(CONF_END_LATITUDE): cv.latitude, vol.Optional(CONF_END_LONGITUDE): cv.longitude, - vol.Optional(CONF_PRODUCT_IDS, default=[]): + vol.Optional(CONF_PRODUCT_IDS): vol.All(cv.ensure_list, [cv.string]), }) diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index 4d4e0601ca5..415ff1f8745 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_date import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['upsmychoice==1.0.1'] +REQUIREMENTS = ['upsmychoice==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/usps.py b/homeassistant/components/sensor/usps.py index 5c2bb84daa1..c0fc3d9cfe4 100644 --- a/homeassistant/components/sensor/usps.py +++ b/homeassistant/components/sensor/usps.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_datetime import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['myusps==1.0.3'] +REQUIREMENTS = ['myusps==1.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 81f69001f82..e0fa16d7907 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -10,6 +10,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.util import convert from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) @@ -49,6 +50,8 @@ class VeraSensor(VeraDevice, Entity): return 'lux' elif self.vera_device.category == "Humidity Sensor": return '%' + elif self.vera_device.category == "Power meter": + return 'watts' def update(self): """Update the state.""" @@ -67,6 +70,9 @@ class VeraSensor(VeraDevice, Entity): self.current_value = self.vera_device.light elif self.vera_device.category == "Humidity Sensor": self.current_value = self.vera_device.humidity + elif self.vera_device.category == "Power meter": + power = convert(self.vera_device.power, float, 0) + self.current_value = int(round(power, 0)) elif self.vera_device.category == "Sensor": tripped = self.vera_device.is_tripped self.current_value = 'Tripped' if tripped else 'Not Tripped' diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index cf5999200d8..6cff6d5f4f4 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -338,6 +338,9 @@ hassio: description: Optional or it will be use the latest version. example: '0.3' + supervisor_reload: + description: Reload HassIO supervisor addons/updates/configs. + homeassistant_update: description: Update HomeAssistant docker image. fields: diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 19f0b0fabba..f5fb0115a43 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -30,6 +30,10 @@ STATE_BELOW_HORIZON = 'below_horizon' STATE_ATTR_AZIMUTH = 'azimuth' STATE_ATTR_ELEVATION = 'elevation' +STATE_ATTR_NEXT_DAWN = 'next_dawn' +STATE_ATTR_NEXT_DUSK = 'next_dusk' +STATE_ATTR_NEXT_MIDNIGHT = 'next_midnight' +STATE_ATTR_NEXT_NOON = 'next_noon' STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' @@ -47,6 +51,118 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) +def next_dawn(hass, entity_id=None): + """Local datetime object of the next dawn. + + Async friendly. + """ + utc_next = next_dawn_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_dawn_utc(hass, entity_id=None): + """UTC datetime object of the next dawn. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_DAWN]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_DAWN does not exist + return None + + +def next_dusk(hass, entity_id=None): + """Local datetime object of the next dusk. + + Async friendly. + """ + utc_next = next_dusk_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_dusk_utc(hass, entity_id=None): + """UTC datetime object of the next dusk. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_DUSK]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_DUSK does not exist + return None + + +def next_midnight(hass, entity_id=None): + """Local datetime object of the next midnight. + + Async friendly. + """ + utc_next = next_midnight_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_midnight_utc(hass, entity_id=None): + """UTC datetime object of the next midnight. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_MIDNIGHT]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_MIDNIGHT does not exist + return None + + +def next_noon(hass, entity_id=None): + """Local datetime object of the next solar noon. + + Async friendly. + """ + utc_next = next_noon_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_noon_utc(hass, entity_id=None): + """UTC datetime object of the next noon. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_NOON]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_NOON does not exist + return None + + def next_setting(hass, entity_id=None): """Local datetime object of the next sun setting. @@ -153,6 +269,8 @@ class Sun(Entity): self.hass = hass self.location = location self._state = self.next_rising = self.next_setting = None + self.next_dawn = self.next_dusk = None + self.next_midnight = self.next_noon = None self.solar_elevation = self.solar_azimuth = 0 track_utc_time_change(hass, self.timer_update, second=30) @@ -174,6 +292,10 @@ class Sun(Entity): def state_attributes(self): """Return the state attributes of the sun.""" return { + STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), + STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), + STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), + STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), STATE_ATTR_ELEVATION: round(self.solar_elevation, 2), @@ -183,36 +305,41 @@ class Sun(Entity): @property def next_change(self): """Datetime when the next change to the state is.""" - return min(self.next_rising, self.next_setting) + return min(self.next_dawn, self.next_dusk, self.next_midnight, + self.next_noon, self.next_rising, self.next_setting) - def update_as_of(self, utc_point_in_time): + @staticmethod + def get_next_solar_event(callable_on_astral_location, + utc_point_in_time, mod, increment): """Calculate sun state at a point in UTC time.""" import astral - mod = -1 while True: try: - next_rising_dt = self.location.sunrise( + next_dt = callable_on_astral_location( utc_point_in_time + timedelta(days=mod), local=False) - if next_rising_dt > utc_point_in_time: + if next_dt > utc_point_in_time: break except astral.AstralError: pass - mod += 1 + mod += increment - mod = -1 - while True: - try: - next_setting_dt = (self.location.sunset( - utc_point_in_time + timedelta(days=mod), local=False)) - if next_setting_dt > utc_point_in_time: - break - except astral.AstralError: - pass - mod += 1 + return next_dt - self.next_rising = next_rising_dt - self.next_setting = next_setting_dt + def update_as_of(self, utc_point_in_time): + """Update the attributes containing solar events.""" + self.next_dawn = Sun.get_next_solar_event( + self.location.dawn, utc_point_in_time, -1, 1) + self.next_dusk = Sun.get_next_solar_event( + self.location.dusk, utc_point_in_time, -1, 1) + self.next_midnight = Sun.get_next_solar_event( + self.location.solar_midnight, utc_point_in_time, -1, 1) + self.next_noon = Sun.get_next_solar_event( + self.location.solar_noon, utc_point_in_time, -1, 1) + self.next_rising = Sun.get_next_solar_event( + self.location.sunrise, utc_point_in_time, -1, 1) + self.next_setting = Sun.get_next_solar_event( + self.location.sunset, utc_point_in_time, -1, 1) def update_sun_position(self, utc_point_in_time): """Calculate the position of the sun.""" diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py index 515900dd2df..e813da43dfa 100644 --- a/homeassistant/components/switch/mystrom.py +++ b/homeassistant/components/switch/mystrom.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-mystrom==0.3.6'] +REQUIREMENTS = ['python-mystrom==0.3.8'] DEFAULT_NAME = 'myStrom Switch' diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 6cd5c5088dc..b6cf6549cae 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -12,6 +12,8 @@ from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['neato'] + SWITCH_TYPE_CLEAN = 'clean' SWITCH_TYPE_SCHEDULE = 'scedule' @@ -23,9 +25,6 @@ SWITCH_TYPES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Neato switches.""" - if not hass.data[NEATO_ROBOTS]: - return False - dev = [] for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: @@ -43,7 +42,12 @@ class NeatoConnectedSwitch(ToggleEntity): self.robot = robot self.neato = hass.data[NEATO_LOGIN] self._robot_name = self.robot.name + ' ' + SWITCH_TYPES[self.type][0] - self._state = self.robot.state + try: + self._state = self.robot.state + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning('Neato connection error: %s', ex) + self._state = None self._schedule_state = None self._clean_state = None @@ -51,14 +55,13 @@ class NeatoConnectedSwitch(ToggleEntity): """Update the states of Neato switches.""" _LOGGER.debug('Running switch update') self.neato.update_robots() - if not self._state: - return try: self._state = self.robot.state - except requests.exceptions.HTTPError: + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning('Neato connection error: %s', ex) self._state = None return - self._state = self.robot.state _LOGGER.debug('self._state=%s', self._state) if self.type == SWITCH_TYPE_CLEAN: if (self.robot.state['action'] == 1 or diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index c86a4674bb6..4413da191d3 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) CONF_MAC_ADDRESS = 'mac_address' CONF_OFF_ACTION = 'turn_off' +CONF_BROADCAST_ADDRESS = 'broadcast_address' DEFAULT_NAME = 'Wake on LAN' DEFAULT_PING_TIMEOUT = 1 @@ -28,6 +29,7 @@ DEFAULT_PING_TIMEOUT = 1 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC_ADDRESS): cv.string, vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, }) @@ -38,21 +40,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) host = config.get(CONF_HOST) mac_address = config.get(CONF_MAC_ADDRESS) + broadcast_address = config.get(CONF_BROADCAST_ADDRESS) off_action = config.get(CONF_OFF_ACTION) - add_devices([WOLSwitch(hass, name, host, mac_address, off_action)]) + add_devices([WOLSwitch(hass, name, host, mac_address, + off_action, broadcast_address)]) class WOLSwitch(SwitchDevice): """Representation of a wake on lan switch.""" - def __init__(self, hass, name, host, mac_address, off_action): + def __init__(self, hass, name, host, mac_address, + off_action, broadcast_address): """Initialize the WOL switch.""" from wakeonlan import wol self._hass = hass self._name = name self._host = host self._mac_address = mac_address + self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False self._wol = wol @@ -75,9 +81,9 @@ class WOLSwitch(SwitchDevice): def turn_on(self): """Turn the device on.""" - if self._host: + if self._broadcast_address: self._wol.send_magic_packet(self._mac_address, - ip_address=self._host) + ip_address=self._broadcast_address) else: self._wol.send_magic_packet(self._mac_address) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 0e40c3eff3b..c92523ad705 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -36,8 +36,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo.discovery as discovery if discovery_info is not None: - location = discovery_info[2] - mac = discovery_info[3] + location = discovery_info['ssdp_description'] + mac = discovery_info['mac_address'] device = discovery.device_from_description(location, mac) if device: diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py new file mode 100644 index 00000000000..92a87153d99 --- /dev/null +++ b/homeassistant/components/telegram_bot/__init__.py @@ -0,0 +1,131 @@ +""" +Component to receive telegram messages. + +Either by polling or webhook. +""" + +import asyncio +import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PLATFORM, CONF_API_KEY +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import discovery, config_per_platform +from homeassistant.setup import async_prepare_setup_platform + +DOMAIN = 'telegram_bot' + +_LOGGER = logging.getLogger(__name__) + +EVENT_TELEGRAM_COMMAND = 'telegram_command' +EVENT_TELEGRAM_TEXT = 'telegram_text' + +ATTR_COMMAND = 'command' +ATTR_USER_ID = 'user_id' +ATTR_ARGS = 'args' +ATTR_FROM_FIRST = 'from_first' +ATTR_FROM_LAST = 'from_last' +ATTR_TEXT = 'text' + +CONF_ALLOWED_CHAT_IDS = 'allowed_chat_ids' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ALLOWED_CHAT_IDS): vol.All(cv.ensure_list, + [cv.positive_int]) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the telegram bot component.""" + @asyncio.coroutine + def async_setup_platform(p_type, p_config=None, discovery_info=None): + """Setup a telegram bot platform.""" + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, p_type) + + if platform is None: + _LOGGER.error("Unknown notification service specified") + return + + _LOGGER.info("Setting up1 %s.%s", DOMAIN, p_type) + + try: + if hasattr(platform, 'async_setup_platform'): + notify_service = yield from \ + platform.async_setup_platform(hass, p_config, + discovery_info) + elif hasattr(platform, 'setup_platform'): + notify_service = yield from hass.loop.run_in_executor( + None, platform.setup_platform, hass, p_config, + discovery_info) + else: + raise HomeAssistantError("Invalid telegram bot platform.") + + if notify_service is None: + _LOGGER.error( + "Failed to initialize telegram bot %s", p_type) + return + + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error setting up platform %s', p_type) + return + + return True + + setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config + in config_per_platform(config, DOMAIN)] + + if setup_tasks: + yield from asyncio.wait(setup_tasks, loop=hass.loop) + + @asyncio.coroutine + def async_platform_discovered(platform, info): + """Callback to load a platform.""" + yield from async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + + return True + + +class BaseTelegramBotEntity: + """The base class for the telegram bot.""" + + def __init__(self, hass, allowed_chat_ids): + """Initialize the bot base class.""" + self.allowed_chat_ids = allowed_chat_ids + self.hass = hass + + def process_message(self, data): + """Check for basic message rules and fire an event if message is ok.""" + data = data.get('message') + + if (not data + or 'from' not in data + or 'text' not in data + or data['from'].get('id') not in self.allowed_chat_ids): + # Message is not correct. + _LOGGER.error("Incoming message does not have required data.") + return False + + event = EVENT_TELEGRAM_COMMAND + event_data = { + ATTR_USER_ID: data['from']['id'], + ATTR_FROM_FIRST: data['from']['first_name'], + ATTR_FROM_LAST: data['from']['last_name']} + + if data['text'][0] == '/': + pieces = data['text'].split(' ') + event_data[ATTR_COMMAND] = pieces[0] + event_data[ATTR_ARGS] = pieces[1:] + + else: + event_data[ATTR_TEXT] = data['text'] + event = EVENT_TELEGRAM_TEXT + + self.hass.bus.async_fire(event, event_data) + + return True diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py new file mode 100644 index 00000000000..3e0dfa89375 --- /dev/null +++ b/homeassistant/components/telegram_bot/polling.py @@ -0,0 +1,121 @@ +"""Telegram bot polling implementation.""" + +import asyncio +from asyncio.futures import CancelledError +import logging + +import async_timeout +from aiohttp.client_exceptions import ClientError + +from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \ + BaseTelegramBotEntity, PLATFORM_SCHEMA +from homeassistant.const import EVENT_HOMEASSISTANT_START, \ + EVENT_HOMEASSISTANT_STOP, CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['python-telegram-bot==5.3.0'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the polling platform.""" + import telegram + bot = telegram.Bot(config[CONF_API_KEY]) + pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) + + @callback + def _start_bot(_event): + """Start the bot.""" + pol.start_polling() + + @callback + def _stop_bot(_event): + """Stop the bot.""" + pol.stop_polling() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, + _start_bot + ) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + _stop_bot + ) + + return True + + +class TelegramPoll(BaseTelegramBotEntity): + """asyncio telegram incoming message handler.""" + + def __init__(self, bot, hass, allowed_chat_ids): + """Initialize the polling instance.""" + BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) + self.update_id = 0 + self.websession = async_get_clientsession(hass) + self.update_url = '{0}/getUpdates'.format(bot.base_url) + self.polling_task = None # The actuall polling task. + self.timeout = 15 # async post timeout + # polling timeout should always be less than async post timeout. + self.post_data = {'timeout': self.timeout - 5} + + def start_polling(self): + """Start the polling task.""" + self.polling_task = self.hass.async_add_job(self.check_incoming()) + + def stop_polling(self): + """Stop the polling task.""" + self.polling_task.cancel() + + @asyncio.coroutine + def get_updates(self, offset): + """Bypass the default long polling method to enable asyncio.""" + resp = None + _json = [] # The actual value to be returned. + + if offset: + self.post_data['offset'] = offset + try: + with async_timeout.timeout(self.timeout, loop=self.hass.loop): + resp = yield from self.websession.post( + self.update_url, data=self.post_data, + headers={'connection': 'keep-alive'} + ) + if resp.status != 200: + _LOGGER.error("Error %s on %s", resp.status, self.update_url) + _json = yield from resp.json() + except ValueError: + _LOGGER.error("Error parsing Json message") + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Client connection error") + finally: + if resp is not None: + yield from resp.release() + + return _json + + @asyncio.coroutine + def handle(self): + """" Receiving and processing incoming messages.""" + _updates = yield from self.get_updates(self.update_id) + for update in _updates['result']: + self.update_id = update['update_id'] + 1 + self.process_message(update) + + @asyncio.coroutine + def check_incoming(self): + """"Loop which continuously checks for incoming telegram messages.""" + try: + while True: + # Each handle call sends a long polling post request + # to the telegram server. If no incoming message it will return + # an empty list. Calling self.handle() without any delay or + # timeout will for this reason not really stress the processor. + yield from self.handle() + except CancelledError: + _LOGGER.debug("Stopping telegram polling bot") diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py new file mode 100644 index 00000000000..3ffc03780bd --- /dev/null +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -0,0 +1,97 @@ +""" +Allows utilizing telegram webhooks. + +See https://core.telegram.org/bots/webhooks for details + about webhooks. + +""" +import asyncio +import logging +from ipaddress import ip_network + +import voluptuous as vol + + +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) +import homeassistant.helpers.config_validation as cv +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \ + BaseTelegramBotEntity, PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY +from homeassistant.components.http.util import get_real_ip + +DEPENDENCIES = ['http'] +REQUIREMENTS = ['python-telegram-bot==5.3.0'] + +_LOGGER = logging.getLogger(__name__) + +TELEGRAM_HANDLER_URL = '/api/telegram_webhooks' + +CONF_TRUSTED_NETWORKS = 'trusted_networks' +DEFAULT_TRUSTED_NETWORKS = [ + ip_network('149.154.167.197/32'), + ip_network('149.154.167.198/31'), + ip_network('149.154.167.200/29'), + ip_network('149.154.167.208/28'), + ip_network('149.154.167.224/29'), + ip_network('149.154.167.232/31') +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): + vol.All(cv.ensure_list, [ip_network]) +}) + + +def setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the polling platform.""" + import telegram + bot = telegram.Bot(config[CONF_API_KEY]) + + current_status = bot.getWebhookInfo() + handler_url = "{0}{1}".format(hass.config.api.base_url, + TELEGRAM_HANDLER_URL) + if current_status and current_status['url'] != handler_url: + if bot.setWebhook(handler_url): + _LOGGER.info("set new telegram webhook %s", handler_url) + + hass.http.register_view( + BotPushReceiver( + hass, + config[CONF_ALLOWED_CHAT_IDS], + config[CONF_TRUSTED_NETWORKS])) + + else: + _LOGGER.error("set telegram webhook failed %s", handler_url) + + +class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): + """Handle pushes from telegram.""" + + requires_auth = False + url = TELEGRAM_HANDLER_URL + name = "telegram_webhooks" + + def __init__(self, hass, allowed_chat_ids, trusted_networks): + """Initialize the class.""" + BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) + self.trusted_networks = trusted_networks + + @asyncio.coroutine + def post(self, request): + """Accept the POST from telegram.""" + real_ip = get_real_ip(request) + if not any(real_ip in net for net in self.trusted_networks): + _LOGGER.warning("Access denied from %s", real_ip) + return self.json_message('Access denied', HTTP_UNAUTHORIZED) + + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + if not self.process_message(data): + return self.json_message('Invalid message', HTTP_BAD_REQUEST) + else: + return self.json({}) diff --git a/homeassistant/components/telegram_webhooks.py b/homeassistant/components/telegram_webhooks.py deleted file mode 100644 index f952145f822..00000000000 --- a/homeassistant/components/telegram_webhooks.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -Allows utilizing telegram webhooks. - -See https://core.telegram.org/bots/webhooks for details - about webhooks. - -""" -import asyncio -import logging -from ipaddress import ip_network - -import voluptuous as vol - -from homeassistant.const import ( - HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) -import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_API_KEY -from homeassistant.components.http.util import get_real_ip - -DOMAIN = 'telegram_webhooks' -DEPENDENCIES = ['http'] -REQUIREMENTS = ['python-telegram-bot==5.3.0'] - -_LOGGER = logging.getLogger(__name__) - -EVENT_TELEGRAM_COMMAND = 'telegram_command' -EVENT_TELEGRAM_TEXT = 'telegram_text' - -TELEGRAM_HANDLER_URL = '/api/telegram_webhooks' - -CONF_USER_ID = 'user_id' -CONF_TRUSTED_NETWORKS = 'trusted_networks' -DEFAULT_TRUSTED_NETWORKS = [ - ip_network('149.154.167.197/32'), - ip_network('149.154.167.198/31'), - ip_network('149.154.167.200/29'), - ip_network('149.154.167.208/28'), - ip_network('149.154.167.224/29'), - ip_network('149.154.167.232/31') -] - -ATTR_COMMAND = 'command' -ATTR_TEXT = 'text' -ATTR_USER_ID = 'user_id' -ATTR_ARGS = 'args' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): - vol.All(cv.ensure_list, [ip_network]), - vol.Required(CONF_USER_ID): {cv.string: cv.positive_int}, - }), -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Setup the telegram_webhooks component. - - register webhook if API_KEY is specified - register /api/telegram_webhooks as web service for telegram bot - """ - import telegram - - conf = config[DOMAIN] - - if CONF_API_KEY in conf: - bot = telegram.Bot(conf[CONF_API_KEY]) - current_status = bot.getWebhookInfo() - _LOGGER.debug("telegram webhook status: %s", current_status) - handler_url = "{0}{1}".format(hass.config.api.base_url, - TELEGRAM_HANDLER_URL) - if current_status and current_status['url'] != handler_url: - if bot.setWebhook(handler_url): - _LOGGER.info("set new telegram webhook %s", handler_url) - else: - _LOGGER.error("set telegram webhook failed %s", handler_url) - - hass.http.register_view(BotPushReceiver(conf[CONF_USER_ID], - conf[CONF_TRUSTED_NETWORKS])) - return True - - -class BotPushReceiver(HomeAssistantView): - """Handle pushes from telegram.""" - - requires_auth = False - url = TELEGRAM_HANDLER_URL - name = "telegram_webhooks" - - def __init__(self, user_id_array, trusted_networks): - """Initialize users allowed to send messages to bot.""" - self.trusted_networks = trusted_networks - self.users = {user_id: dev_id for dev_id, user_id in - user_id_array.items()} - _LOGGER.debug("users allowed: %s", self.users) - - @asyncio.coroutine - def post(self, request): - """Accept the POST from telegram.""" - real_ip = get_real_ip(request) - if not any(real_ip in net for net in self.trusted_networks): - _LOGGER.warning("Access denied from %s", real_ip) - return self.json_message('Access denied', HTTP_UNAUTHORIZED) - - try: - data = yield from request.json() - except ValueError: - _LOGGER.error("Received telegram data: %s", data) - return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - - # check for basic message rules - data = data.get('message') - if not data or 'from' not in data or 'text' not in data: - return self.json({}) - - if data['from'].get('id') not in self.users: - _LOGGER.warning("User not allowed") - return self.json_message('Invalid user', HTTP_BAD_REQUEST) - - _LOGGER.debug("Received telegram data: %s", data) - if not data['text']: - _LOGGER.warning('no text') - return self.json({}) - - if data['text'][:1] == '/': - # telegram command "/blabla arg1 arg2 ..." - pieces = data['text'].split(' ') - - request.app['hass'].bus.async_fire(EVENT_TELEGRAM_COMMAND, { - ATTR_COMMAND: pieces[0], - ATTR_ARGS: " ".join(pieces[1:]), - ATTR_USER_ID: data['from']['id'], - }) - - # telegram text "bla bla" - request.app['hass'].bus.async_fire(EVENT_TELEGRAM_TEXT, { - ATTR_TEXT: data['text'], - ATTR_USER_ID: data['from']['id'], - }) - - return self.json({}) diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py new file mode 100644 index 00000000000..098b1788742 --- /dev/null +++ b/homeassistant/components/tradfri.py @@ -0,0 +1,141 @@ +""" +Support for Ikea Tradfri. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ikea_tradfri/ +""" +import asyncio +import json +import logging +import os + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_HOST, CONF_API_KEY +from homeassistant.loader import get_component +from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI + +DOMAIN = 'tradfri' +CONFIG_FILE = 'tradfri.conf' +KEY_CONFIG = 'tradfri_configuring' +KEY_GATEWAY = 'tradfri_gateway' +REQUIREMENTS = ['pytradfri==1.0'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Inclusive(CONF_HOST, 'gateway'): cv.string, + vol.Inclusive(CONF_API_KEY, 'gateway'): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +def request_configuration(hass, config, host): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + hass.data.setdefault(KEY_CONFIG, {}) + instance = hass.data[KEY_CONFIG].get(host) + + # Configuration already in progress + if instance: + return + + @asyncio.coroutine + def configuration_callback(callback_data): + """Called when config is submitted.""" + res = yield from _setup_gateway(hass, config, host, + callback_data.get('key')) + if not res: + hass.async_add_job(configurator.notify_errors, instance, + "Unable to connect.") + return + + def success(): + """Set up was successful.""" + conf = _read_config(hass) + conf[host] = {'key': callback_data.get('key')} + _write_config(hass, conf) + hass.async_add_job(configurator.request_done, instance) + + hass.async_add_job(success) + + instance = configurator.request_config( + hass, "IKEA Trådfri", configuration_callback, + description='Please enter the security code written at the bottom of ' + 'your IKEA Trådfri Gateway.', + submit_caption="Confirm", + fields=[{'id': 'key', 'name': 'Security Code', 'type': 'password'}] + ) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup Tradfri.""" + conf = config.get(DOMAIN, {}) + host = conf.get(CONF_HOST) + key = conf.get(CONF_API_KEY) + + @asyncio.coroutine + def gateway_discovered(service, info): + """Called when a gateway is discovered.""" + keys = yield from hass.async_add_job(_read_config, hass) + host = info['host'] + + if host in keys: + yield from _setup_gateway(hass, config, host, keys[host]['key']) + else: + hass.async_add_job(request_configuration, hass, config, host) + + discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered) + + if host is None: + return True + + return (yield from _setup_gateway(hass, config, host, key)) + + +@asyncio.coroutine +def _setup_gateway(hass, hass_config, host, key): + """Create a gateway.""" + from pytradfri import cli_api_factory, Gateway, RequestError, retry_timeout + + try: + api = retry_timeout(cli_api_factory(host, key)) + except RequestError: + return False + + gateway = Gateway(api) + gateway_id = gateway.get_gateway_info().id + hass.data.setdefault(KEY_GATEWAY, {}) + gateways = hass.data[KEY_GATEWAY] + + # Check if already set up + if gateway_id in gateways: + return True + + gateways[gateway_id] = gateway + hass.async_add_job(discovery.async_load_platform( + hass, 'light', DOMAIN, {'gateway': gateway_id}, hass_config)) + return True + + +def _read_config(hass): + """Read tradfri config.""" + path = hass.config.path(CONFIG_FILE) + + if not os.path.isfile(path): + return {} + + with open(path) as f_handle: + # Guard against empty file + return json.loads(f_handle.read() or '{}') + + +def _write_config(hass, config): + """Write tradfri config.""" + data = json.dumps(config) + with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile: + outfile.write(data) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index d433aef8c47..e88022a8ba9 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -87,7 +87,7 @@ class GoogleProvider(Provider): url_param = { 'ie': 'UTF-8', 'tl': language, - 'q': yarl.quote(part), + 'q': yarl.quote(part, strict=False), 'tk': part_token, 'total': len(message_parts), 'idx': idx, diff --git a/homeassistant/components/tts/marytts.py b/homeassistant/components/tts/marytts.py new file mode 100644 index 00000000000..ffb6950d79b --- /dev/null +++ b/homeassistant/components/tts/marytts.py @@ -0,0 +1,117 @@ +""" +Support for the MaryTTS service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts.marytts/ +""" +import asyncio +import logging +import re + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_LANGUAGES = [ + 'de', 'en-GB', 'en-US', 'fr', 'it', 'lb', 'ru', 'sv', 'te', 'tr' +] + +SUPPORT_VOICES = [ + 'cmu-slt-hsmm' +] + +SUPPORT_CODEC = [ + 'aiff', 'au', 'wav' +] + +CONF_VOICE = 'voice' +CONF_CODEC = 'codec' + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 59125 +DEFAULT_LANG = 'en-US' +DEFAULT_VOICE = 'cmu-slt-hsmm' +DEFAULT_CODEC = 'wav' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORT_VOICES), + vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC) +}) + + +@asyncio.coroutine +def async_get_engine(hass, config): + """Setup MaryTTS speech component.""" + return MaryTTSProvider(hass, config) + + +class MaryTTSProvider(Provider): + """MaryTTS speech api provider.""" + + def __init__(self, hass, conf): + """Init MaryTTS TTS service.""" + self.hass = hass + self._host = conf.get(CONF_HOST) + self._port = conf.get(CONF_PORT) + self._codec = conf.get(CONF_CODEC) + self._voice = conf.get(CONF_VOICE) + self._language = conf.get(CONF_LANG) + self.name = 'MaryTTS' + + @property + def default_language(self): + """Default language.""" + return self._language + + @property + def supported_languages(self): + """List of supported languages.""" + return SUPPORT_LANGUAGES + + @asyncio.coroutine + def async_get_tts_audio(self, message, language, options=None): + """Load TTS from MaryTTS.""" + websession = async_get_clientsession(self.hass) + + actual_language = re.sub('-', '_', language) + + try: + with async_timeout.timeout(10, loop=self.hass.loop): + url = 'http://{}:{}/process?'.format(self._host, self._port) + + audio = self._codec.upper() + if audio == 'WAV': + audio = 'WAVE' + + url_param = { + 'INPUT_TEXT': message, + 'INPUT_TYPE': 'TEXT', + 'AUDIO': audio, + 'VOICE': self._voice, + 'OUTPUT_TYPE': 'AUDIO', + 'LOCALE': actual_language + } + + request = yield from websession.get(url, params=url_param) + + if request.status != 200: + _LOGGER.error("Error %d on load url %s.", + request.status, request.url) + return (None, None) + data = yield from request.read() + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout for MaryTTS API.") + return (None, None) + + return (self._codec, data) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 555a800708c..9d7494147e0 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -20,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.25'] +REQUIREMENTS = ['pyvera==0.2.26'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +35,7 @@ CONF_LIGHTS = 'lights' VERA_ID_FORMAT = '{}_{}' ATTR_CURRENT_POWER_W = "current_power_w" +ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" VERA_DEVICES = defaultdict(list) @@ -181,6 +182,10 @@ class VeraDevice(Entity): if power: attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) + energy = self.vera_device.energy + if energy: + attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) + attr['Vera Device Id'] = self.vera_device.vera_device_id return attr diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index dfcc0c96c0b..a4b6674af74 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -14,7 +14,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['pywemo==0.4.16'] +REQUIREMENTS = ['pywemo==0.4.18'] DOMAIN = 'wemo' @@ -62,7 +62,8 @@ def setup(hass, config): def discovery_dispatch(service, discovery_info): """Dispatcher for WeMo discovery events.""" # name, model, location, mac - _, model_name, _, _, serial = discovery_info + model_name = discovery_info.get('model_name') + serial = discovery_info.get('serial') # Only register a device once if serial in KNOWN_DEVICES: @@ -95,7 +96,12 @@ def setup(hass, config): if device is None: device = pywemo.discovery.device_from_description(url, None) - discovery_info = (device.name, device.model_name, url, device.mac, - device.serialnumber) + discovery_info = { + 'model_name': device.model_name, + 'serial': device.serialnumber, + 'mac_address': device.mac, + 'ssdp_description': url, + } + discovery.discover(hass, SERVICE_WEMO, discovery_info) return True diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index 80f3827a135..235fe11934a 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -47,7 +47,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, vol.Optional(CONF_ICON): cv.icon, -}) +}, extra=vol.ALLOW_EXTRA) def active_zone(hass, latitude, longitude, radius=0): diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 254b488ccbb..0a32a664dc3 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -13,6 +13,7 @@ from pprint import pprint import voluptuous as vol +from homeassistant.core import CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity_component import EntityComponent @@ -25,6 +26,7 @@ import homeassistant.config as conf_util import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) +from homeassistant.components.frontend import register_built_in_panel from . import const from .const import DOMAIN @@ -64,8 +66,7 @@ DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 DATA_ZWAVE_DICT = 'zwave_devices' - -NETWORK = None +ZWAVE_NETWORK = 'zwave_network' RENAME_NODE_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), @@ -185,8 +186,8 @@ def get_config_value(node, value_index, tries=5): """Return the current configuration value for a specific index.""" try: for value in node.values.values(): - # 112 == config command class - if value.command_class == 112 and value.index == value_index: + if (value.command_class == const.COMMAND_CLASS_CONFIGURATION + and value.index == value_index): return value.data except RuntimeError: # If we get an runtime error the dict has changed while @@ -199,16 +200,17 @@ def get_config_value(node, value_index, tries=5): @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Generic Z-Wave platform setup.""" - if discovery_info is None or NETWORK is None: + if discovery_info is None or ZWAVE_NETWORK not in hass.data: return False + device = hass.data[DATA_ZWAVE_DICT].pop( - discovery_info[const.DISCOVERY_DEVICE]) - if device: - async_add_devices([device]) - return True - else: + discovery_info[const.DISCOVERY_DEVICE], None) + if device is None: return False + async_add_devices([device]) + return True + # pylint: disable=R0914 def setup(hass, config): @@ -216,9 +218,6 @@ def setup(hass, config): Will automatically load components to support devices found on the network. """ - # pylint: disable=global-statement, import-error - global NETWORK - descriptions = conf_util.load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) @@ -230,6 +229,7 @@ def setup(hass, config): "https://home-assistant.io/components/zwave/") return False from pydispatch import dispatcher + # pylint: disable=import-error from openzwave.option import ZWaveOption from openzwave.network import ZWaveNetwork from openzwave.group import ZWaveGroup @@ -255,10 +255,10 @@ def setup(hass, config): options.set_console_output(use_debug) options.lock() - NETWORK = ZWaveNetwork(options, autostart=False) + network = hass.data[ZWAVE_NETWORK] = ZWaveNetwork(options, autostart=False) hass.data[DATA_ZWAVE_DICT] = {} - if use_debug: + if use_debug: # pragma: no cover def log_all(signal, value=None): """Log all the signals.""" print("") @@ -299,7 +299,7 @@ def setup(hass, config): def node_added(node): """Called when a node is added on the network.""" - entity = ZWaveNodeEntity(node) + entity = ZWaveNodeEntity(node, network) node_config = device_config.get(entity.entity_id) if node_config.get(CONF_IGNORED): _LOGGER.info( @@ -352,49 +352,49 @@ def setup(hass, config): def add_node(service): """Switch into inclusion mode.""" _LOGGER.info("Zwave add_node have been initialized.") - NETWORK.controller.add_node() + network.controller.add_node() def add_node_secure(service): """Switch into secure inclusion mode.""" _LOGGER.info("Zwave add_node_secure have been initialized.") - NETWORK.controller.add_node(True) + network.controller.add_node(True) def remove_node(service): """Switch into exclusion mode.""" _LOGGER.info("Zwave remove_node have been initialized.") - NETWORK.controller.remove_node() + network.controller.remove_node() def cancel_command(service): """Cancel a running controller command.""" _LOGGER.info("Cancel running ZWave command.") - NETWORK.controller.cancel_command() + network.controller.cancel_command() def heal_network(service): """Heal the network.""" _LOGGER.info("ZWave heal running.") - NETWORK.heal() + network.heal() def soft_reset(service): """Soft reset the controller.""" _LOGGER.info("Zwave soft_reset have been initialized.") - NETWORK.controller.soft_reset() + network.controller.soft_reset() def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Zwave test_network have been initialized.") - NETWORK.test() + network.test() - def stop_zwave(_service_or_event): + def stop_network(_service_or_event): """Stop Z-Wave network.""" _LOGGER.info("Stopping ZWave network.") - NETWORK.stop() - if hass.state == 'RUNNING': + network.stop() + if hass.state == CoreState.running: hass.bus.fire(const.EVENT_NETWORK_STOP) def rename_node(service): """Rename a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = hass.data[ZWAVE_NETWORK].nodes[node_id] name = service.data.get(const.ATTR_NAME) node.name = name _LOGGER.info( @@ -404,18 +404,18 @@ def setup(hass, config): """Remove failed node.""" node_id = service.data.get(const.ATTR_NODE_ID) _LOGGER.info('Trying to remove zwave node %d', node_id) - NETWORK.controller.remove_failed_node(node_id) + network.controller.remove_failed_node(node_id) def replace_failed_node(service): """Replace failed node.""" node_id = service.data.get(const.ATTR_NODE_ID) _LOGGER.info('Trying to replace zwave node %d', node_id) - NETWORK.controller.replace_failed_node(node_id) + network.controller.replace_failed_node(node_id) def set_config_parameter(service): """Set a config parameter to a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = network.nodes[node_id] param = service.data.get(const.ATTR_CONFIG_PARAMETER) selection = service.data.get(const.ATTR_CONFIG_VALUE) size = service.data.get(const.ATTR_CONFIG_SIZE, 2) @@ -444,7 +444,7 @@ def setup(hass, config): def print_config_parameter(service): """Print a config parameter from a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = network.nodes[node_id] param = service.data.get(const.ATTR_CONFIG_PARAMETER) _LOGGER.info("Config parameter %s on Node %s : %s", param, node_id, get_config_value(node, param)) @@ -452,13 +452,13 @@ def setup(hass, config): def print_node(service): """Print all information about z-wave node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = network.nodes[node_id] nice_print_node(node) def set_wakeup(service): """Set wake-up interval of a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = network.nodes[node_id] value = service.data.get(const.ATTR_CONFIG_VALUE) if node.can_wake_up(): for value_id in node.get_values( @@ -476,7 +476,7 @@ def setup(hass, config): group = service.data.get(const.ATTR_GROUP) instance = service.data.get(const.ATTR_INSTANCE) - node = ZWaveGroup(group, NETWORK, node_id) + node = ZWaveGroup(group, network, node_id) if association_type == 'add': node.add_association(target_node_id, instance) _LOGGER.info("Adding association for node:%s in group:%s " @@ -498,13 +498,13 @@ def setup(hass, config): def refresh_node(service): """Refresh all node info.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = NETWORK.nodes[node_id] + node = network.nodes[node_id] node.refresh_info() def start_zwave(_service_or_event): """Startup Z-Wave network.""" _LOGGER.info("Starting ZWave network.") - NETWORK.start() + network.start() hass.bus.fire(const.EVENT_NETWORK_START) # Need to be in STATE_AWAKED before talking to nodes. @@ -512,8 +512,9 @@ def setup(hass, config): # to be ready. for i in range(const.NETWORK_READY_WAIT_SECS): _LOGGER.debug( - "network state: %d %s", NETWORK.state, NETWORK.state_str) - if NETWORK.state >= NETWORK.STATE_AWAKED: + "network state: %d %s", hass.data[ZWAVE_NETWORK].state, + network.state_str) + if network.state >= network.STATE_AWAKED: _LOGGER.info("zwave ready after %d seconds", i) break time.sleep(1) @@ -522,17 +523,18 @@ def setup(hass, config): "zwave not ready after %d seconds, continuing anyway", const.NETWORK_READY_WAIT_SECS) _LOGGER.info( - "final network state: %d %s", NETWORK.state, NETWORK.state_str) + "final network state: %d %s", network.state, + network.state_str) polling_interval = convert( config[DOMAIN].get(CONF_POLLING_INTERVAL), int) if polling_interval is not None: - NETWORK.set_poll_interval(polling_interval, False) + network.set_poll_interval(polling_interval, False) - poll_interval = NETWORK.get_poll_interval() + poll_interval = network.get_poll_interval() _LOGGER.info("zwave polling interval set to %d ms", poll_interval) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_network) # Register node services for Z-Wave network hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node, @@ -553,7 +555,8 @@ def setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network, descriptions[const.SERVICE_TEST_NETWORK]) - hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_zwave, + hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, + stop_network, descriptions[const.SERVICE_STOP_NETWORK]) hass.services.register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave, @@ -613,6 +616,9 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) + if 'frontend' in hass.config.components: + register_built_in_panel(hass, 'zwave', 'Z-Wave', 'mdi:nfc') + return True @@ -732,7 +738,7 @@ class ZWaveDeviceEntityValues(): device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass) - if not device: + if device is None: # No entity will be created for this value self._workaround_ignore = True return @@ -840,4 +846,5 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def refresh_from_network(self): """Refresh all dependent values from zwave network.""" for value in self.values: - self.node.refresh_value(value.value_id) + if value is not None: + self.node.refresh_value(value.value_id) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 0ec699b8ee6..e43ee735ac7 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -15,6 +15,8 @@ ATTR_QUERY_STAGE = 'query_stage' ATTR_AWAKE = 'is_awake' ATTR_READY = 'is_ready' ATTR_FAILED = 'is_failed' +ATTR_PRODUCT_NAME = 'product_name' +ATTR_MANUFACTURER_NAME = 'manufacturer_name' STAGE_COMPLETE = 'Complete' @@ -22,6 +24,10 @@ _REQUIRED_ATTRIBUTES = [ ATTR_QUERY_STAGE, ATTR_AWAKE, ATTR_READY, ATTR_FAILED, 'is_info_received', 'max_baud_rate', 'is_zwave_plus'] _OPTIONAL_ATTRIBUTES = ['capabilities', 'neighbors', 'location'] +_COMM_ATTRIBUTES = [ + 'sentCnt', 'sentFailed', 'retries', 'receivedCnt', 'receivedDups', + 'receivedUnsolicited', 'sentTS', 'receivedTS', 'lastRequestRTT', + 'averageRequestRTT', 'lastResponseRTT', 'averageResponseRTT'] ATTRIBUTES = _REQUIRED_ATTRIBUTES + _OPTIONAL_ATTRIBUTES @@ -65,15 +71,18 @@ def sub_status(status, stage): class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" - def __init__(self, node): + def __init__(self, node, network): """Initialize node.""" # pylint: disable=import-error super().__init__() from openzwave.network import ZWaveNetwork from pydispatch import dispatcher + self._network = network self.node = node self.node_id = self.node.node_id self._name = node_name(self.node) + self._product_name = node.product_name + self._manufacturer_name = node.manufacturer_name self.entity_id = "{}.{}_{}".format( DOMAIN, slugify(self._name), self.node_id) self._attributes = {} @@ -95,13 +104,22 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return self.node_changed() + def get_node_statistics(self): + """Retrieve statistics from the node.""" + return self._network.manager.getNodeStatistics(self._network.home_id, + self.node_id) + def node_changed(self): """Update node properties.""" - self._attributes = {} + attributes = {} + stats = self.get_node_statistics() for attr in ATTRIBUTES: value = getattr(self.node, attr) if attr in _REQUIRED_ATTRIBUTES or value: - self._attributes[attr] = value + attributes[attr] = value + + for attr in _COMM_ATTRIBUTES: + attributes[attr] = stats[attr] if self.node.can_wake_up(): for value in self.node.get_values(COMMAND_CLASS_WAKE_UP).values(): @@ -111,6 +129,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.wakeup_interval = None self.battery_level = self.node.get_battery_level() + self._attributes = attributes self.maybe_schedule_update() @@ -146,10 +165,13 @@ class ZWaveNodeEntity(ZWaveBaseEntity): """Return the device specific state attributes.""" attrs = { ATTR_NODE_ID: self.node_id, + ATTR_MANUFACTURER_NAME: self._manufacturer_name, + ATTR_PRODUCT_NAME: self._product_name, } attrs.update(self._attributes) if self.battery_level is not None: attrs[ATTR_BATTERY_LEVEL] = self.battery_level if self.wakeup_interval is not None: attrs[ATTR_WAKEUP] = self.wakeup_interval + return attrs diff --git a/homeassistant/const.py b/homeassistant/const.py index 65b15797380..5cc08642299 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 42 -PATCH_VERSION = '4' +MINOR_VERSION = 43 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) @@ -361,7 +361,6 @@ URL_API_EVENTS = '/api/events' URL_API_EVENTS_EVENT = '/api/events/{}' URL_API_SERVICES = '/api/services' URL_API_SERVICES_SERVICE = '/api/services/{}/{}' -URL_API_EVENT_FORWARD = '/api/event_forwarding' URL_API_COMPONENTS = '/api/components' URL_API_ERROR_LOG = '/api/error_log' URL_API_LOG_OUT = '/api/log_out' diff --git a/homeassistant/core.py b/homeassistant/core.py index 320e857ac9e..a467cf28e51 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -648,7 +648,7 @@ class StateMachine(object): Async friendly. """ state_obj = self.get(entity_id) - return state_obj and state_obj.state == state + return state_obj is not None and state_obj.state == state def is_state_attr(self, entity_id, name, value): """Test if entity exists and has a state attribute set to value. @@ -656,7 +656,8 @@ class StateMachine(object): Async friendly. """ state_obj = self.get(entity_id) - return state_obj and state_obj.attributes.get(name, None) == value + return state_obj is not None and \ + state_obj.attributes.get(name, None) == value def remove(self, entity_id): """Remove the state of an entity. diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 0fc75a476f6..47a7627b5ce 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -14,6 +14,7 @@ if False: ConfigType = Dict[str, Any] +# pylint: disable=invalid-sequence-index def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: """Generator to break a component config into different platforms. diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 5330eafed19..9e059528619 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -95,7 +95,7 @@ class EntityComponent(object): ).result() def async_extract_from_service(self, service, expand_group=True): - """Extract all known entities from a service call. + """Extract all known and available entities from a service call. Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. @@ -103,11 +103,13 @@ class EntityComponent(object): This method must be run in the event loop. """ if ATTR_ENTITY_ID not in service.data: - return list(self.entities.values()) + return [entity for entity in self.entities.values() + if entity.available] return [self.entities[entity_id] for entity_id in extract_entity_ids(self.hass, service, expand_group) - if entity_id in self.entities] + if entity_id in self.entities and + self.entities[entity_id].available] @asyncio.coroutine def _async_setup_platform(self, platform_type, platform_config, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 343e17ff7b5..f2ea6e743c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,3 +7,4 @@ voluptuous==0.9.3 typing>=3,<4 aiohttp==2.0.7 async_timeout==1.2.0 +chardet==3.0.2 diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 1107eda8742..c0e3d9d6459 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -7,23 +7,19 @@ HomeAssistantError will be raised. For more details about the Python API, please refer to the documentation at https://home-assistant.io/developers/python_api/ """ -import asyncio -from concurrent.futures import ThreadPoolExecutor from datetime import datetime import enum import json import logging -import time -import threading import urllib.parse from typing import Optional import requests -from homeassistant import setup, core as ha +from homeassistant import core as ha from homeassistant.const import ( - HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API, URL_API_EVENT_FORWARD, + HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API, URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, URL_API_CONFIG, URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY, HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) @@ -116,195 +112,6 @@ class API(object): self.base_url, 'yes' if self.api_password is not None else 'no') -class HomeAssistant(ha.HomeAssistant): - """Home Assistant that forwards work.""" - - # pylint: disable=super-init-not-called - def __init__(self, remote_api, local_api=None, loop=None): - """Initalize the forward instance.""" - _LOGGER.warning('Remote instances of Home Assistant are deprecated. ' - 'Will be removed by 0.43') - if not remote_api.validate_api(): - raise HomeAssistantError( - "Remote API at {}:{} not valid: {}".format( - remote_api.host, remote_api.port, remote_api.status)) - - self.remote_api = remote_api - - self.loop = loop or asyncio.get_event_loop() - self.executor = ThreadPoolExecutor(max_workers=5) - self.loop.set_default_executor(self.executor) - self.loop.set_exception_handler(ha.async_loop_exception_handler) - self._pending_tasks = [] - self._pending_sheduler = None - - self.bus = EventBus(remote_api, self) - self.services = ha.ServiceRegistry(self) - self.states = StateMachine(self.bus, self.loop, self.remote_api) - self.config = ha.Config() - # This is a dictionary that any component can store any data on. - self.data = {} - self.state = ha.CoreState.not_running - self.exit_code = None - self.config.api = local_api - - def start(self): - """Start the instance.""" - # Ensure a local API exists to connect with remote - if 'api' not in self.config.components: - if not setup.setup_component(self, 'api'): - raise HomeAssistantError( - 'Unable to setup local API to receive events') - - self.state = ha.CoreState.starting - # pylint: disable=protected-access - ha._async_create_timer(self) - - self.bus.fire(ha.EVENT_HOMEASSISTANT_START, - origin=ha.EventOrigin.remote) - - # Ensure local HTTP is started - self.block_till_done() - self.state = ha.CoreState.running - time.sleep(0.05) - - # Setup that events from remote_api get forwarded to local_api - # Do this after we are running, otherwise HTTP is not started - # or requests are blocked - if not connect_remote_events(self.remote_api, self.config.api): - raise HomeAssistantError(( - 'Could not setup event forwarding from api {} to ' - 'local api {}').format(self.remote_api, self.config.api)) - - def stop(self): - """Stop Home Assistant and shuts down all threads.""" - _LOGGER.info("Stopping") - self.state = ha.CoreState.stopping - - self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, - origin=ha.EventOrigin.remote) - - # Disconnect master event forwarding - disconnect_remote_events(self.remote_api, self.config.api) - self.state = ha.CoreState.not_running - - -class EventBus(ha.EventBus): - """EventBus implementation that forwards fire_event to remote API.""" - - def __init__(self, api, hass): - """Initalize the eventbus.""" - super().__init__(hass) - self._api = api - - def fire(self, event_type, event_data=None, origin=ha.EventOrigin.local): - """Forward local events to remote target. - - Handles remote event as usual. - """ - # All local events that are not TIME_CHANGED are forwarded to API - if origin == ha.EventOrigin.local and \ - event_type != ha.EVENT_TIME_CHANGED: - - fire_event(self._api, event_type, event_data) - - else: - super().fire(event_type, event_data, origin) - - -class EventForwarder(object): - """Listens for events and forwards to specified APIs.""" - - def __init__(self, hass, restrict_origin=None): - """Initalize the event forwarder.""" - _LOGGER.warning('API forwarding is deprecated. ' - 'Will be removed by 0.43') - - self.hass = hass - self.restrict_origin = restrict_origin - - # We use a tuple (host, port) as key to ensure - # that we do not forward to the same host twice - self._targets = {} - - self._lock = threading.Lock() - self._async_unsub_listener = None - - @ha.callback - def async_connect(self, api): - """Attach to a Home Assistant instance and forward events. - - Will overwrite old target if one exists with same host/port. - """ - if self._async_unsub_listener is None: - self._async_unsub_listener = self.hass.bus.async_listen( - ha.MATCH_ALL, self._event_listener) - - key = (api.host, api.port) - - self._targets[key] = api - - @ha.callback - def async_disconnect(self, api): - """Remove target from being forwarded to.""" - key = (api.host, api.port) - - did_remove = self._targets.pop(key, None) is None - - if len(self._targets) == 0: - # Remove event listener if no forwarding targets present - self._async_unsub_listener() - self._async_unsub_listener = None - - return did_remove - - def _event_listener(self, event): - """Listen and forward all events.""" - with self._lock: - # We don't forward time events or, if enabled, non-local events - if event.event_type == ha.EVENT_TIME_CHANGED or \ - (self.restrict_origin and event.origin != self.restrict_origin): - return - - for api in self._targets.values(): - fire_event(api, event.event_type, event.data) - - -class StateMachine(ha.StateMachine): - """Fire set events to an API. Uses state_change events to track states.""" - - def __init__(self, bus, loop, api): - """Initalize the statemachine.""" - super().__init__(bus, loop) - self._api = api - self.mirror() - - bus.listen(ha.EVENT_STATE_CHANGED, self._state_changed_listener) - - def remove(self, entity_id): - """Remove the state of an entity. - - Returns boolean to indicate if an entity was removed. - """ - return remove_state(self._api, entity_id) - - def set(self, entity_id, new_state, attributes=None, force_update=False): - """Call set_state on remote API.""" - set_state(self._api, entity_id, new_state, attributes, force_update) - - def mirror(self): - """Discard current data and mirrors the remote state machine.""" - self._states = {state.entity_id: state for state - in get_states(self._api)} - - def _state_changed_listener(self, event): - """Listen for state changed events and applies them.""" - if event.data['new_state'] is None: - self._states.pop(event.data['entity_id'], None) - else: - self._states[event.data['entity_id']] = event.data['new_state'] - - class JSONEncoder(json.JSONEncoder): """JSONEncoder that supports Home Assistant objects.""" @@ -352,59 +159,6 @@ def validate_api(api): return APIStatus.CANNOT_CONNECT -def connect_remote_events(from_api, to_api): - """Setup from_api to forward all events to to_api.""" - _LOGGER.warning('Event forwarding is deprecated. ' - 'Will be removed by 0.43') - data = { - 'host': to_api.host, - 'api_password': to_api.api_password, - 'port': to_api.port - } - - try: - req = from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) - - if req.status_code == 200: - return True - else: - _LOGGER.error( - "Error setting up event forwarding: %s - %s", - req.status_code, req.text) - - return False - - except HomeAssistantError: - _LOGGER.exception("Error setting up event forwarding") - return False - - -def disconnect_remote_events(from_api, to_api): - """Disconnect forwarding events from from_api to to_api.""" - _LOGGER.warning('Event forwarding is deprecated. ' - 'Will be removed by 0.43') - data = { - 'host': to_api.host, - 'port': to_api.port - } - - try: - req = from_api(METHOD_DELETE, URL_API_EVENT_FORWARD, data) - - if req.status_code == 200: - return True - else: - _LOGGER.error( - "Error removing event forwarding: %s - %s", - req.status_code, req.text) - - return False - - except HomeAssistantError: - _LOGGER.exception("Error removing an event forwarder") - return False - - def get_event_listeners(api): """List of events that is being listened for.""" try: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 6d77f67161d..1186892b512 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -22,6 +22,9 @@ U = TypeVar('U') RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') RE_SLUGIFY = re.compile(r'[^a-z0-9_]+') +TBL_SLUGIFY = { + ord('ß'): 'ss' +} def sanitize_filename(filename: str) -> str: @@ -36,9 +39,13 @@ def sanitize_path(path: str) -> str: def slugify(text: str) -> str: """Slugify a given text.""" - text = normalize('NFKD', text).lower().replace(" ", "_") + text = normalize('NFKD', text) + text = text.lower() + text = text.replace(" ", "_") + text = text.translate(TBL_SLUGIFY) + text = RE_SLUGIFY.sub("", text) - return RE_SLUGIFY.sub("", text) + return text def repr_helper(inp: Any) -> str: diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 52d7a9f63aa..57d88c5328d 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -183,7 +183,7 @@ def color_name_to_rgb(color_name): # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. -# pylint: disable=invalid-name +# pylint: disable=invalid-name, invalid-sequence-index def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" if iR + iG + iB == 0: @@ -219,6 +219,7 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy +# pylint: disable=invalid-sequence-index def color_xy_brightness_to_RGB(vX: float, vY: float, ibrightness: int) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" @@ -259,18 +260,21 @@ def color_xy_brightness_to_RGB(vX: float, vY: float, return (ir, ig, ib) +# pylint: disable=invalid-sequence-index def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[int, int, int]: """Convert an rgb color to its hsv representation.""" fHSV = colorsys.rgb_to_hsv(iR/255.0, iG/255.0, iB/255.0) return (int(fHSV[0]*65536), int(fHSV[1]*255), int(fHSV[2]*255)) +# pylint: disable=invalid-sequence-index def color_xy_brightness_to_hsv(vX: float, vY: float, ibrightness: int) -> Tuple[int, int, int]: """Convert an xy brightness color to its hsv representation.""" return color_RGB_to_hsv(*color_xy_brightness_to_RGB(vX, vY, ibrightness)) +# pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: """Match the maximum value of the output to the input.""" @@ -305,6 +309,11 @@ def color_rgbw_to_rgb(r, g, b, w): return _match_max_scale((r, g, b, w), rgb) +def color_rgb_to_hex(r, g, b): + """Return a RGB color from a hex color string.""" + return '{0:02x}{1:02x}{2:02x}'.format(r, g, b) + + def rgb_hex_to_rgb_list(hex_string): """Return an RGB color value list from a hex color string.""" return [int(hex_string[i:i + len(hex_string) // 3], 16) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 52e85081599..5e8b3382fb1 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,4 +1,4 @@ -"""Provides helper methods to handle the time in HA.""" +"""Helper methods to handle the time in Home Assistant.""" import datetime as dt import re @@ -184,6 +184,7 @@ def get_age(date: dt.datetime) -> str: elif number > 1: return "%d %ss" % (number, unit) + # pylint: disable=invalid-sequence-index def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" return first // second, first % second diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 950f2f99fed..c7bc4205297 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -9,7 +9,6 @@ from typing import Any, Optional, Tuple, Dict import requests - ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' FREEGEO_API = 'https://freegeoip.io/json/' IP_API = 'http://ip-api.com/json' @@ -83,7 +82,7 @@ def elevation(latitude, longitude): # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE -# pylint: disable=invalid-name, unused-variable +# pylint: disable=invalid-name, unused-variable, invalid-sequence-index def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], miles: bool=False) -> Optional[float]: """ diff --git a/requirements_all.txt b/requirements_all.txt index b0d909f74a6..e98df3bf99b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,6 +8,7 @@ voluptuous==0.9.3 typing>=3,<4 aiohttp==2.0.7 async_timeout==1.2.0 +chardet==3.0.2 # homeassistant.components.nuimo_controller --only-binary=all http://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 @@ -25,7 +26,7 @@ PyJWT==1.4.2 PyMVGLive==1.1.3 # homeassistant.components.arduino -PyMata==2.13 +PyMata==2.14 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 @@ -36,6 +37,9 @@ SoCo==0.12 # homeassistant.components.notify.twitter TwitterAPI==2.4.5 +# homeassistant.components.device_tracker.automatic +aioautomatic==0.1.1 + # homeassistant.components.sensor.dnsip aiodns==1.1.1 @@ -46,9 +50,12 @@ aiohttp_cors==0.5.2 # homeassistant.components.light.lifx aiolifx==0.4.4 +# homeassistant.components.alarmdecoder +alarmdecoder==0.12.1.0 + # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest -amcrest==1.1.8 +amcrest==1.1.9 # homeassistant.components.media_player.anthemav anthemav==1.1.8 @@ -162,7 +169,7 @@ evohomeclient==0.2.5 fastdotcom==0.0.1 # homeassistant.components.sensor.fedex -fedexdeliverymanager==1.0.1 +fedexdeliverymanager==1.0.2 # homeassistant.components.feedreader feedparser==5.2.1 @@ -212,7 +219,7 @@ googlemaps==2.4.6 gps3==0.33.3 # homeassistant.components.media_player.gstreamer -gstreamer-player==1.0.0 +gstreamer-player==1.1.0 # homeassistant.components.ffmpeg ha-ffmpeg==1.5 @@ -238,16 +245,12 @@ holidays==0.8.1 # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.4.zip#pyW215==0.4 -# homeassistant.components.media_player.webostv -# homeassistant.components.notify.webostv -https://github.com/TheRealLink/pylgtv/archive/v0.1.4.zip#pylgtv==0.1.4 - # homeassistant.components.sensor.thinkingcleaner # homeassistant.components.switch.thinkingcleaner https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcleaner==0.0.2 # homeassistant.components.media_player.braviatv -https://github.com/aparraga/braviarc/archive/0.3.6.zip#braviarc==0.3.6 +https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.cover.myq https://github.com/arraylabs/pymyq/archive/v0.0.8.zip#pymyq==0.0.8 @@ -259,13 +262,16 @@ https://github.com/bah2830/python-roku/archive/3.1.3.zip#roku==3.1.3 https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 # homeassistant.components.lutron_caseta -https://github.com/gurumitts/pylutron-caseta/archive/v0.2.5.zip#pylutron-caseta==v0.2.5 +https://github.com/gurumitts/pylutron-caseta/archive/v0.2.6.zip#pylutron-caseta==v0.2.6 + +# homeassistant.components.media_player.spotify +https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 # homeassistant.components.netatmo https://github.com/jabesq/netatmo-api-python/archive/v0.9.1.zip#lnetatmo==0.9.1 # homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 +https://github.com/jabesq/pybotvac/archive/v0.0.3.zip#pybotvac==0.0.3 # homeassistant.components.sensor.sabnzbd https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 @@ -355,7 +361,7 @@ knxip==0.3.3 libnacl==1.5.0 # homeassistant.components.media_player.soundtouch -libsoundtouch==0.1.0 +libsoundtouch==0.3.0 # homeassistant.components.light.lifx_legacy liffylights==0.9.4 @@ -389,10 +395,10 @@ miflora==0.1.16 mutagen==1.36.2 # homeassistant.components.sensor.usps -myusps==1.0.3 +myusps==1.0.5 # homeassistant.components.discovery -netdisco==0.9.2 +netdisco==1.0.0rc3 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -413,7 +419,7 @@ openhomedevice==0.2.1 orvibo==1.1.1 # homeassistant.components.mqtt -paho-mqtt==1.2.1 +paho-mqtt==1.2.2 # homeassistant.components.media_player.panasonic_viera panasonic_viera==0.2 @@ -448,7 +454,7 @@ pmsensor==0.4 proliphix==0.4.1 # homeassistant.components.sensor.systemmonitor -psutil==5.2.1 +psutil==5.2.2 # homeassistant.components.wink pubnubsub-handler==1.0.2 @@ -462,8 +468,11 @@ pushetta==1.0.15 # homeassistant.components.sensor.waqi pwaqi==3.0 +# homeassistant.components.light.rpi_gpio_pwm +pwmled==1.1.1 + # homeassistant.components.sensor.cpuspeed -py-cpuinfo==3.0.0 +py-cpuinfo==3.2.0 # homeassistant.components.hdmi_cec pyCEC==0.4.13 @@ -553,6 +562,10 @@ pykwb==0.0.8 # homeassistant.components.sensor.lastfm pylast==1.8.0 +# homeassistant.components.media_player.webostv +# homeassistant.components.notify.webostv +pylgtv==0.1.6 + # homeassistant.components.litejet pylitejet==0.1 @@ -616,8 +629,9 @@ python-hpilo==3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 +# homeassistant.components.light.mystrom # homeassistant.components.switch.mystrom -python-mystrom==0.3.6 +python-mystrom==0.3.8 # homeassistant.components.nest python-nest==3.1.0 @@ -631,8 +645,9 @@ python-pushover==0.2 # homeassistant.components.sensor.synologydsm python-synology==0.1.0 -# homeassistant.components.telegram_webhooks # homeassistant.components.notify.telegram +# homeassistant.components.telegram_bot.polling +# homeassistant.components.telegram_bot.webhooks python-telegram-bot==5.3.0 # homeassistant.components.sensor.twitch @@ -647,6 +662,9 @@ python-wink==1.2.3 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 +# homeassistant.components.tradfri +pytradfri==1.0 + # homeassistant.components.device_tracker.unifi pyunifi==2.0 @@ -654,19 +672,19 @@ pyunifi==2.0 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.25 +pyvera==0.2.26 # homeassistant.components.notify.html5 pywebpush==0.6.1 # homeassistant.components.wemo -pywemo==0.4.16 +pywemo==0.4.18 # homeassistant.components.zabbix pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.3 +qnapstats==0.2.4 # homeassistant.components.climate.radiotherm radiotherm==1.2 @@ -693,7 +711,7 @@ schiene==0.18 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==3.6.5 +sendgrid==4.0.0 # homeassistant.components.sensor.sensehat sense-hat==2.2.0 @@ -720,7 +738,7 @@ snapcast==1.2.2 somecomfort==0.4.1 # homeassistant.components.sensor.speedtest -speedtest-cli==1.0.3 +speedtest-cli==1.0.4 # homeassistant.components.recorder # homeassistant.scripts.db_migrator @@ -759,10 +777,10 @@ transmissionrpc==0.11 twilio==5.7.0 # homeassistant.components.sensor.uber -uber_rides==0.2.7 +uber_rides==0.4.1 # homeassistant.components.sensor.ups -upsmychoice==1.0.1 +upsmychoice==1.0.2 # homeassistant.components.camera.uvc uvcclient==0.10.0 diff --git a/requirements_test.txt b/requirements_test.txt index 2d8ebe6a238..c2c5cf65f1b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,7 @@ # new version flake8==3.3 pylint==1.6.5 -mypy-lang==0.4.5 +mypy==0.501 pydocstyle==1.1.1 coveralls>=1.1 pytest>=2.9.2 diff --git a/script/dev_docker b/script/dev_docker index 73c4ee60d0a..514fce73477 100755 --- a/script/dev_docker +++ b/script/dev_docker @@ -27,6 +27,7 @@ else -v /etc/localtime:/etc/localtime:ro \ -v `pwd`:/usr/src/app \ -v `pwd`/config:/config \ + --rm \ -t -i home-assistant-dev fi diff --git a/setup.py b/setup.py index 161484d1f83..05f117652d1 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ REQUIRES = [ 'typing>=3,<4', 'aiohttp==2.0.7', 'async_timeout==1.2.0', + 'chardet==3.0.2' ] setup( diff --git a/tests/common.py b/tests/common.py index 03a4de235d7..a6627344879 100644 --- a/tests/common.py +++ b/tests/common.py @@ -170,8 +170,11 @@ def mock_service(hass, domain, service): @ha.callback def async_fire_mqtt_message(hass, topic, payload, qos=0): """Fire the MQTT message.""" + if isinstance(payload, str): + payload = payload.encode('utf-8') async_dispatcher_send( - hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, topic, payload, qos) + hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, topic, + payload, qos) def fire_mqtt_message(hass, topic, payload, qos=0): diff --git a/tests/components/camera/test_mqtt.py b/tests/components/camera/test_mqtt.py new file mode 100644 index 00000000000..802d29a510a --- /dev/null +++ b/tests/components/camera/test_mqtt.py @@ -0,0 +1,47 @@ +"""The tests for mqtt camera component.""" +import asyncio +import unittest + +from homeassistant.setup import async_setup_component + +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, get_test_instance_port) + +import requests + +SERVER_PORT = get_test_instance_port() +HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) + + +class TestComponentsMQTTCamera(unittest.TestCase): + """Test MQTT camera platform.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_mqtt = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @asyncio.coroutine + def test_run_camera_setup(self): + """Test that it fetches the given payload.""" + topic = 'test/camera' + yield from async_setup_component(self.hass, 'camera', { + 'camera': { + 'platform': 'mqtt', + 'topic': topic, + 'name': 'Test Camera', + }}) + + self.mock_mqtt.publish(self.hass, topic, 0xFFD8FF) + yield from self.hass.async_block_till_done() + + resp = requests.get(HTTP_BASE_URL + + '/api/camera_proxy/camera.test_camera') + + assert resp.status_code == 200 + body = yield from resp.text + assert body == '16767231' diff --git a/tests/components/climate/test_zwave.py b/tests/components/climate/test_zwave.py index 711c7f2fabb..ed9e0cf9daa 100644 --- a/tests/components/climate/test_zwave.py +++ b/tests/components/climate/test_zwave.py @@ -15,7 +15,7 @@ def device(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node), + temperature=MockValue(data=5, node=node, units=None), mode=MockValue(data=b'test1', data_items=[0, 1, 2], node=node), fan_mode=MockValue(data=b'test2', data_items=[3, 4, 5], node=node), operating_state=MockValue(data=6, node=node), @@ -30,9 +30,10 @@ def device(hass, mock_openzwave): def device_zxt_120(hass, mock_openzwave): """Fixture to provide a precreated climate device.""" node = MockNode(manufacturer_id='5254', product_id='8377') + values = MockEntityValues( primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node), + temperature=MockValue(data=5, node=node, units=None), mode=MockValue(data=b'test1', data_items=[0, 1, 2], node=node), fan_mode=MockValue(data=b'test2', data_items=[3, 4, 5], node=node), operating_state=MockValue(data=6, node=node), diff --git a/tests/components/cover/test_zwave.py b/tests/components/cover/test_zwave.py index 425331ff35c..aebc04c2d4c 100644 --- a/tests/components/cover/test_zwave.py +++ b/tests/components/cover/test_zwave.py @@ -1,5 +1,5 @@ """Test Z-Wave cover devices.""" -from unittest.mock import patch +from unittest.mock import MagicMock from homeassistant.components.cover import zwave, SUPPORT_OPEN, SUPPORT_CLOSE from homeassistant.components.zwave import const @@ -8,58 +8,66 @@ from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) -def test_get_device_detects_none(mock_openzwave): +def test_get_device_detects_none(hass, mock_openzwave): """Test device returns none.""" node = MockNode() value = MockValue(data=0, node=node) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert device is None -def test_get_device_detects_rollershutter(mock_openzwave): +def test_get_device_detects_rollershutter(hass, mock_openzwave): """Test device returns rollershutter.""" + hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=0, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert isinstance(device, zwave.ZwaveRollershutter) -def test_get_device_detects_garagedoor(mock_openzwave): +def test_get_device_detects_garagedoor(hass, mock_openzwave): """Test device returns garage door.""" node = MockNode() value = MockValue(data=0, node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert isinstance(device, zwave.ZwaveGarageDoor) assert device.device_class == "garage" assert device.supported_features == SUPPORT_OPEN | SUPPORT_CLOSE -def test_roller_no_position_workaround(mock_openzwave): +def test_roller_no_position_workaround(hass, mock_openzwave): """Test position changed.""" + hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode(manufacturer_id='0047', product_type='5a52') value = MockValue(data=45, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert device.current_cover_position is None -def test_roller_value_changed(mock_openzwave): +def test_roller_value_changed(hass, mock_openzwave): """Test position changed.""" + hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=None, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert device.current_cover_position is None assert device.is_closed is None @@ -83,9 +91,9 @@ def test_roller_value_changed(mock_openzwave): assert not device.is_closed -@patch('homeassistant.components.zwave.NETWORK') -def test_roller_commands(mock_network, mock_openzwave): +def test_roller_commands(hass, mock_openzwave): """Test position changed.""" + mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) @@ -93,7 +101,8 @@ def test_roller_commands(mock_network, mock_openzwave): close_value = MockValue(data=False, node=node) values = MockEntityValues(primary=value, open=open_value, close=close_value, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) device.set_cover_position(25) assert node.set_dimmer.called @@ -117,9 +126,9 @@ def test_roller_commands(mock_network, mock_openzwave): assert value_id == open_value.value_id -@patch('homeassistant.components.zwave.NETWORK') -def test_roller_reverse_open_close(mock_network, mock_openzwave): +def test_roller_reverse_open_close(hass, mock_openzwave): """Test position changed.""" + mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode() value = MockValue(data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL) @@ -128,6 +137,7 @@ def test_roller_reverse_open_close(mock_network, mock_openzwave): values = MockEntityValues(primary=value, open=open_value, close=close_value, node=node) device = zwave.get_device( + hass=hass, node=node, values=values, node_config={zwave.zwave.CONF_INVERT_OPENCLOSE_BUTTONS: True}) @@ -148,13 +158,14 @@ def test_roller_reverse_open_close(mock_network, mock_openzwave): assert value_id == close_value.value_id -def test_garage_value_changed(mock_openzwave): +def test_garage_value_changed(hass, mock_openzwave): """Test position changed.""" node = MockNode() value = MockValue(data=False, node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert device.is_closed @@ -164,13 +175,14 @@ def test_garage_value_changed(mock_openzwave): assert not device.is_closed -def test_garage_commands(mock_openzwave): +def test_garage_commands(hass, mock_openzwave): """Test position changed.""" node = MockNode() value = MockValue(data=False, node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR) values = MockEntityValues(primary=value, node=node) - device = zwave.get_device(node=node, values=values, node_config={}) + device = zwave.get_device(hass=hass, node=node, values=values, + node_config={}) assert value.data is False device.open_cover() diff --git a/tests/components/device_tracker/test_automatic.py b/tests/components/device_tracker/test_automatic.py index 8e7d37d8798..dd03fd1da57 100644 --- a/tests/components/device_tracker/test_automatic.py +++ b/tests/components/device_tracker/test_automatic.py @@ -1,241 +1,90 @@ """Test the automatic device tracker platform.""" - +import asyncio import logging -import requests -import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock +import aioautomatic from homeassistant.components.device_tracker.automatic import ( - URL_AUTHORIZE, URL_VEHICLES, URL_TRIPS, setup_scanner) - -from tests.common import get_test_home_assistant + async_setup_scanner) _LOGGER = logging.getLogger(__name__) -INVALID_USERNAME = 'bob' -VALID_USERNAME = 'jim' -PASSWORD = 'password' -CLIENT_ID = '12345' -CLIENT_SECRET = '54321' -FUEL_LEVEL = 77.2 -LATITUDE = 32.82336 -LONGITUDE = -117.23743 -ACCURACY = 8 -DISPLAY_NAME = 'My Vehicle' + +@patch('aioautomatic.Client.create_session_from_password') +def test_invalid_credentials(mock_create_session, hass): + """Test with invalid credentials.""" + @asyncio.coroutine + def get_session(*args, **kwargs): + """Return the test session.""" + raise aioautomatic.exceptions.ForbiddenError() + + mock_create_session.side_effect = get_session + + config = { + 'platform': 'automatic', + 'username': 'bad_username', + 'password': 'bad_password', + 'client_id': 'client_id', + 'secret': 'client_secret', + 'devices': None, + } + result = hass.loop.run_until_complete( + async_setup_scanner(hass, config, None)) + assert not result -def mocked_requests(*args, **kwargs): - """Mock requests.get invocations.""" - class MockResponse: - """Class to represent a mocked response.""" +@patch('aioautomatic.Client.create_session_from_password') +def test_valid_credentials(mock_create_session, hass): + """Test with valid credentials.""" + session = MagicMock() + vehicle = MagicMock() + trip = MagicMock() + mock_see = MagicMock() - def __init__(self, json_data, status_code): - """Initialize the mock response class.""" - self.json_data = json_data - self.status_code = status_code + vehicle.id = 'mock_id' + vehicle.display_name = 'mock_display_name' + vehicle.fuel_level_percent = 45.6 - def json(self): - """Return the json of the response.""" - return self.json_data + trip.end_location.lat = 45.567 + trip.end_location.lon = 34.345 + trip.end_location.accuracy_m = 5.6 - @property - def content(self): - """Return the content of the response.""" - return self.json() + @asyncio.coroutine + def get_session(*args, **kwargs): + """Return the test session.""" + return session - def raise_for_status(self): - """Raise an HTTPError if status is not 200.""" - if self.status_code != 200: - raise requests.HTTPError(self.status_code) + @asyncio.coroutine + def get_vehicles(*args, **kwargs): + """Return list of test vehicles.""" + return [vehicle] - data = kwargs.get('data') + @asyncio.coroutine + def get_trips(*args, **kwargs): + """Return list of test trips.""" + return [trip] - if data and data.get('username', None) == INVALID_USERNAME: - return MockResponse({ - "error": "invalid_credentials" - }, 401) - elif str(args[0]).startswith(URL_AUTHORIZE): - return MockResponse({ - "user": { - "sid": "sid", - "id": "id" - }, - "token_type": "Bearer", - "access_token": "accesstoken", - "refresh_token": "refreshtoken", - "expires_in": 31521669, - "scope": "" - }, 200) - elif str(args[0]).startswith(URL_VEHICLES): - return MockResponse({ - "_metadata": { - "count": 2, - "next": None, - "previous": None - }, - "results": [ - { - "url": "https://api.automatic.com/vehicle/vid/", - "id": "vid", - "created_at": "2016-03-05T20:05:16.240000Z", - "updated_at": "2016-08-29T01:52:59.597898Z", - "make": "Honda", - "model": "Element", - "year": 2007, - "submodel": "EX", - "display_name": DISPLAY_NAME, - "fuel_grade": "regular", - "fuel_level_percent": FUEL_LEVEL, - "active_dtcs": [] - }] - }, 200) - elif str(args[0]).startswith(URL_TRIPS): - return MockResponse({ - "_metadata": { - "count": 1594, - "next": "https://api.automatic.com/trip/?page=2", - "previous": None - }, - "results": [ - { - "url": "https://api.automatic.com/trip/tid1/", - "id": "tid1", - "driver": "https://api.automatic.com/user/uid/", - "user": "https://api.automatic.com/user/uid/", - "started_at": "2016-08-28T19:37:23.986000Z", - "ended_at": "2016-08-28T19:43:22.500000Z", - "distance_m": 3931.6, - "duration_s": 358.5, - "vehicle": "https://api.automatic.com/vehicle/vid/", - "start_location": { - "lat": 32.87336, - "lon": -117.22743, - "accuracy_m": 10 - }, - "start_address": { - "name": "123 Fake St, Nowhere, NV 12345", - "display_name": "123 Fake St, Nowhere, NV", - "street_number": "Unknown", - "street_name": "Fake St", - "city": "Nowhere", - "state": "NV", - "country": "US" - }, - "end_location": { - "lat": LATITUDE, - "lon": LONGITUDE, - "accuracy_m": ACCURACY - }, - "end_address": { - "name": "321 Fake St, Nowhere, NV 12345", - "display_name": "321 Fake St, Nowhere, NV", - "street_number": "Unknown", - "street_name": "Fake St", - "city": "Nowhere", - "state": "NV", - "country": "US" - }, - "path": "path", - "vehicle_events": [], - "start_timezone": "America/Denver", - "end_timezone": "America/Denver", - "idling_time_s": 0, - "tags": [] - }, - { - "url": "https://api.automatic.com/trip/tid2/", - "id": "tid2", - "driver": "https://api.automatic.com/user/uid/", - "user": "https://api.automatic.com/user/uid/", - "started_at": "2016-08-28T18:48:00.727000Z", - "ended_at": "2016-08-28T18:55:25.800000Z", - "distance_m": 3969.1, - "duration_s": 445.1, - "vehicle": "https://api.automatic.com/vehicle/vid/", - "start_location": { - "lat": 32.87336, - "lon": -117.22743, - "accuracy_m": 11 - }, - "start_address": { - "name": "123 Fake St, Nowhere, NV, USA", - "display_name": "Fake St, Nowhere, NV", - "street_number": "123", - "street_name": "Fake St", - "city": "Nowhere", - "state": "NV", - "country": "US" - }, - "end_location": { - "lat": 32.82336, - "lon": -117.23743, - "accuracy_m": 10 - }, - "end_address": { - "name": "321 Fake St, Nowhere, NV, USA", - "display_name": "Fake St, Nowhere, NV", - "street_number": "Unknown", - "street_name": "Fake St", - "city": "Nowhere", - "state": "NV", - "country": "US" - }, - "path": "path", - "vehicle_events": [], - "start_timezone": "America/Denver", - "end_timezone": "America/Denver", - "idling_time_s": 0, - "tags": [] - } - ] - }, 200) - else: - _LOGGER.debug('UNKNOWN ROUTE') + mock_create_session.side_effect = get_session + session.get_vehicles.side_effect = get_vehicles + session.get_trips.side_effect = get_trips + config = { + 'platform': 'automatic', + 'username': 'bad_username', + 'password': 'bad_password', + 'client_id': 'client_id', + 'secret': 'client_secret', + 'devices': None, + } + result = hass.loop.run_until_complete( + async_setup_scanner(hass, config, mock_see)) -class TestAutomatic(unittest.TestCase): - """Test cases around the automatic device scanner.""" - - def see_mock(self, **kwargs): - """Mock see function.""" - self.assertEqual('vid', kwargs.get('dev_id')) - self.assertEqual(FUEL_LEVEL, - kwargs.get('attributes', {}).get('fuel_level')) - self.assertEqual((LATITUDE, LONGITUDE), kwargs.get('gps')) - self.assertEqual(ACCURACY, kwargs.get('gps_accuracy')) - - def setUp(self): - """Set up test data.""" - self.hass = get_test_home_assistant() - - def tearDown(self): - """Tear down test data.""" - self.hass.stop() - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_invalid_credentials(self, mock_get, mock_post): - """Test error is raised with invalid credentials.""" - config = { - 'platform': 'automatic', - 'username': INVALID_USERNAME, - 'password': PASSWORD, - 'client_id': CLIENT_ID, - 'secret': CLIENT_SECRET - } - - self.assertFalse(setup_scanner(self.hass, config, self.see_mock)) - - @patch('requests.get', side_effect=mocked_requests) - @patch('requests.post', side_effect=mocked_requests) - def test_valid_credentials(self, mock_get, mock_post): - """Test error is raised with invalid credentials.""" - config = { - 'platform': 'automatic', - 'username': VALID_USERNAME, - 'password': PASSWORD, - 'client_id': CLIENT_ID, - 'secret': CLIENT_SECRET - } - - self.assertTrue(setup_scanner(self.hass, config, self.see_mock)) + assert result + assert mock_see.called + assert len(mock_see.mock_calls) == 2 + assert mock_see.mock_calls[0][2]['dev_id'] == 'mock_id' + assert mock_see.mock_calls[0][2]['mac'] == 'mock_id' + assert mock_see.mock_calls[0][2]['host_name'] == 'mock_display_name' + assert mock_see.mock_calls[0][2]['attributes'] == {'fuel_level': 45.6} + assert mock_see.mock_calls[0][2]['gps'] == (45.567, 34.345) + assert mock_see.mock_calls[0][2]['gps_accuracy'] == 5.6 diff --git a/tests/components/device_tracker/test_mqtt_json.py b/tests/components/device_tracker/test_mqtt_json.py new file mode 100644 index 00000000000..fdca113a7ff --- /dev/null +++ b/tests/components/device_tracker/test_mqtt_json.py @@ -0,0 +1,128 @@ +"""The tests for the JSON MQTT device tracker platform.""" +import asyncio +import json +import unittest +from unittest.mock import patch +import logging +import os + +from homeassistant.setup import setup_component +from homeassistant.components import device_tracker +from homeassistant.const import CONF_PLATFORM + +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) + +_LOGGER = logging.getLogger(__name__) + +LOCATION_MESSAGE = { + 'longitude': 1.0, + 'gps_accuracy': 60, + 'latitude': 2.0, + 'battery_level': 99.9} + +LOCATION_MESSAGE_INCOMPLETE = { + 'longitude': 2.0} + + +class TestComponentsDeviceTrackerJSONMQTT(unittest.TestCase): + """Test JSON MQTT device tracker platform.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + try: + os.remove(self.hass.config.path(device_tracker.YAML_DEVICES)) + except FileNotFoundError: + pass + + def test_ensure_device_tracker_platform_validation(self): \ + # pylint: disable=invalid-name + """Test if platform validation was done.""" + @asyncio.coroutine + def mock_setup_scanner(hass, config, see, discovery_info=None): + """Check that Qos was added by validation.""" + self.assertTrue('qos' in config) + + with patch('homeassistant.components.device_tracker.mqtt_json.' + 'async_setup_scanner', autospec=True, + side_effect=mock_setup_scanner) as mock_sp: + + dev_id = 'paulus' + topic = 'location/paulus' + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: topic} + } + }) + assert mock_sp.call_count == 1 + + def test_json_message(self): + """Test json location message.""" + dev_id = 'zanzito' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE) + + self.hass.config.components = set(['mqtt_json', 'zone']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: topic} + } + }) + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + state = self.hass.states.get('device_tracker.zanzito') + self.assertEqual(state.attributes.get('latitude'), 2.0) + self.assertEqual(state.attributes.get('longitude'), 1.0) + + def test_non_json_message(self): + """Test receiving a non JSON message.""" + dev_id = 'zanzito' + topic = 'location/zanzito' + location = 'home' + + self.hass.config.components = set(['mqtt_json']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: topic} + } + }) + + with self.assertLogs(level='ERROR') as test_handle: + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIn( + "ERROR:homeassistant.components.device_tracker.mqtt_json:" + "Error parsing JSON payload: home", + test_handle.output[0]) + + def test_incomplete_message(self): + """Test receiving an incomplete message.""" + dev_id = 'zanzito' + topic = 'location/zanzito' + location = json.dumps(LOCATION_MESSAGE_INCOMPLETE) + + self.hass.config.components = set(['mqtt_json']) + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt_json', + 'devices': {dev_id: topic} + } + }) + + with self.assertLogs(level='ERROR') as test_handle: + fire_mqtt_message(self.hass, topic, location) + self.hass.block_till_done() + self.assertIn( + "ERROR:homeassistant.components.device_tracker.mqtt_json:" + "Skipping update for following data because of missing " + "or malformatted data: {\"longitude\": 2.0}", + test_handle.output[0]) diff --git a/tests/components/lock/test_zwave.py b/tests/components/lock/test_zwave.py index 33cc2e61483..9fb634f49e2 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/lock/test_zwave.py @@ -7,7 +7,7 @@ from homeassistant.components.lock import zwave from homeassistant.components.zwave import const from tests.mock.zwave import ( - MockNode, MockValue, MockEntityValues, value_changed) + MockNode, MockValue, MockEntityValues, value_changed) def test_get_device_detects_lock(mock_openzwave): @@ -171,6 +171,7 @@ def test_lock_alarm_level(mock_openzwave): @asyncio.coroutine def test_lock_set_usercode_service(hass, mock_openzwave): """Test the zwave lock set_usercode service.""" + mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=None, node=node, index=0) value1 = MockValue(data=None, node=node, index=1) @@ -182,31 +183,29 @@ def test_lock_set_usercode_service(hass, mock_openzwave): value1.value_id: value1, } - with patch.object(zwave.zwave, 'NETWORK') as mock_network: - mock_network.nodes = { - node.node_id: node - } - yield from hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { - const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_USERCODE: '1234', - zwave.ATTR_CODE_SLOT: 1, - }) - yield from hass.async_block_till_done() + mock_network.nodes = { + node.node_id: node + } + yield from hass.services.async_call( + zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { + const.ATTR_NODE_ID: node.node_id, + zwave.ATTR_USERCODE: '1234', + zwave.ATTR_CODE_SLOT: 1, + }) + yield from hass.async_block_till_done() assert value1.data == '1234' - with patch.object(zwave.zwave, 'NETWORK') as mock_network: - mock_network.nodes = { - node.node_id: node - } - yield from hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { - const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_USERCODE: '12345', - zwave.ATTR_CODE_SLOT: 1, - }) - yield from hass.async_block_till_done() + mock_network.nodes = { + node.node_id: node + } + yield from hass.services.async_call( + zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { + const.ATTR_NODE_ID: node.node_id, + zwave.ATTR_USERCODE: '12345', + zwave.ATTR_CODE_SLOT: 1, + }) + yield from hass.async_block_till_done() assert value1.data == '1234' @@ -214,6 +213,7 @@ def test_lock_set_usercode_service(hass, mock_openzwave): @asyncio.coroutine def test_lock_get_usercode_service(hass, mock_openzwave): """Test the zwave lock get_usercode service.""" + mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=None, node=node, index=0) value1 = MockValue(data='1234', node=node, index=1) @@ -225,27 +225,24 @@ def test_lock_get_usercode_service(hass, mock_openzwave): value1.value_id: value1, } - with patch.object(zwave.zwave, 'NETWORK') as mock_network: - with patch.object(zwave, '_LOGGER') as mock_logger: - mock_network.nodes = { - node.node_id: node - } - yield from hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_GET_USERCODE, { - const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_CODE_SLOT: 1, - }) - yield from hass.async_block_till_done() - - # This service only seems to write to the log - assert mock_logger.info.called - assert len(mock_logger.info.mock_calls) == 1 - assert mock_logger.info.mock_calls[0][1][2] == '1234' + with patch.object(zwave, '_LOGGER') as mock_logger: + mock_network.nodes = {node.node_id: node} + yield from hass.services.async_call( + zwave.DOMAIN, zwave.SERVICE_GET_USERCODE, { + const.ATTR_NODE_ID: node.node_id, + zwave.ATTR_CODE_SLOT: 1, + }) + yield from hass.async_block_till_done() + # This service only seems to write to the log + assert mock_logger.info.called + assert len(mock_logger.info.mock_calls) == 1 + assert mock_logger.info.mock_calls[0][1][2] == '1234' @asyncio.coroutine def test_lock_clear_usercode_service(hass, mock_openzwave): """Test the zwave lock clear_usercode service.""" + mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode(node_id=12) value0 = MockValue(data=None, node=node, index=0) value1 = MockValue(data='123', node=node, index=1) @@ -257,15 +254,14 @@ def test_lock_clear_usercode_service(hass, mock_openzwave): value1.value_id: value1, } - with patch.object(zwave.zwave, 'NETWORK') as mock_network: - mock_network.nodes = { - node.node_id: node - } - yield from hass.services.async_call( - zwave.DOMAIN, zwave.SERVICE_CLEAR_USERCODE, { - const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_CODE_SLOT: 1 - }) - yield from hass.async_block_till_done() + mock_network.nodes = { + node.node_id: node + } + yield from hass.services.async_call( + zwave.DOMAIN, zwave.SERVICE_CLEAR_USERCODE, { + const.ATTR_NODE_ID: node.node_id, + zwave.ATTR_CODE_SLOT: 1 + }) + yield from hass.async_block_till_done() assert value1.data == '\0\0\0' diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 582f5f8eb1c..c29d41cc590 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -37,6 +37,8 @@ class TestCastMediaPlayer(unittest.TestCase): assert not mock_device.called # Test chromecasts as if they were automatically discovered - cast.setup_platform(None, {}, lambda _: _, ('some_host', - cast.DEFAULT_PORT)) + cast.setup_platform(None, {}, lambda _: _, { + 'host': 'some_host', + 'port': cast.DEFAULT_PORT, + }) assert not mock_device.called diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index c157b8651a4..0a111ef3b36 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -143,7 +143,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_discovery(self, *args): """Test a single device using the autodiscovery provided by HASS.""" - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1) self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen') @@ -250,7 +252,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'join') def test_sonos_group_players(self, join_mock, *args): """Ensuring soco methods called for sonos_group_players service.""" - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass @@ -268,7 +272,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'unjoin') def test_sonos_unjoin(self, unjoinMock, *args): """Ensuring soco methods called for sonos_unjoin service.""" - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass @@ -282,7 +288,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass @@ -294,7 +302,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(SoCoMock, 'set_sleep_timer') def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args): """Ensuring soco methods called for sonos_clear_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + sonos.setup_platform(self.hass, {}, mock.MagicMock(), { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass @@ -306,7 +316,9 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') def test_sonos_snapshot(self, snapshotMock, *args): """Ensuring soco methods called for sonos_snapshot service.""" - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass @@ -322,7 +334,9 @@ class TestSonosMediaPlayer(unittest.TestCase): """Ensuring soco methods called for sonos_restor service.""" from soco.snapshot import Snapshot - sonos.setup_platform(self.hass, {}, fake_add_device, '192.0.2.1') + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) device = self.hass.data[sonos.DATA_SONOS][-1] device.hass = self.hass diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py index 84551241694..4958f5ee263 100644 --- a/tests/components/media_player/test_soundtouch.py +++ b/tests/components/media_player/test_soundtouch.py @@ -155,21 +155,67 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" logging.disable(logging.NOTSET) - soundtouch.DEVICES = [] self.hass.stop() @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) def test_ensure_setup_config(self, mocked_sountouch_device): - """Test setup OK.""" + """Test setup OK with custom config.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - # soundtouch.DEVICES[0].entity_id = 'entity_1' - self.assertEqual(len(soundtouch.DEVICES), 1) - self.assertEqual(soundtouch.DEVICES[0].name, 'soundtouch') - self.assertEqual(soundtouch.DEVICES[0].config['port'], 8090) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(len(all_devices), 1) + self.assertEqual(all_devices[0].name, 'soundtouch') + self.assertEqual(all_devices[0].config['port'], 8090) self.assertEqual(mocked_sountouch_device.call_count, 1) + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_discovery(self, mocked_sountouch_device): + """Test setup with discovery.""" + new_device = {"port": "8090", + "host": "192.168.1.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + new_device) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(len(all_devices), 1) + self.assertEqual(all_devices[0].config['port'], 8090) + self.assertEqual(all_devices[0].config['host'], '192.168.1.1') + self.assertEqual(mocked_sountouch_device.call_count, 1) + + @mock.patch('libsoundtouch.soundtouch_device', side_effect=None) + def test_ensure_setup_discovery_no_duplicate(self, + mocked_sountouch_device): + """Test setup OK if device already exists.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 1) + new_device = {"port": "8090", + "host": "192.168.1.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + new_device # New device + ) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) + existing_device = {"port": "8090", + "host": "192.168.0.1", + "properties": {}, + "hostname": "hostname.local"} + soundtouch.setup_platform(self.hass, + None, + mock.MagicMock(), + existing_device # Existing device + ) + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2) + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @mock.patch('libsoundtouch.soundtouch_device', @@ -183,7 +229,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - soundtouch.DEVICES[0].update() + self.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update() self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) @@ -201,13 +247,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) - self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") - self.assertEqual(soundtouch.DEVICES[0].media_title, "artist - track") - self.assertEqual(soundtouch.DEVICES[0].media_track, "track") - self.assertEqual(soundtouch.DEVICES[0].media_artist, "artist") - self.assertEqual(soundtouch.DEVICES[0].media_album_name, "album") - self.assertEqual(soundtouch.DEVICES[0].media_duration, 1) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PLAYING) + self.assertEqual(all_devices[0].media_image_url, "image.url") + self.assertEqual(all_devices[0].media_title, "artist - track") + self.assertEqual(all_devices[0].media_track, "track") + self.assertEqual(all_devices[0].media_artist, "artist") + self.assertEqual(all_devices[0].media_album_name, "album") + self.assertEqual(all_devices[0].media_duration, 1) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -223,7 +270,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].media_title, None) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].media_title, None) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -239,13 +287,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].state, STATE_PLAYING) - self.assertEqual(soundtouch.DEVICES[0].media_image_url, "image.url") - self.assertEqual(soundtouch.DEVICES[0].media_title, "station") - self.assertEqual(soundtouch.DEVICES[0].media_track, None) - self.assertEqual(soundtouch.DEVICES[0].media_artist, None) - self.assertEqual(soundtouch.DEVICES[0].media_album_name, None) - self.assertEqual(soundtouch.DEVICES[0].media_duration, None) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PLAYING) + self.assertEqual(all_devices[0].media_image_url, "image.url") + self.assertEqual(all_devices[0].media_title, "station") + self.assertEqual(all_devices[0].media_track, None) + self.assertEqual(all_devices[0].media_artist, None) + self.assertEqual(all_devices[0].media_album_name, None) + self.assertEqual(all_devices[0].media_duration, None) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', side_effect=MockVolume) @@ -261,7 +310,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].volume_level, 0.12) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].volume_level, 0.12) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -277,7 +327,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].state, STATE_OFF) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_OFF) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status', @@ -293,7 +344,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].state, STATE_PAUSED) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].state, STATE_PAUSED) @mock.patch('libsoundtouch.device.SoundTouchDevice.volume', side_effect=MockVolumeMuted) @@ -309,7 +361,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].is_volume_muted, True) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].is_volume_muted, True) @mock.patch('libsoundtouch.soundtouch_device') def test_media_commands(self, mocked_sountouch_device): @@ -318,7 +371,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): default_component(), mock.MagicMock()) self.assertEqual(mocked_sountouch_device.call_count, 1) - self.assertEqual(soundtouch.DEVICES[0].supported_features, 17853) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + self.assertEqual(all_devices[0].supported_features, 17853) @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @@ -331,7 +385,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].turn_off() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].turn_off() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) @@ -348,7 +403,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].turn_on() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].turn_on() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) @@ -365,7 +421,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].volume_up() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].volume_up() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) @@ -382,7 +439,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].volume_down() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].volume_down() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) @@ -399,7 +457,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].set_volume_level(0.17) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].set_volume_level(0.17) self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) @@ -416,7 +475,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].mute_volume(None) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].mute_volume(None) self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 2) @@ -433,7 +493,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_play() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].media_play() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) @@ -450,7 +511,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_pause() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].media_pause() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) @@ -467,7 +529,8 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].media_play_pause() + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].media_play_pause() self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 1) @@ -486,13 +549,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - soundtouch.DEVICES[0].media_next_track() + all_devices[0].media_next_track() self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_next_track.call_count, 1) - soundtouch.DEVICES[0].media_previous_track() + all_devices[0].media_previous_track() self.assertEqual(mocked_status.call_count, 3) self.assertEqual(mocked_previous_track.call_count, 1) @@ -509,13 +573,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] self.assertEqual(mocked_sountouch_device.call_count, 1) self.assertEqual(mocked_status.call_count, 1) self.assertEqual(mocked_volume.call_count, 1) - soundtouch.DEVICES[0].play_media('PLAYLIST', 1) + all_devices[0].play_media('PLAYLIST', 1) self.assertEqual(mocked_presets.call_count, 1) self.assertEqual(mocked_select_preset.call_count, 1) - soundtouch.DEVICES[0].play_media('PLAYLIST', 2) + all_devices[0].play_media('PLAYLIST', 2) self.assertEqual(mocked_presets.call_count, 2) self.assertEqual(mocked_select_preset.call_count, 1) @@ -533,26 +598,30 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.entity_2" self.assertEqual(mocked_sountouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) # one master, one slave => create zone - service = MockService("entity_1", []) - soundtouch.play_everywhere_service(service) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_1"}, True) self.assertEqual(mocked_create_zone.call_count, 1) # unknown master. create zone is must not be called - service = MockService("entity_X", []) - soundtouch.play_everywhere_service(service) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_X"}, True) self.assertEqual(mocked_create_zone.call_count, 1) # no slaves, create zone must not be called - soundtouch.DEVICES.pop(1) - service = MockService("entity_1", []) - soundtouch.play_everywhere_service(service) + all_devices.pop(1) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_PLAY_EVERYWHERE, + {"master": "media_player.entity_1"}, True) self.assertEqual(mocked_create_zone.call_count, 1) @mock.patch('libsoundtouch.device.SoundTouchDevice.create_zone') @@ -569,63 +638,34 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.entity_2" self.assertEqual(mocked_sountouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) # one master, one slave => create zone - service = MockService("entity_1", ["entity_2"]) - soundtouch.create_zone_service(service) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) self.assertEqual(mocked_create_zone.call_count, 1) # unknown master. create zone is must not be called - service = MockService("entity_X", []) - soundtouch.create_zone_service(service) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) self.assertEqual(mocked_create_zone.call_count, 1) # no slaves, create zone must not be called - soundtouch.DEVICES.pop(1) - service = MockService("entity_1", []) - soundtouch.create_zone_service(service) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_CREATE_ZONE, + {"master": "media_player.entity_X", + "slaves": []}, True) self.assertEqual(mocked_create_zone.call_count, 1) - @mock.patch('libsoundtouch.device.SoundTouchDevice.add_zone_slave') - @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') - @mock.patch('libsoundtouch.device.SoundTouchDevice.status') - @mock.patch('libsoundtouch.soundtouch_device', - side_effect=_mock_soundtouch_device) - def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, - mocked_volume, mocked_add_zone_slave): - """Test adding a slave to an existing zone.""" - soundtouch.setup_platform(self.hass, - default_component(), - mock.MagicMock()) - soundtouch.setup_platform(self.hass, - default_component(), - mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" - self.assertEqual(mocked_sountouch_device.call_count, 2) - self.assertEqual(mocked_status.call_count, 2) - self.assertEqual(mocked_volume.call_count, 2) - - # remove one slave - service = MockService("entity_1", ["entity_2"]) - soundtouch.add_zone_slave(service) - self.assertEqual(mocked_add_zone_slave.call_count, 1) - - # unknown master. add zone slave is not called - service = MockService("entity_X", ["entity_2"]) - soundtouch.add_zone_slave(service) - self.assertEqual(mocked_add_zone_slave.call_count, 1) - - # no slave to add, add zone slave is not called - service = MockService("entity_1", []) - soundtouch.add_zone_slave(service) - self.assertEqual(mocked_add_zone_slave.call_count, 1) - @mock.patch('libsoundtouch.device.SoundTouchDevice.remove_zone_slave') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') @mock.patch('libsoundtouch.device.SoundTouchDevice.status') @@ -633,6 +673,48 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): side_effect=_mock_soundtouch_device) def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status, mocked_volume, mocked_remove_zone_slave): + """Test adding a slave to an existing zone.""" + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + soundtouch.setup_platform(self.hass, + default_component(), + mock.MagicMock()) + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.entity_2" + self.assertEqual(mocked_sountouch_device.call_count, 2) + self.assertEqual(mocked_status.call_count, 2) + self.assertEqual(mocked_volume.call_count, 2) + + # remove one slave + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # unknown master. add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + # no slave to add, add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": []}, True) + self.assertEqual(mocked_remove_zone_slave.call_count, 1) + + @mock.patch('libsoundtouch.device.SoundTouchDevice.add_zone_slave') + @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') + @mock.patch('libsoundtouch.device.SoundTouchDevice.status') + @mock.patch('libsoundtouch.soundtouch_device', + side_effect=_mock_soundtouch_device) + def test_add_zone_slave(self, mocked_sountouch_device, mocked_status, + mocked_volume, mocked_add_zone_slave): """Test removing a slave from a zone.""" soundtouch.setup_platform(self.hass, default_component(), @@ -640,23 +722,30 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) - soundtouch.DEVICES[0].entity_id = "entity_1" - soundtouch.DEVICES[1].entity_id = "entity_2" + all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] + all_devices[0].entity_id = "media_player.entity_1" + all_devices[1].entity_id = "media_player.entity_2" self.assertEqual(mocked_sountouch_device.call_count, 2) self.assertEqual(mocked_status.call_count, 2) self.assertEqual(mocked_volume.call_count, 2) - # remove one slave - service = MockService("entity_1", ["entity_2"]) - soundtouch.remove_zone_slave(service) - self.assertEqual(mocked_remove_zone_slave.call_count, 1) + # add one slave + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_add_zone_slave.call_count, 1) - # unknown master. remove zone slave is not called - service = MockService("entity_X", ["entity_2"]) - soundtouch.remove_zone_slave(service) - self.assertEqual(mocked_remove_zone_slave.call_count, 1) + # unknown master. add zone slave is not called + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_X", + "slaves": ["media_player.entity_2"]}, True) + self.assertEqual(mocked_add_zone_slave.call_count, 1) # no slave to add, add zone slave is not called - service = MockService("entity_1", []) - soundtouch.remove_zone_slave(service) - self.assertEqual(mocked_remove_zone_slave.call_count, 1) + self.hass.services.call(soundtouch.DOMAIN, + soundtouch.SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_1", + "slaves": ["media_player.entity_X"]}, True) + self.assertEqual(mocked_add_zone_slave.call_count, 1) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f387c7c0bd7..0017674e82f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -209,6 +209,31 @@ class TestMQTT(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) + def test_subscribe_binary_topic(self): + """Test the subscription to a binary topic.""" + mqtt.subscribe(self.hass, 'test-topic', self.record_calls, + 0, None) + + fire_mqtt_message(self.hass, 'test-topic', 0x9a) + + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual('test-topic', self.calls[0][0]) + self.assertEqual(0x9a, self.calls[0][1]) + + def test_receiving_non_utf8_message_gets_logged(self): + """Test receiving a non utf8 encoded message.""" + mqtt.subscribe(self.hass, 'test-topic', self.record_calls) + + with self.assertLogs(level='ERROR') as test_handle: + fire_mqtt_message(self.hass, 'test-topic', 0x9a) + self.hass.block_till_done() + self.assertIn( + "ERROR:homeassistant.components.mqtt:Illegal payload " + "encoding utf-8 from MQTT " + "topic: test-topic, Payload: 154", + test_handle.output[0]) + class TestMQTTCallbacks(unittest.TestCase): """Test the MQTT callbacks.""" @@ -255,7 +280,8 @@ class TestMQTTCallbacks(unittest.TestCase): self.assertEqual(1, len(calls)) last_event = calls[0] - self.assertEqual('Hello World!', last_event['payload']) + self.assertEqual(bytearray('Hello World!', 'utf-8'), + last_event['payload']) self.assertEqual(message.topic, last_event['topic']) self.assertEqual(message.qos, last_event['qos']) @@ -298,38 +324,6 @@ class TestMQTTCallbacks(unittest.TestCase): self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic') self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one') - def test_receiving_non_utf8_message_gets_logged(self): - """Test receiving a non utf8 encoded message.""" - calls = [] - - @callback - def record(topic, payload, qos): - """Helper to record calls.""" - data = { - 'topic': topic, - 'payload': payload, - 'qos': qos, - } - calls.append(data) - - async_dispatcher_connect( - self.hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, record) - - payload = 0x9a - topic = 'test_topic' - MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload']) - message = MQTTMessage(topic, 1, payload) - with self.assertLogs(level='ERROR') as test_handle: - self.hass.data['mqtt']._mqtt_on_message( - None, - {'hass': self.hass}, - message) - self.hass.block_till_done() - self.assertIn( - "ERROR:homeassistant.components.mqtt:Illegal utf-8 unicode " - "payload from MQTT topic: %s, Payload: " % topic, - test_handle.output[0]) - @asyncio.coroutine def test_setup_embedded_starts_with_no_config(hass): diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py index a09f16b00fd..59e66ca82b6 100644 --- a/tests/components/sensor/test_dsmr.py +++ b/tests/components/sensor/test_dsmr.py @@ -56,7 +56,7 @@ def test_default_setup(hass, mock_connection_factory): telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject([ - {'value': Decimal('0.1'), 'unit': 'kWh'} + {'value': Decimal('0.0'), 'unit': 'kWh'} ]), ELECTRICITY_ACTIVE_TARIFF: CosemObject([ {'value': '0001', 'unit': ''} @@ -82,7 +82,7 @@ def test_default_setup(hass, mock_connection_factory): # ensure entities have new state value after incoming telegram power_consumption = hass.states.get('sensor.power_consumption') - assert power_consumption.state == '0.1' + assert power_consumption.state == '0.0' assert power_consumption.attributes.get('unit_of_measurement') is 'kWh' # tariff should be translated in human readable and have no unit @@ -199,5 +199,5 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory): # wait for sleep to resolve yield from hass.async_block_till_done() - assert connection_factory.call_count == 2, \ + assert connection_factory.call_count >= 2, \ 'connecting not retried' diff --git a/tests/components/sensor/test_min_max.py b/tests/components/sensor/test_min_max.py index b610775b39b..a6d6a5adc68 100644 --- a/tests/components/sensor/test_min_max.py +++ b/tests/components/sensor/test_min_max.py @@ -30,7 +30,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_min', 'type': 'min', 'entity_ids': [ 'sensor.test_1', @@ -59,7 +59,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_max', 'type': 'max', 'entity_ids': [ 'sensor.test_1', @@ -88,7 +88,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_mean', 'type': 'mean', 'entity_ids': [ 'sensor.test_1', @@ -117,7 +117,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_mean', 'type': 'mean', 'round_digits': 1, 'entity_ids': [ @@ -147,7 +147,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_mean', 'type': 'mean', 'round_digits': 4, 'entity_ids': [ @@ -177,7 +177,7 @@ class TestMinMaxSensor(unittest.TestCase): config = { 'sensor': { 'platform': 'min_max', - 'name': 'test', + 'name': 'test_max', 'type': 'max', 'entity_ids': [ 'sensor.test_1', @@ -191,7 +191,7 @@ class TestMinMaxSensor(unittest.TestCase): entity_ids = config['sensor']['entity_ids'] - self.hass.states.set(entity_ids[0], self.values[0]) + self.hass.states.set(entity_ids[0], STATE_UNKNOWN) self.hass.block_till_done() state = self.hass.states.get('sensor.test_max') @@ -201,14 +201,20 @@ class TestMinMaxSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get('sensor.test_max') - self.assertEqual(STATE_UNKNOWN, state.state) + self.assertNotEqual(STATE_UNKNOWN, state.state) - self.hass.states.set(entity_ids[2], self.values[2]) + self.hass.states.set(entity_ids[2], STATE_UNKNOWN) self.hass.block_till_done() state = self.hass.states.get('sensor.test_max') self.assertNotEqual(STATE_UNKNOWN, state.state) + self.hass.states.set(entity_ids[1], STATE_UNKNOWN) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_max') + self.assertEqual(STATE_UNKNOWN, state.state) + def test_different_unit_of_measurement(self): """Test for different unit of measurement.""" config = { @@ -232,21 +238,25 @@ class TestMinMaxSensor(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() - state = self.hass.states.get('sensor.test_mean') + state = self.hass.states.get('sensor.test') - self.assertEqual(STATE_UNKNOWN, state.state) + self.assertEqual(str(float(self.values[0])), state.state) self.assertEqual('°C', state.attributes.get('unit_of_measurement')) self.hass.states.set(entity_ids[1], self.values[1], {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNKNOWN, state.state) - self.assertEqual('°C', state.attributes.get('unit_of_measurement')) + self.assertEqual('ERR', state.attributes.get('unit_of_measurement')) self.hass.states.set(entity_ids[2], self.values[2], {ATTR_UNIT_OF_MEASUREMENT: '%'}) self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + self.assertEqual(STATE_UNKNOWN, state.state) - self.assertEqual('°C', state.attributes.get('unit_of_measurement')) + self.assertEqual('ERR', state.attributes.get('unit_of_measurement')) diff --git a/tests/components/test_api.py b/tests/components/test_api.py index e2d93c9cce7..8d6041b49c1 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -337,81 +337,6 @@ class TestAPI(unittest.TestCase): self.assertEqual(400, req.status_code) - def test_api_event_forward(self): - """Test setting up event forwarding.""" - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - headers=HA_HEADERS) - self.assertEqual(400, req.status_code) - - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({'host': '127.0.0.1'}), - headers=HA_HEADERS) - self.assertEqual(400, req.status_code) - - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({'api_password': 'bla-di-bla'}), - headers=HA_HEADERS) - self.assertEqual(400, req.status_code) - - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({ - 'api_password': 'bla-di-bla', - 'host': '127.0.0.1', - 'port': 'abcd' - }), - headers=HA_HEADERS) - self.assertEqual(422, req.status_code) - - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({ - 'api_password': 'bla-di-bla', - 'host': '127.0.0.1', - 'port': get_test_instance_port() - }), - headers=HA_HEADERS) - self.assertEqual(422, req.status_code) - - # Setup a real one - req = requests.post( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({ - 'api_password': API_PASSWORD, - 'host': '127.0.0.1', - 'port': SERVER_PORT - }), - headers=HA_HEADERS) - self.assertEqual(200, req.status_code) - - # Delete it again.. - req = requests.delete( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({}), - headers=HA_HEADERS) - self.assertEqual(400, req.status_code) - - req = requests.delete( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({ - 'host': '127.0.0.1', - 'port': 'abcd' - }), - headers=HA_HEADERS) - self.assertEqual(422, req.status_code) - - req = requests.delete( - _url(const.URL_API_EVENT_FORWARD), - data=json.dumps({ - 'host': '127.0.0.1', - 'port': SERVER_PORT - }), - headers=HA_HEADERS) - self.assertEqual(200, req.status_code) - def test_stream(self): """Test the stream.""" listen_count = self._listen_count() diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index 6b03ffa34e7..7073c420341 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -1,5 +1,6 @@ """The tests for the discovery component.""" import asyncio +import os from unittest.mock import patch @@ -128,3 +129,18 @@ def test_discover_duplicates(hass): mock_discover.assert_called_with( hass, SERVICE_NO_PLATFORM, SERVICE_INFO, SERVICE_NO_PLATFORM_COMPONENT, BASE_CONFIG) + + +@asyncio.coroutine +def test_load_component_hassio(hass): + """Test load hassio component.""" + def discover(netdisco): + """Fake discovery.""" + return [] + + with patch.dict(os.environ, {'HASSIO': "FAKE_HASSIO"}), \ + patch('homeassistant.components.hassio.async_setup', + return_value=mock_coro(return_value=True)) as mock_hassio: + yield from mock_discovery(hass, discover) + + assert mock_hassio.called diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py index bde419c4104..2574e7fa9f3 100644 --- a/tests/components/test_hassio.py +++ b/tests/components/test_hassio.py @@ -53,6 +53,8 @@ class TestHassIOSetup(object): assert self.hass.services.has_service( ho.DOMAIN, ho.SERVICE_SUPERVISOR_UPDATE) + assert self.hass.services.has_service( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD) assert self.hass.services.has_service( ho.DOMAIN, ho.SERVICE_ADDON_INSTALL) @@ -216,6 +218,22 @@ class TestHassIOComponent(object): assert len(aioclient_mock.mock_calls) == 2 assert aioclient_mock.mock_calls[-1][2]['version'] == '0.4' + def test_rest_command_http_supervisor_reload(self, aioclient_mock): + """Call a hassio for supervisor reload.""" + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json=self.ok_msg) + with assert_setup_component(0, ho.DOMAIN): + setup_component(self.hass, ho.DOMAIN, self.config) + + aioclient_mock.get( + self.url.format("supervisor/reload"), json=self.ok_msg) + + self.hass.services.call( + ho.DOMAIN, ho.SERVICE_SUPERVISOR_RELOAD, {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 2 + def test_rest_command_http_homeassistant_update(self, aioclient_mock): """Call a hassio for homeassistant update.""" aioclient_mock.get( diff --git a/tests/components/test_rest_command.py b/tests/components/test_rest_command.py index 0648a30c922..9dbea53cd64 100644 --- a/tests/components/test_rest_command.py +++ b/tests/components/test_rest_command.py @@ -221,3 +221,22 @@ class TestRestCommandComponent(object): assert len(aioclient_mock.mock_calls) == 1 assert aioclient_mock.mock_calls[0][2] == b'data' + + def test_rest_command_content_type(self, aioclient_mock): + """Call a rest command with a content type.""" + data = { + 'payload': 'item', + 'content_type': 'text/plain' + } + self.config[rc.DOMAIN]['post_test'].update(data) + + with assert_setup_component(4): + setup_component(self.hass, rc.DOMAIN, self.config) + + aioclient_mock.post(self.url, content=b'success') + + self.hass.services.call(rc.DOMAIN, 'post_test', {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == b'item' diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 33fc5ad40c5..659e4b1a43d 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -44,6 +44,38 @@ class TestSun(unittest.TestCase): latitude = self.hass.config.latitude longitude = self.hass.config.longitude + mod = -1 + while True: + next_dawn = (astral.dawn_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_dawn > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_dusk = (astral.dusk_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_dusk > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_midnight = (astral.solar_midnight_utc(utc_now + + timedelta(days=mod), longitude)) + if next_midnight > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_noon = (astral.solar_noon_utc(utc_now + + timedelta(days=mod), longitude)) + if next_noon > utc_now: + break + mod += 1 + mod = -1 while True: next_rising = (astral.sunrise_utc(utc_now + @@ -60,15 +92,27 @@ class TestSun(unittest.TestCase): break mod += 1 + self.assertEqual(next_dawn, sun.next_dawn_utc(self.hass)) + self.assertEqual(next_dusk, sun.next_dusk_utc(self.hass)) + self.assertEqual(next_midnight, sun.next_midnight_utc(self.hass)) + self.assertEqual(next_noon, sun.next_noon_utc(self.hass)) self.assertEqual(next_rising, sun.next_rising_utc(self.hass)) self.assertEqual(next_setting, sun.next_setting_utc(self.hass)) # Point it at a state without the proper attributes self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) + self.assertIsNone(sun.next_dawn(self.hass)) + self.assertIsNone(sun.next_dusk(self.hass)) + self.assertIsNone(sun.next_midnight(self.hass)) + self.assertIsNone(sun.next_noon(self.hass)) self.assertIsNone(sun.next_rising(self.hass)) self.assertIsNone(sun.next_setting(self.hass)) # Point it at a non-existing state + self.assertIsNone(sun.next_dawn(self.hass, 'non.existing')) + self.assertIsNone(sun.next_dusk(self.hass, 'non.existing')) + self.assertIsNone(sun.next_midnight(self.hass, 'non.existing')) + self.assertIsNone(sun.next_noon(self.hass, 'non.existing')) self.assertIsNone(sun.next_rising(self.hass, 'non.existing')) self.assertIsNone(sun.next_setting(self.hass, 'non.existing')) diff --git a/tests/components/tts/test_google.py b/tests/components/tts/test_google.py index 4cbec95dc2b..9f7cc9e9d50 100644 --- a/tests/components/tts/test_google.py +++ b/tests/components/tts/test_google.py @@ -23,10 +23,11 @@ class TestTTSGooglePlatform(object): self.url = "http://translate.google.com/translate_tts" self.url_param = { 'tl': 'en', - 'q': 'I%20person%20is%20on%20front%20of%20your%20door.', + 'q': + '90%25%20of%20I%20person%20is%20on%20front%20of%20your%20door.', 'tk': 5, 'client': 'tw-ob', - 'textlen': 34, + 'textlen': 41, 'total': 1, 'idx': 0, 'ie': 'UTF-8', @@ -70,7 +71,7 @@ class TestTTSGooglePlatform(object): setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call(tts.DOMAIN, 'google_say', { - tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", }) self.hass.block_till_done() @@ -99,7 +100,7 @@ class TestTTSGooglePlatform(object): setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call(tts.DOMAIN, 'google_say', { - tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", }) self.hass.block_till_done() @@ -126,7 +127,7 @@ class TestTTSGooglePlatform(object): setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call(tts.DOMAIN, 'google_say', { - tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", tts.ATTR_LANGUAGE: "de" }) self.hass.block_till_done() @@ -153,7 +154,7 @@ class TestTTSGooglePlatform(object): setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call(tts.DOMAIN, 'google_say', { - tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", }) self.hass.block_till_done() @@ -179,7 +180,7 @@ class TestTTSGooglePlatform(object): setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call(tts.DOMAIN, 'google_say', { - tts.ATTR_MESSAGE: "I person is on front of your door.", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", }) self.hass.block_till_done() diff --git a/tests/components/tts/test_marytts.py b/tests/components/tts/test_marytts.py new file mode 100644 index 00000000000..29e1a635462 --- /dev/null +++ b/tests/components/tts/test_marytts.py @@ -0,0 +1,121 @@ +"""The tests for the MaryTTS speech platform.""" +import asyncio +import os +import shutil + +import homeassistant.components.tts as tts +from homeassistant.setup import setup_component +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) + +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTSMaryTTSPlatform(object): + """Test the speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + self.url = "http://localhost:59125/process?" + self.url_param = { + 'INPUT_TEXT': 'HomeAssistant', + 'INPUT_TYPE': 'TEXT', + 'AUDIO': 'WAVE', + 'VOICE': 'cmu-slt-hsmm', + 'OUTPUT_TYPE': 'AUDIO', + 'LOCALE': 'en_US' + } + + def teardown_method(self): + """Stop everything that was started.""" + default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + self.hass.stop() + + def test_setup_component(self): + """Test setup component.""" + config = { + tts.DOMAIN: { + 'platform': 'marytts' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_service_say(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1 + + def test_service_say_timeout(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=200, + exc=asyncio.TimeoutError()) + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + def test_service_say_http_error(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + aioclient_mock.get( + self.url, params=self.url_param, status=403, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'marytts', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'marytts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0 diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 14b2a0226fe..91902f9c4a8 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1,11 +1,28 @@ """Tests for the Z-Wave init.""" import asyncio -import unittest from collections import OrderedDict from homeassistant.bootstrap import async_setup_component +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.components import zwave +from homeassistant.components.binary_sensor.zwave import get_device from homeassistant.components.zwave import ( - CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB) + const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, ZWAVE_NETWORK) +from homeassistant.setup import setup_component + +import pytest +import unittest +from unittest.mock import patch, MagicMock + +from tests.common import get_test_home_assistant +from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues + + +@asyncio.coroutine +def test_missing_openzwave(hass): + """Test that missing openzwave lib stops setup.""" + result = yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + assert not result @asyncio.coroutine @@ -40,6 +57,395 @@ def test_invalid_device_config(hass, mock_openzwave): assert not result +def test_config_access_error(): + """Test threading error accessing config values.""" + node = MagicMock() + + def side_effect(): + raise RuntimeError + + node.values.values.side_effect = side_effect + result = zwave.get_config_value(node, 1) + assert result is None + + +@asyncio.coroutine +def test_setup_platform(hass, mock_openzwave): + """Test invalid device config.""" + mock_device = MagicMock() + hass.data[ZWAVE_NETWORK] = MagicMock() + hass.data[zwave.DATA_ZWAVE_DICT] = {456: mock_device} + async_add_devices = MagicMock() + + result = yield from zwave.async_setup_platform( + hass, None, async_add_devices, None) + assert not result + assert not async_add_devices.called + + result = yield from zwave.async_setup_platform( + hass, None, async_add_devices, {const.DISCOVERY_DEVICE: 123}) + assert not result + assert not async_add_devices.called + + result = yield from zwave.async_setup_platform( + hass, None, async_add_devices, {const.DISCOVERY_DEVICE: 456}) + assert result + assert async_add_devices.called + assert len(async_add_devices.mock_calls) == 1 + assert async_add_devices.mock_calls[0][1][0] == [mock_device] + + +@asyncio.coroutine +def test_zwave_ready_wait(hass, mock_openzwave): + """Test that zwave continues after waiting for network ready.""" + # Initialize zwave + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + yield from hass.async_block_till_done() + + with patch.object(zwave.time, 'sleep') as mock_sleep: + with patch.object(zwave, '_LOGGER') as mock_logger: + hass.data[ZWAVE_NETWORK].state = MockNetwork.STATE_STARTED + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + yield from hass.async_block_till_done() + + assert mock_sleep.called + assert len(mock_sleep.mock_calls) == const.NETWORK_READY_WAIT_SECS + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + assert mock_logger.warning.mock_calls[0][1][1] == \ + const.NETWORK_READY_WAIT_SECS + + +@asyncio.coroutine +def test_device_entity(hass, mock_openzwave): + """Test device entity base class.""" + node = MockNode(node_id='10', name='Mock Node') + value = MockValue(data=False, node=node, instance=2, object_id='11', + label='Sensor', + command_class=const.COMMAND_CLASS_SENSOR_BINARY) + power_value = MockValue(data=50.123456, node=node, precision=3, + command_class=const.COMMAND_CLASS_METER) + values = MockEntityValues(primary=value, power=power_value) + device = zwave.ZWaveDeviceEntity(values, 'zwave') + device.hass = hass + device.value_added() + device.update_properties() + yield from hass.async_block_till_done() + + assert not device.should_poll + assert device.unique_id == "ZWAVE-10-11" + assert device.name == 'Mock Node Sensor' + assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 + + +class TestZWaveDeviceEntityValues(unittest.TestCase): + """Tests for the ZWaveDeviceEntityValues helper.""" + + @pytest.fixture(autouse=True) + def set_mock_openzwave(self, mock_openzwave): + """Use the mock_openzwave fixture for this class.""" + self.mock_openzwave = mock_openzwave + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.hass.start() + + setup_component(self.hass, 'zwave', {'zwave': {}}) + self.hass.block_till_done() + + self.node = MockNode() + self.mock_schema = { + const.DISC_COMPONENT: 'mock_component', + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: ['mock_primary_class'], + }, + 'secondary': { + const.DISC_COMMAND_CLASS: ['mock_secondary_class'], + }, + 'optional': { + const.DISC_COMMAND_CLASS: ['mock_optional_class'], + const.DISC_OPTIONAL: True, + }}} + self.primary = MockValue( + command_class='mock_primary_class', node=self.node) + self.secondary = MockValue( + command_class='mock_secondary_class', node=self.node) + self.duplicate_secondary = MockValue( + command_class='mock_secondary_class', node=self.node) + self.optional = MockValue( + command_class='mock_optional_class', node=self.node) + self.no_match_value = MockValue( + command_class='mock_bad_class', node=self.node) + + self.entity_id = '{}.{}'.format('mock_component', + zwave.object_id(self.primary)) + self.zwave_config = {} + self.device_config = {self.entity_id: {}} + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_discovery(self, discovery, get_platform): + """Test the creation of a new entity.""" + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + + assert values.primary is self.primary + assert len(list(values)) == 3 + self.assertEqual(sorted(list(values), + key=lambda a: id(a)), + sorted([self.primary, None, None], + key=lambda a: id(a))) + assert not discovery.async_load_platform.called + + values.check_value(self.secondary) + self.hass.block_till_done() + + assert values.secondary is self.secondary + assert len(list(values)) == 3 + self.assertEqual(sorted(list(values), + key=lambda a: id(a)), + sorted([self.primary, self.secondary, None], + key=lambda a: id(a))) + + assert discovery.async_load_platform.called + # Second call is to async yield from + assert len(discovery.async_load_platform.mock_calls) == 2 + args = discovery.async_load_platform.mock_calls[0][1] + assert args[0] == self.hass + assert args[1] == 'mock_component' + assert args[2] == 'zwave' + assert args[3] == {const.DISCOVERY_DEVICE: id(values)} + assert args[4] == self.zwave_config + + discovery.async_load_platform.reset_mock() + values.check_value(self.optional) + values.check_value(self.duplicate_secondary) + values.check_value(self.no_match_value) + self.hass.block_till_done() + + assert values.optional is self.optional + assert len(list(values)) == 3 + self.assertEqual(sorted(list(values), + key=lambda a: id(a)), + sorted([self.primary, self.secondary, self.optional], + key=lambda a: id(a))) + assert not discovery.async_load_platform.called + + assert values._entity.value_added.called + assert len(values._entity.value_added.mock_calls) == 1 + assert values._entity.value_changed.called + assert len(values._entity.value_changed.mock_calls) == 1 + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_existing_values(self, discovery, get_platform): + """Test the loading of already discovered values.""" + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + self.optional.value_id: self.optional, + self.no_match_value.value_id: self.no_match_value, + } + + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + self.hass.block_till_done() + + assert values.primary is self.primary + assert values.secondary is self.secondary + assert values.optional is self.optional + assert len(list(values)) == 3 + self.assertEqual(sorted(list(values), + key=lambda a: id(a)), + sorted([self.primary, self.secondary, self.optional], + key=lambda a: id(a))) + + assert discovery.async_load_platform.called + # Second call is to async yield from + assert len(discovery.async_load_platform.mock_calls) == 2 + args = discovery.async_load_platform.mock_calls[0][1] + assert args[0] == self.hass + assert args[1] == 'mock_component' + assert args[2] == 'zwave' + assert args[3] == {const.DISCOVERY_DEVICE: id(values)} + assert args[4] == self.zwave_config + assert not self.primary.enable_poll.called + assert self.primary.disable_poll.called + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_node_schema_mismatch(self, discovery, get_platform): + """Test node schema mismatch.""" + self.node.generic = 'no_match' + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.mock_schema[const.DISC_GENERIC_DEVICE_CLASS] = ['generic_match'] + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_workaround_component(self, discovery, get_platform): + """Test ignore workaround.""" + self.node.manufacturer_id = '010f' + self.node.product_type = '0b00' + self.primary.command_class = const.COMMAND_CLASS_SENSOR_ALARM + self.entity_id = '{}.{}'.format('binary_sensor', + zwave.object_id(self.primary)) + self.device_config = {self.entity_id: {}} + + self.mock_schema = { + const.DISC_COMPONENT: 'mock_component', + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_SWITCH_BINARY], + }}} + + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert discovery.async_load_platform.called + # Second call is to async yield from + assert len(discovery.async_load_platform.mock_calls) == 2 + args = discovery.async_load_platform.mock_calls[0][1] + assert args[1] == 'binary_sensor' + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_workaround_ignore(self, discovery, get_platform): + """Test ignore workaround.""" + self.node.manufacturer_id = '010f' + self.node.product_type = '0301' + self.primary.command_class = const.COMMAND_CLASS_SWITCH_BINARY + + self.mock_schema = { + const.DISC_COMPONENT: 'mock_component', + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_SWITCH_BINARY], + }}} + + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_config_ignore(self, discovery, get_platform): + """Test ignore config.""" + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.device_config = {self.entity_id: { + zwave.CONF_IGNORED: True + }} + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_platform_ignore(self, discovery, get_platform): + """Test platform ignore device.""" + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + platform = MagicMock() + get_platform.return_value = platform + platform.get_device.return_value = None + zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_config_polling_intensity(self, discovery, get_platform): + """Test polling intensity.""" + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.device_config = {self.entity_id: { + zwave.CONF_POLLING_INTENSITY: 123, + }} + values = zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + ) + values._check_entity_ready() + self.hass.block_till_done() + + assert discovery.async_load_platform.called + assert self.primary.enable_poll.called + assert len(self.primary.enable_poll.mock_calls) == 1 + assert self.primary.enable_poll.mock_calls[0][1][0] == 123 + assert not self.primary.disable_poll.called + + class TestZwave(unittest.TestCase): """Test zwave init.""" @@ -49,3 +455,371 @@ class TestZwave(unittest.TestCase): {'zwave': {CONF_DEVICE_CONFIG_GLOB: OrderedDict()}}) self.assertIsInstance( conf['zwave'][CONF_DEVICE_CONFIG_GLOB], OrderedDict) + + +class TestZWaveServices(unittest.TestCase): + """Tests for zwave services.""" + + @pytest.fixture(autouse=True) + def set_mock_openzwave(self, mock_openzwave): + """Use the mock_openzwave fixture for this class.""" + self.mock_openzwave = mock_openzwave + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.hass.start() + + # Initialize zwave + setup_component(self.hass, 'zwave', {'zwave': {}}) + self.hass.block_till_done() + self.zwave_network = self.hass.data[ZWAVE_NETWORK] + self.zwave_network.state = MockNetwork.STATE_READY + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.services.call('zwave', 'stop_network', {}) + self.hass.block_till_done() + self.hass.stop() + + def test_add_node(self): + """Test zwave add_node service.""" + self.hass.services.call('zwave', 'add_node', {}) + self.hass.block_till_done() + + assert self.zwave_network.controller.add_node.called + assert len(self.zwave_network.controller + .add_node.mock_calls) == 1 + assert len(self.zwave_network.controller + .add_node.mock_calls[0][1]) == 0 + + def test_add_node_secure(self): + """Test zwave add_node_secure service.""" + self.hass.services.call('zwave', 'add_node_secure', {}) + self.hass.block_till_done() + + assert self.zwave_network.controller.add_node.called + assert len(self.zwave_network.controller.add_node.mock_calls) == 1 + assert (self.zwave_network.controller + .add_node.mock_calls[0][1][0] is True) + + def test_remove_node(self): + """Test zwave remove_node service.""" + self.hass.services.call('zwave', 'remove_node', {}) + self.hass.block_till_done() + + assert self.zwave_network.controller.remove_node.called + assert len(self.zwave_network.controller.remove_node.mock_calls) == 1 + + def test_cancel_command(self): + """Test zwave cancel_command service.""" + self.hass.services.call('zwave', 'cancel_command', {}) + self.hass.block_till_done() + + assert self.zwave_network.controller.cancel_command.called + assert len(self.zwave_network.controller + .cancel_command.mock_calls) == 1 + + def test_heal_network(self): + """Test zwave heal_network service.""" + self.hass.services.call('zwave', 'heal_network', {}) + self.hass.block_till_done() + + assert self.zwave_network.heal.called + assert len(self.zwave_network.heal.mock_calls) == 1 + + def test_soft_reset(self): + """Test zwave soft_reset service.""" + self.hass.services.call('zwave', 'soft_reset', {}) + self.hass.block_till_done() + + assert self.zwave_network.controller.soft_reset.called + assert len(self.zwave_network.controller.soft_reset.mock_calls) == 1 + + def test_test_network(self): + """Test zwave test_network service.""" + self.hass.services.call('zwave', 'test_network', {}) + self.hass.block_till_done() + + assert self.zwave_network.test.called + assert len(self.zwave_network.test.mock_calls) == 1 + + def test_stop_network(self): + """Test zwave stop_network service.""" + with patch.object(self.hass.bus, 'fire') as mock_fire: + self.hass.services.call('zwave', 'stop_network', {}) + self.hass.block_till_done() + + assert self.zwave_network.stop.called + assert len(self.zwave_network.stop.mock_calls) == 1 + assert mock_fire.called + assert len(mock_fire.mock_calls) == 2 + assert mock_fire.mock_calls[0][1][0] == const.EVENT_NETWORK_STOP + + def test_rename_node(self): + """Test zwave rename_node service.""" + self.zwave_network.nodes = {11: MagicMock()} + self.hass.services.call('zwave', 'rename_node', { + const.ATTR_NODE_ID: 11, + const.ATTR_NAME: 'test_name', + }) + self.hass.block_till_done() + + assert self.zwave_network.nodes[11].name == 'test_name' + + def test_remove_failed_node(self): + """Test zwave remove_failed_node service.""" + self.hass.services.call('zwave', 'remove_failed_node', { + const.ATTR_NODE_ID: 12, + }) + self.hass.block_till_done() + + remove_failed_node = self.zwave_network.controller.remove_failed_node + assert remove_failed_node.called + assert len(remove_failed_node.mock_calls) == 1 + assert remove_failed_node.mock_calls[0][1][0] == 12 + + def test_replace_failed_node(self): + """Test zwave replace_failed_node service.""" + self.hass.services.call('zwave', 'replace_failed_node', { + const.ATTR_NODE_ID: 13, + }) + self.hass.block_till_done() + + replace_failed_node = self.zwave_network.controller.replace_failed_node + assert replace_failed_node.called + assert len(replace_failed_node.mock_calls) == 1 + assert replace_failed_node.mock_calls[0][1][0] == 13 + + def test_set_config_parameter(self): + """Test zwave set_config_parameter service.""" + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_CONFIGURATION, + ) + value_list = MockValue( + index=13, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_LIST, + data_items=['item1', 'item2', 'item3'], + ) + node = MockNode(node_id=14) + node.get_values.return_value = {12: value, 13: value_list} + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 13, + const.ATTR_CONFIG_VALUE: 1, + }) + self.hass.block_till_done() + + assert node.set_config_param.called + assert len(node.set_config_param.mock_calls) == 1 + assert node.set_config_param.mock_calls[0][1][0] == 13 + assert node.set_config_param.mock_calls[0][1][1] == 1 + assert node.set_config_param.mock_calls[0][1][2] == 2 + node.set_config_param.reset_mock() + + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 13, + const.ATTR_CONFIG_VALUE: 7, + }) + self.hass.block_till_done() + + assert not node.set_config_param.called + node.set_config_param.reset_mock() + + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 12, + const.ATTR_CONFIG_VALUE: 0x01020304, + const.ATTR_CONFIG_SIZE: 4, + }) + self.hass.block_till_done() + + assert node.set_config_param.called + assert len(node.set_config_param.mock_calls) == 1 + assert node.set_config_param.mock_calls[0][1][0] == 12 + assert node.set_config_param.mock_calls[0][1][1] == 0x01020304 + assert node.set_config_param.mock_calls[0][1][2] == 4 + node.set_config_param.reset_mock() + + def test_print_config_parameter(self): + """Test zwave print_config_parameter service.""" + value1 = MockValue( + index=12, + command_class=const.COMMAND_CLASS_CONFIGURATION, + data=1234, + ) + value2 = MockValue( + index=13, + command_class=const.COMMAND_CLASS_CONFIGURATION, + data=2345, + ) + node = MockNode(node_id=14) + node.values = {12: value1, 13: value2} + self.zwave_network.nodes = {14: node} + + with patch.object(zwave, '_LOGGER') as mock_logger: + self.hass.services.call('zwave', 'print_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 13, + }) + self.hass.block_till_done() + + assert mock_logger.info.called + assert len(mock_logger.info.mock_calls) == 1 + assert mock_logger.info.mock_calls[0][1][1] == 13 + assert mock_logger.info.mock_calls[0][1][2] == 14 + assert mock_logger.info.mock_calls[0][1][3] == 2345 + + def test_print_node(self): + """Test zwave print_config_parameter service.""" + node1 = MockNode(node_id=14) + node2 = MockNode(node_id=15) + self.zwave_network.nodes = {14: node1, 15: node2} + + with patch.object(zwave, 'pprint') as mock_pprint: + self.hass.services.call('zwave', 'print_node', { + const.ATTR_NODE_ID: 15, + }) + self.hass.block_till_done() + + assert mock_pprint.called + assert len(mock_pprint.mock_calls) == 1 + assert mock_pprint.mock_calls[0][1][0]['node_id'] == 15 + + def test_set_wakeup(self): + """Test zwave set_wakeup service.""" + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_WAKE_UP, + ) + node = MockNode(node_id=14) + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'set_wakeup', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_VALUE: 15, + }) + self.hass.block_till_done() + + assert value.data == 15 + + node.can_wake_up_value = False + self.hass.services.call('zwave', 'set_wakeup', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_VALUE: 20, + }) + self.hass.block_till_done() + + assert value.data == 15 + + def test_add_association(self): + """Test zwave change_association service.""" + ZWaveGroup = self.mock_openzwave.group.ZWaveGroup + group = MagicMock() + ZWaveGroup.return_value = group + + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_WAKE_UP, + ) + node = MockNode(node_id=14) + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'change_association', { + const.ATTR_ASSOCIATION: 'add', + const.ATTR_NODE_ID: 14, + const.ATTR_TARGET_NODE_ID: 24, + const.ATTR_GROUP: 3, + const.ATTR_INSTANCE: 5, + }) + self.hass.block_till_done() + + assert ZWaveGroup.called + assert len(ZWaveGroup.mock_calls) == 2 + assert ZWaveGroup.mock_calls[0][1][0] == 3 + assert ZWaveGroup.mock_calls[0][1][2] == 14 + assert group.add_association.called + assert len(group.add_association.mock_calls) == 1 + assert group.add_association.mock_calls[0][1][0] == 24 + assert group.add_association.mock_calls[0][1][1] == 5 + + def test_remove_association(self): + """Test zwave change_association service.""" + ZWaveGroup = self.mock_openzwave.group.ZWaveGroup + group = MagicMock() + ZWaveGroup.return_value = group + + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_WAKE_UP, + ) + node = MockNode(node_id=14) + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'change_association', { + const.ATTR_ASSOCIATION: 'remove', + const.ATTR_NODE_ID: 14, + const.ATTR_TARGET_NODE_ID: 24, + const.ATTR_GROUP: 3, + const.ATTR_INSTANCE: 5, + }) + self.hass.block_till_done() + + assert ZWaveGroup.called + assert len(ZWaveGroup.mock_calls) == 2 + assert ZWaveGroup.mock_calls[0][1][0] == 3 + assert ZWaveGroup.mock_calls[0][1][2] == 14 + assert group.remove_association.called + assert len(group.remove_association.mock_calls) == 1 + assert group.remove_association.mock_calls[0][1][0] == 24 + assert group.remove_association.mock_calls[0][1][1] == 5 + + def test_refresh_entity(self): + """Test zwave refresh_entity service.""" + node = MockNode() + value = MockValue(data=False, node=node, + command_class=const.COMMAND_CLASS_SENSOR_BINARY) + power_value = MockValue(data=50, node=node, + command_class=const.COMMAND_CLASS_METER) + values = MockEntityValues(primary=value, power=power_value) + device = get_device(node=node, values=values, node_config={}) + device.hass = self.hass + device.entity_id = 'binary_sensor.mock_entity_id' + self.hass.add_job(device.async_added_to_hass()) + self.hass.block_till_done() + + self.hass.services.call('zwave', 'refresh_entity', { + ATTR_ENTITY_ID: 'binary_sensor.mock_entity_id', + }) + self.hass.block_till_done() + + assert node.refresh_value.called + assert len(node.refresh_value.mock_calls) == 2 + self.assertEqual(sorted([node.refresh_value.mock_calls[0][1][0], + node.refresh_value.mock_calls[1][1][0]]), + sorted([value.value_id, power_value.value_id])) + + def test_refresh_node(self): + """Test zwave refresh_node service.""" + node = MockNode(node_id=14) + self.zwave_network.nodes = {14: node} + self.hass.services.call('zwave', 'refresh_node', { + const.ATTR_NODE_ID: 14, + }) + self.hass.block_till_done() + + assert node.refresh_info.called + assert len(node.refresh_info.mock_calls) == 1 diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index c171155404f..871520d1e6d 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,7 +1,7 @@ """Test Z-Wave node entity.""" import asyncio import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock import tests.mock.zwave as mock_zwave import pytest from homeassistant.components.zwave import node_entity @@ -36,11 +36,15 @@ class TestZWaveNodeEntity(unittest.TestCase): def setUp(self): """Initialize values for this testcase class.""" + self.zwave_network = MagicMock() self.node = mock_zwave.MockNode( query_stage='Dynamic', is_awake=True, is_ready=False, is_failed=False, is_info_received=True, max_baud_rate=40000, is_zwave_plus=False, capabilities=[], neighbors=[], location=None) - self.entity = node_entity.ZWaveNodeEntity(self.node) + self.node.manufacturer_name = 'Test Manufacturer' + self.node.product_name = 'Test Product' + self.entity = node_entity.ZWaveNodeEntity(self.node, + self.zwave_network) def test_network_node_changed_from_value(self): """Test for network_node_changed.""" @@ -76,16 +80,68 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_node_changed(self): """Test node_changed function.""" - self.assertEqual({'node_id': self.node.node_id}, - self.entity.device_state_attributes) + self.maxDiff = None + self.assertEqual( + {'node_id': self.node.node_id, + 'manufacturer_name': 'Test Manufacturer', + 'product_name': 'Test Product'}, + self.entity.device_state_attributes) self.node.get_values.return_value = { 1: mock_zwave.MockValue(data=1800) } + self.zwave_network.manager.getNodeStatistics.return_value = { + "receivedCnt": 4, "ccData": [{"receivedCnt": 0, + "commandClassId": 134, + "sentCnt": 0}, + {"receivedCnt": 1, + "commandClassId": 133, + "sentCnt": 1}, + {"receivedCnt": 1, + "commandClassId": 115, + "sentCnt": 1}, + {"receivedCnt": 0, + "commandClassId": 114, + "sentCnt": 0}, + {"receivedCnt": 0, + "commandClassId": 112, + "sentCnt": 0}, + {"receivedCnt": 1, + "commandClassId": 32, + "sentCnt": 1}, + {"receivedCnt": 0, + "commandClassId": 0, + "sentCnt": 0}], + "receivedUnsolicited": 0, + "sentTS": "2017-03-27 15:38:15:620 ", "averageRequestRTT": 2462, + "lastResponseRTT": 3679, "retries": 0, "sentFailed": 1, + "sentCnt": 7, "quality": 0, "lastRequestRTT": 1591, + "lastReceivedMessage": [0, 4, 0, 15, 3, 32, 3, 0, 221, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0], "receivedDups": 1, + "averageResponseRTT": 2443, + "receivedTS": "2017-03-27 15:38:19:298 "} self.entity.node_changed() - self.assertEqual( {'node_id': self.node.node_id, + 'manufacturer_name': 'Test Manufacturer', + 'product_name': 'Test Product', 'query_stage': 'Dynamic', 'is_awake': True, 'is_ready': False, @@ -94,7 +150,19 @@ class TestZWaveNodeEntity(unittest.TestCase): 'max_baud_rate': 40000, 'is_zwave_plus': False, 'battery_level': 42, - 'wake_up_interval': 1800}, + 'wake_up_interval': 1800, + 'averageRequestRTT': 2462, + 'averageResponseRTT': 2443, + 'lastRequestRTT': 1591, + 'lastResponseRTT': 3679, + 'receivedCnt': 4, + 'receivedDups': 1, + 'receivedTS': '2017-03-27 15:38:19:298 ', + 'receivedUnsolicited': 0, + 'retries': 0, + 'sentCnt': 7, + 'sentFailed': 1, + 'sentTS': '2017-03-27 15:38:15:620 '}, self.entity.device_state_attributes) self.node.can_wake_up_value = False diff --git a/tests/conftest.py b/tests/conftest.py index 56d4c793b8e..b6c9795f127 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,9 @@ from homeassistant import util, setup from homeassistant.util import location from homeassistant.components import mqtt -from .common import async_test_home_assistant, mock_coro -from .test_util.aiohttp import mock_aiohttp_client -from .mock.zwave import SIGNAL_VALUE_CHANGED, SIGNAL_NODE, SIGNAL_NOTIFICATION +from tests.common import async_test_home_assistant, mock_coro +from tests.test_util.aiohttp import mock_aiohttp_client +from tests.mock.zwave import MockNetwork if os.environ.get('UVLOOP') == '1': import uvloop @@ -100,9 +100,7 @@ def mock_openzwave(): base_mock = MagicMock() libopenzwave = base_mock.libopenzwave libopenzwave.__file__ = 'test' - base_mock.network.ZWaveNetwork.SIGNAL_VALUE_CHANGED = SIGNAL_VALUE_CHANGED - base_mock.network.ZWaveNetwork.SIGNAL_NODE = SIGNAL_NODE - base_mock.network.ZWaveNetwork.SIGNAL_NOTIFICATION = SIGNAL_NOTIFICATION + base_mock.network.ZWaveNetwork = MockNetwork with patch.dict('sys.modules', { 'libopenzwave': libopenzwave, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 38dc01b4387..a76b3a15068 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -50,6 +50,11 @@ class EntityTest(Entity): """Return the unique ID of the entity.""" return self._handle('unique_id') + @property + def available(self): + """Return True if entity is available.""" + return self._handle('available') + def _handle(self, attr): """Helper for the attributes.""" if attr in self._values: @@ -474,3 +479,29 @@ def test_platform_warn_slow_setup(hass): assert logger_method == _LOGGER.warning assert mock_call().cancel.called + + +@asyncio.coroutine +def test_extract_from_service_available_device(hass): + """Test the extraction of entity from service and device is available.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + EntityTest(name='test_1'), + EntityTest(name='test_2', available=False), + EntityTest(name='test_3'), + EntityTest(name='test_4', available=False), + ]) + + call_1 = ha.ServiceCall('test', 'service') + + assert ['test_domain.test_1', 'test_domain.test_3'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call_1)) + + call_2 = ha.ServiceCall('test', 'service', data={ + 'entity_id': ['test_domain.test_3', 'test_domain.test_4'], + }) + + assert ['test_domain.test_3'] == \ + sorted(ent.entity_id for ent in + component.async_extract_from_service(call_2)) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b6951172c64..71075124f32 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -314,6 +314,11 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('yes', tpl.render()) + tpl = template.Template(""" +{{ is_state("test.noobject", "available") }} + """, self.hass) + self.assertEqual('False', tpl.render()) + def test_is_state_attr(self): """Test is_state_attr method.""" self.hass.states.set('test.object', 'available', {'mode': 'on'}) @@ -322,6 +327,11 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('yes', tpl.render()) + tpl = template.Template(""" +{{ is_state_attr("test.noobject", "mode", "on") }} + """, self.hass) + self.assertEqual('False', tpl.render()) + def test_states_function(self): """Test using states as a function.""" self.hass.states.set('test.object', 'available') diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 0e20be6db4b..513c606aab2 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -3,15 +3,11 @@ from unittest.mock import MagicMock from pydispatch import dispatcher -SIGNAL_VALUE_CHANGED = 'mock_value_changed' -SIGNAL_NODE = 'mock_node' -SIGNAL_NOTIFICATION = 'mock_notification' - def value_changed(value): """Fire a value changed.""" dispatcher.send( - SIGNAL_VALUE_CHANGED, + MockNetwork.SIGNAL_VALUE_CHANGED, value=value, node=value.node, network=value.node._network @@ -21,7 +17,7 @@ def value_changed(value): def node_changed(node): """Fire a node changed.""" dispatcher.send( - SIGNAL_NODE, + MockNetwork.SIGNAL_NODE, node=node, network=node._network ) @@ -30,12 +26,70 @@ def node_changed(node): def notification(node_id, network=None): """Fire a notification.""" dispatcher.send( - SIGNAL_NOTIFICATION, + MockNetwork.SIGNAL_NOTIFICATION, args={'nodeId': node_id}, network=network ) +class MockNetwork(MagicMock): + """Mock Z-Wave network.""" + + SIGNAL_NETWORK_FAILED = 'mock_NetworkFailed' + SIGNAL_NETWORK_STARTED = 'mock_NetworkStarted' + SIGNAL_NETWORK_READY = 'mock_NetworkReady' + SIGNAL_NETWORK_STOPPED = 'mock_NetworkStopped' + SIGNAL_NETWORK_RESETTED = 'mock_DriverResetted' + SIGNAL_NETWORK_AWAKED = 'mock_DriverAwaked' + SIGNAL_DRIVER_FAILED = 'mock_DriverFailed' + SIGNAL_DRIVER_READY = 'mock_DriverReady' + SIGNAL_DRIVER_RESET = 'mock_DriverReset' + SIGNAL_DRIVER_REMOVED = 'mock_DriverRemoved' + SIGNAL_GROUP = 'mock_Group' + SIGNAL_NODE = 'mock_Node' + SIGNAL_NODE_ADDED = 'mock_NodeAdded' + SIGNAL_NODE_EVENT = 'mock_NodeEvent' + SIGNAL_NODE_NAMING = 'mock_NodeNaming' + SIGNAL_NODE_NEW = 'mock_NodeNew' + SIGNAL_NODE_PROTOCOL_INFO = 'mock_NodeProtocolInfo' + SIGNAL_NODE_READY = 'mock_NodeReady' + SIGNAL_NODE_REMOVED = 'mock_NodeRemoved' + SIGNAL_SCENE_EVENT = 'mock_SceneEvent' + SIGNAL_VALUE = 'mock_Value' + SIGNAL_VALUE_ADDED = 'mock_ValueAdded' + SIGNAL_VALUE_CHANGED = 'mock_ValueChanged' + SIGNAL_VALUE_REFRESHED = 'mock_ValueRefreshed' + SIGNAL_VALUE_REMOVED = 'mock_ValueRemoved' + SIGNAL_POLLING_ENABLED = 'mock_PollingEnabled' + SIGNAL_POLLING_DISABLED = 'mock_PollingDisabled' + SIGNAL_CREATE_BUTTON = 'mock_CreateButton' + SIGNAL_DELETE_BUTTON = 'mock_DeleteButton' + SIGNAL_BUTTON_ON = 'mock_ButtonOn' + SIGNAL_BUTTON_OFF = 'mock_ButtonOff' + SIGNAL_ESSENTIAL_NODE_QUERIES_COMPLETE = \ + 'mock_EssentialNodeQueriesComplete' + SIGNAL_NODE_QUERIES_COMPLETE = 'mock_NodeQueriesComplete' + SIGNAL_AWAKE_NODES_QUERIED = 'mock_AwakeNodesQueried' + SIGNAL_ALL_NODES_QUERIED = 'mock_AllNodesQueried' + SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = 'mock_AllNodesQueriedSomeDead' + SIGNAL_MSG_COMPLETE = 'mock_MsgComplete' + SIGNAL_NOTIFICATION = 'mock_Notification' + SIGNAL_CONTROLLER_COMMAND = 'mock_ControllerCommand' + SIGNAL_CONTROLLER_WAITING = 'mock_ControllerWaiting' + + STATE_STOPPED = 0 + STATE_FAILED = 1 + STATE_RESETTED = 3 + STATE_STARTED = 5 + STATE_AWAKED = 7 + STATE_READY = 10 + + def __init__(self, *args, **kwargs): + """Initialize a Z-Wave mock network.""" + super().__init__() + self.state = MockNetwork.STATE_STOPPED + + class MockNode(MagicMock): """Mock Z-Wave node.""" @@ -47,6 +101,7 @@ class MockNode(MagicMock): product_type='678', command_classes=None, can_wake_up_value=True, + network=None, **kwargs): """Initialize a Z-Wave mock node.""" super().__init__() @@ -57,6 +112,8 @@ class MockNode(MagicMock): self.product_type = product_type self.can_wake_up_value = can_wake_up_value self._command_classes = command_classes or [] + if network is not None: + self._network = network for attr_name in kwargs: setattr(self, attr_name, kwargs[attr_name]) @@ -84,30 +141,23 @@ class MockValue(MagicMock): def __init__(self, *, label='Mock Value', - data=None, - data_items=None, node=None, instance=0, index=0, - command_class=None, - units=None, - type=None, - value_id=None): + value_id=None, + **kwargs): """Initialize a Z-Wave mock value.""" super().__init__() self.label = label - self.data = data - self.data_items = data_items self.node = node self.instance = instance self.index = index - self.command_class = command_class - self.units = units - self.type = type if value_id is None: MockValue._mock_value_id += 1 value_id = MockValue._mock_value_id self.value_id = value_id + for attr_name in kwargs: + setattr(self, attr_name, kwargs[attr_name]) def _get_child_mock(self, **kw): """Create child mocks with right MagicMock class.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index eec7b4cf98d..41011794914 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,9 +1,6 @@ """Test Home Assistant remote methods and classes.""" # pylint: disable=protected-access -import asyncio -import threading import unittest -from unittest.mock import patch from homeassistant import remote, setup, core as ha import homeassistant.components.http as http @@ -11,18 +8,17 @@ from homeassistant.const import HTTP_HEADER_HA_AUTH, EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util from tests.common import ( - get_test_instance_port, get_test_home_assistant, get_test_config_dir) + get_test_instance_port, get_test_home_assistant) API_PASSWORD = 'test1234' MASTER_PORT = get_test_instance_port() -SLAVE_PORT = get_test_instance_port() BROKEN_PORT = get_test_instance_port() HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(MASTER_PORT) HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} broken_api = remote.API('127.0.0.1', "bladybla", port=get_test_instance_port()) -hass, slave, master_api = None, None, None +hass, master_api = None, None def _url(path=''): @@ -32,8 +28,8 @@ def _url(path=''): # pylint: disable=invalid-name def setUpModule(): - """Initalization of a Home Assistant server and Slave instance.""" - global hass, slave, master_api + """Initalization of a Home Assistant server instance.""" + global hass, master_api hass = get_test_home_assistant() @@ -51,30 +47,10 @@ def setUpModule(): master_api = remote.API('127.0.0.1', API_PASSWORD, MASTER_PORT) - # Start slave - loop = asyncio.new_event_loop() - - # FIXME: should not be a daemon - threading.Thread(name='SlaveThread', daemon=True, - target=loop.run_forever).start() - - slave = remote.HomeAssistant(master_api, loop=loop) - slave.async_track_tasks() - slave.config.config_dir = get_test_config_dir() - slave.config.skip_pip = True - setup.setup_component( - slave, http.DOMAIN, - {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: SLAVE_PORT}}) - - with patch.object(ha, '_async_create_timer', return_value=None): - slave.start() - # pylint: disable=invalid-name def tearDownModule(): - """Stop the Home Assistant server and slave.""" - slave.stop() + """Stop the Home Assistant server.""" hass.stop() @@ -83,7 +59,6 @@ class TestRemoteMethods(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" - slave.block_till_done() hass.block_till_done() def test_validate_api(self): @@ -228,89 +203,3 @@ class TestRemoteMethods(unittest.TestCase): now = dt_util.utcnow() self.assertEqual(now.isoformat(), ha_json_enc.default(now)) - - -class TestRemoteClasses(unittest.TestCase): - """Test the homeassistant.remote module.""" - - def tearDown(self): - """Stop everything that was started.""" - slave.block_till_done() - hass.block_till_done() - - def test_home_assistant_init(self): - """Test HomeAssistant init.""" - # Wrong password - self.assertRaises( - ha.HomeAssistantError, remote.HomeAssistant, - remote.API('127.0.0.1', API_PASSWORD + 'A', 8124)) - - # Wrong port - self.assertRaises( - ha.HomeAssistantError, remote.HomeAssistant, - remote.API('127.0.0.1', API_PASSWORD, BROKEN_PORT)) - - def test_statemachine_init(self): - """Test if remote.StateMachine copies all states on init.""" - self.assertEqual(sorted(hass.states.all()), - sorted(slave.states.all())) - - def test_statemachine_set(self): - """Test if setting the state on a slave is recorded.""" - slave.states.set("remote.test", "remote.statemachine test") - - # Wait till slave tells master - slave.block_till_done() - # Wait till master gives updated state - hass.block_till_done() - - self.assertEqual("remote.statemachine test", - slave.states.get("remote.test").state) - - def test_statemachine_remove_from_master(self): - """Remove statemachine from master.""" - hass.states.set("remote.master_remove", "remove me!") - hass.block_till_done() - slave.block_till_done() - - self.assertIn('remote.master_remove', slave.states.entity_ids()) - - hass.states.remove("remote.master_remove") - hass.block_till_done() - slave.block_till_done() - - self.assertNotIn('remote.master_remove', slave.states.entity_ids()) - - def test_statemachine_remove_from_slave(self): - """Remove statemachine from slave.""" - hass.states.set("remote.slave_remove", "remove me!") - hass.block_till_done() - - self.assertIn('remote.slave_remove', slave.states.entity_ids()) - - self.assertTrue(slave.states.remove("remote.slave_remove")) - slave.block_till_done() - hass.block_till_done() - - self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) - - def test_eventbus_fire(self): - """Test if events fired from the eventbus get fired.""" - hass_call = [] - slave_call = [] - - hass.bus.listen("test.event_no_data", lambda _: hass_call.append(1)) - slave.bus.listen("test.event_no_data", lambda _: slave_call.append(1)) - slave.bus.fire("test.event_no_data") - - # Wait till slave tells master - slave.block_till_done() - # Wait till master gives updated event - hass.block_till_done() - - self.assertEqual(1, len(hass_call)) - self.assertEqual(1, len(slave_call)) - - def test_get_config(self): - """Test the return of the configuration.""" - self.assertEqual(hass.config.as_dict(), remote.get_config(master_api)) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index d7560d4f7bf..bf2f4e5832f 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -173,6 +173,12 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((127, 127, 127), color_util.color_rgbw_to_rgb(0, 0, 0, 127)) + def test_color_rgb_to_hex(self): + """Test color_rgb_to_hex.""" + assert color_util.color_rgb_to_hex(255, 255, 255) == 'ffffff' + assert color_util.color_rgb_to_hex(0, 0, 0) == '000000' + assert color_util.color_rgb_to_hex(51, 153, 255) == '3399ff' + class ColorTemperatureMiredToKelvinTests(unittest.TestCase): """Test color_temperature_mired_to_kelvin.""" diff --git a/tests/util/test_init.py b/tests/util/test_init.py index d6d583342d7..ba8415d597f 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -31,6 +31,13 @@ class TestUtil(unittest.TestCase): self.assertEqual("test_more", util.slugify("Test More")) self.assertEqual("test_more", util.slugify("Test_(More)")) self.assertEqual("test_more", util.slugify("Tèst_Mörê")) + self.assertEqual("b827eb000000", util.slugify("B8:27:EB:00:00:00")) + self.assertEqual("testcom", util.slugify("test.com")) + self.assertEqual("greg_phone__exp_wayp1", + util.slugify("greg_phone - exp_wayp1")) + self.assertEqual("we_are_we_are_a_test_calendar", + util.slugify("We are, we are, a... Test Calendar")) + self.assertEqual("test_aouss_aou", util.slugify("Tèst_äöüß_ÄÖÜ")) def test_repr_helper(self): """Test repr_helper.""" diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 62c9f9f6596..5d16e9400ef 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -12,6 +12,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no +#ENV INSTALL_COAP_CLIENT no VOLUME /config @@ -25,7 +26,7 @@ RUN virtualization/Docker/setup_docker_prereqs # Install hass component dependencies COPY requirements_all.txt requirements_all.txt RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet # BEGIN: Development additions diff --git a/virtualization/Docker/scripts/coap_client b/virtualization/Docker/scripts/coap_client new file mode 100755 index 00000000000..fa16d14c0cb --- /dev/null +++ b/virtualization/Docker/scripts/coap_client @@ -0,0 +1,14 @@ +#!/bin/sh +# Installs a modified coap client with support for dtls for use with IKEA Tradfri + +# Stop on errors +set -e + +apt-get install -y --no-install-recommends git autoconf automake libtool + +git clone --depth 1 --recursive -b dtls https://github.com/home-assistant/libcoap.git +cd libcoap +./autogen.sh +./configure --disable-documentation --disable-shared --without-debug CFLAGS="-D COAP_DEBUG_FD=stderr" +make +make install diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr index ffecc864914..b8fe8d44338 100755 --- a/virtualization/Docker/scripts/openalpr +++ b/virtualization/Docker/scripts/openalpr @@ -12,7 +12,7 @@ PACKAGES=( apt-get install -y --no-install-recommends ${PACKAGES[@]} # Clone the latest code from GitHub -git clone https://github.com/openalpr/openalpr.git /usr/local/src/openalpr +git clone --depth 1 https://github.com/openalpr/openalpr.git /usr/local/src/openalpr # Setup the build directory cd /usr/local/src/openalpr/src diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index f2238e43876..69f76e927e2 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -10,6 +10,7 @@ INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_OPENZWAVE="${INSTALL_OPENZWAVE:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" +INSTALL_COAP_CLIENT="${INSTALL_COAP_CLIENT:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -62,6 +63,10 @@ if [ "$INSTALL_PHANTOMJS" == "yes" ]; then virtualization/Docker/scripts/phantomjs fi +if [ "$INSTALL_COAP_CLIENT" == "yes" ]; then + virtualization/Docker/scripts/coap_client +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove