diff --git a/.coveragerc b/.coveragerc index 3163b5f723c..c93fecc9c2e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -65,6 +65,9 @@ omit = homeassistant/components/isy994.py homeassistant/components/*/isy994.py + homeassistant/components/juicenet.py + homeassistant/components/*/juicenet.py + homeassistant/components/kira.py homeassistant/components/*/kira.py @@ -74,6 +77,9 @@ omit = homeassistant/components/lutron_caseta.py homeassistant/components/*/lutron_caseta.py + homeassistant/components/mailgun.py + homeassistant/components/*/mailgun.py + homeassistant/components/modbus.py homeassistant/components/*/modbus.py @@ -197,6 +203,7 @@ omit = homeassistant/components/binary_sensor/pilight.py homeassistant/components/binary_sensor/ping.py homeassistant/components/binary_sensor/rest.py + homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/browser.py homeassistant/components/camera/amcrest.py homeassistant/components/camera/bloomsky.py @@ -204,8 +211,10 @@ omit = homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/rpi_camera.py + homeassistant/components/camera/onvif.py homeassistant/components/camera/synology.py homeassistant/components/climate/eq3btsmart.py + homeassistant/components/climate/flexit.py homeassistant/components/climate/heatmiser.py homeassistant/components/climate/homematic.py homeassistant/components/climate/knx.py @@ -286,6 +295,7 @@ omit = homeassistant/components/lirc.py homeassistant/components/lock/nuki.py homeassistant/components/lock/lockitron.py + homeassistant/components/lock/sesame.py homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/apple_tv.py homeassistant/components/media_player/aquostv.py @@ -310,6 +320,7 @@ omit = homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/nad.py + homeassistant/components/media_player/nadtcp.py homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/openhome.py homeassistant/components/media_player/panasonic_viera.py @@ -341,7 +352,6 @@ omit = homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py - homeassistant/components/notify/mailgun.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/nfandroidtv.py @@ -370,8 +380,10 @@ omit = homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bitcoin.py + homeassistant/components/sensor/blockchain.py homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py + homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cert_expiry.py @@ -391,6 +403,7 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/emoncms.py homeassistant/components/sensor/envirophat.py + homeassistant/components/sensor/etherscan.py homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py homeassistant/components/sensor/fido.py @@ -398,6 +411,7 @@ omit = homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py + homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/gpsd.py @@ -435,6 +449,8 @@ omit = homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/qnap.py + homeassistant/components/sensor/radarr.py + homeassistant/components/sensor/ripple.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sensehat.py @@ -464,6 +480,7 @@ omit = homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/sensor/zamg.py + homeassistant/components/spc.py homeassistant/components/switch/acer_projector.py homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/arest.py @@ -492,8 +509,10 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/upnp.py homeassistant/components/weather/bom.py + homeassistant/components/weather/buienradar.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py + homeassistant/components/weather/yweather.py homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py homeassistant/components/zwave/util.py diff --git a/Dockerfile b/Dockerfile index 8c4cd0f5440..6dae36bb24b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP_CLIENT no +#ENV INSTALL_SSOCR no VOLUME /config diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 219d413db12..75aaeaa1fd1 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -31,50 +31,8 @@ def attempt_use_uvloop(): pass -def monkey_patch_asyncio(): - """Replace weakref.WeakSet to address Python 3 bug. - - Under heavy threading operations that schedule calls into - the asyncio event loop, Task objects are created. Due to - a bug in Python, GC may have an issue when switching between - the threads and objects with __del__ (which various components - in HASS have). - - This monkey-patch removes the weakref.Weakset, and replaces it - with an object that ignores the only call utilizing it (the - Task.__init__ which calls _all_tasks.add(self)). It also removes - the __del__ which could trigger the future objects __del__ at - unpredictable times. - - The side-effect of this manipulation of the Task is that - Task.all_tasks() is no longer accurate, and there will be no - warning emitted if a Task is GC'd while in use. - - On Python 3.6, after the bug is fixed, this monkey-patch can be - disabled. - - See https://bugs.python.org/issue26617 for details of the Python - bug. - """ - # pylint: disable=no-self-use, protected-access, bare-except - import asyncio.tasks - - class IgnoreCalls: - """Ignore add calls.""" - - def add(self, other): - """No-op add.""" - return - - asyncio.tasks.Task._all_tasks = IgnoreCalls() - try: - del asyncio.tasks.Task.__del__ - except: - pass - - def validate_python() -> None: - """Validate we're running the right Python version.""" + """Validate that the right Python version is running.""" if sys.platform == "win32" and \ sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN: print("Home Assistant requires at least Python {}.{}.{}".format( @@ -215,7 +173,7 @@ def daemonize() -> None: def check_pid(pid_file: str) -> None: - """Check that HA is not already running.""" + """Check that Home Assistant is not already running.""" # Check pid file try: pid = int(open(pid_file, 'r').readline()) @@ -329,7 +287,7 @@ def setup_and_run_hass(config_dir: str, def try_to_restart() -> None: - """Attempt to clean up state and start a new homeassistant instance.""" + """Attempt to clean up state and start a new Home Assistant instance.""" # Things should be mostly shut down already at this point, now just try # to clean up things that may have been left behind. sys.stderr.write('Home Assistant attempting to restart.\n') @@ -361,11 +319,11 @@ def try_to_restart() -> None: else: os.closerange(3, max_fd) - # Now launch into a new instance of Home-Assistant. If this fails we + # Now launch into a new instance of Home Assistant. If this fails we # fall through and exit with error 100 (RESTART_EXIT_CODE) in which case # systemd will restart us when RestartForceExitStatus=100 is set in the # systemd.service file. - sys.stderr.write("Restarting Home-Assistant\n") + sys.stderr.write("Restarting Home Assistant\n") args = cmdline() os.execv(args[0], args) @@ -374,18 +332,13 @@ def main() -> int: """Start Home Assistant.""" validate_python() - if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1': - if sys.version_info[:3] >= (3, 6): + if os.environ.get('HASS_NO_MONKEY') != '1': + if sys.version_info[:2] >= (3, 6): monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() - elif sys.version_info[:3] < (3, 5, 3): - monkey_patch.patch_weakref_tasks() attempt_use_uvloop() - if sys.version_info[:3] < (3, 5, 3): - monkey_patch_asyncio() - args = get_arguments() if args.script is not None: diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py new file mode 100644 index 00000000000..de4d5098b41 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -0,0 +1,96 @@ +""" +Support for Vanderbilt (formerly Siemens) SPC alarm systems. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.spc/ +""" +import asyncio +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.spc import ( + SpcWebGateway, ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN) + + +_LOGGER = logging.getLogger(__name__) + +SPC_AREA_MODE_TO_STATE = {'0': STATE_ALARM_DISARMED, + '1': STATE_ALARM_ARMED_HOME, + '3': STATE_ALARM_ARMED_AWAY} + + +def _get_alarm_state(spc_mode): + return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the SPC alarm control panel platform.""" + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_AREAS] is None): + return + + entities = [SpcAlarm(hass=hass, + area_id=area['id'], + name=area['name'], + state=_get_alarm_state(area['mode'])) + for area in discovery_info[ATTR_DISCOVER_AREAS]] + + async_add_entities(entities) + + +class SpcAlarm(alarm.AlarmControlPanel): + """Represents the SPC alarm panel.""" + + def __init__(self, hass, area_id, name, state): + """Initialize the SPC alarm panel.""" + self._hass = hass + self._area_id = area_id + self._name = name + self._state = state + self._api = hass.data[DATA_API] + + hass.data[DATA_REGISTRY].register_alarm_device(area_id, self) + + @asyncio.coroutine + def async_update_from_spc(self, state): + """Update the alarm panel with a new state.""" + self._state = state + yield from self.async_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @asyncio.coroutine + def async_alarm_disarm(self, code=None): + """Send disarm command.""" + yield from self._api.send_area_command( + self._area_id, SpcWebGateway.AREA_COMMAND_UNSET) + + @asyncio.coroutine + def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + yield from self._api.send_area_command( + self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET) + + @asyncio.coroutine + def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + yield from self._api.send_area_command( + self._area_id, SpcWebGateway.AREA_COMMAND_SET) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a99113b6f6f..09f0e286755 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.frontend import register_built_in_panel DOMAIN = 'automation' +DEPENDENCIES = ['group'] ENTITY_ID_FORMAT = DOMAIN + '.{}' GROUP_NAME_ALL_AUTOMATIONS = 'all automations' diff --git a/homeassistant/components/binary_sensor/enocean.py b/homeassistant/components/binary_sensor/enocean.py index 358abb434fd..36574450e4d 100644 --- a/homeassistant/components/binary_sensor/enocean.py +++ b/homeassistant/components/binary_sensor/enocean.py @@ -80,6 +80,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): elif value2 == 0x10: self.which = 1 self.onoff = 1 + elif value2 == 0x37: + self.which = 10 + self.onoff = 0 + elif value2 == 0x15: + self.which = 10 + self.onoff = 1 self.hass.bus.fire('button_pressed', {'id': self.dev_id, 'pushed': value, 'which': self.which, diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 17d08265a5b..29ad5847b32 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -1,5 +1,5 @@ """ -Support for Homematic binary sensors. +Support for HomeMatic binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.homematic/ @@ -29,7 +29,7 @@ SENSOR_TYPES_CLASS = { def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Homematic binary sensor platform.""" + """Set up the HomeMatic binary sensor platform.""" if discovery_info is None: return @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HMBinarySensor(HMDevice, BinarySensorDevice): - """Representation of a binary Homematic device.""" + """Representation of a binary HomeMatic device.""" @property def is_on(self): @@ -54,16 +54,14 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): @property def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - # If state is MOTION (RemoteMotion works only) + """Return the class of this sensor from DEVICE_CLASSES.""" + # If state is MOTION (Only RemoteMotion working) if self._state == 'MOTION': return 'motion' return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) def _init_data_struct(self): - """Generate a data struct (self._data) from the Homematic metadata.""" - # add state to data struct + """Generate the data dictionary (self._data) from metadata.""" + # Add state to data struct if self._state: - _LOGGER.debug("%s init datastruct with main node '%s'", self._name, - self._state) self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py new file mode 100644 index 00000000000..8023e1cf4b3 --- /dev/null +++ b/homeassistant/components/binary_sensor/spc.py @@ -0,0 +1,99 @@ +""" +Support for Vanderbilt (formerly Siemens) SPC alarm systems. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.spc/ +""" +import logging +import asyncio + +from homeassistant.components.spc import ( + ATTR_DISCOVER_DEVICES, DATA_REGISTRY) +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import (STATE_UNAVAILABLE, STATE_ON, STATE_OFF) + + +_LOGGER = logging.getLogger(__name__) + +SPC_TYPE_TO_DEVICE_CLASS = {'0': 'motion', + '1': 'opening', + '3': 'smoke'} + + +SPC_INPUT_TO_SENSOR_STATE = {'0': STATE_OFF, + '1': STATE_ON} + + +def _get_device_class(spc_type): + return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None) + + +def _get_sensor_state(spc_input): + return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE) + + +def _create_sensor(hass, zone): + return SpcBinarySensor(zone_id=zone['id'], + name=zone['zone_name'], + state=_get_sensor_state(zone['input']), + device_class=_get_device_class(zone['type']), + spc_registry=hass.data[DATA_REGISTRY]) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Initialize the platform.""" + if (discovery_info is None or + discovery_info[ATTR_DISCOVER_DEVICES] is None): + return + + async_add_entities( + _create_sensor(hass, zone) + for zone in discovery_info[ATTR_DISCOVER_DEVICES] + if _get_device_class(zone['type'])) + + +class SpcBinarySensor(BinarySensorDevice): + """Represents a sensor based on an SPC zone.""" + + def __init__(self, zone_id, name, state, device_class, spc_registry): + """Initialize the sensor device.""" + self._zone_id = zone_id + self._name = name + self._state = state + self._device_class = device_class + + spc_registry.register_sensor_device(zone_id, self) + + @asyncio.coroutine + def async_update_from_spc(self, state): + """Update the state of the device.""" + self._state = state + yield from self.async_update_ha_state() + + @property + def name(self): + """The name of the device.""" + return self._name + + @property + def is_on(self): + """Whether the device is switched on.""" + return self._state == STATE_ON + + @property + def hidden(self) -> bool: + """Whether the device is hidden by default.""" + # these type of sensors are probably mainly used for automations + return True + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """The device class.""" + return self._device_class diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py new file mode 100644 index 00000000000..565abb73b36 --- /dev/null +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -0,0 +1,86 @@ +""" +Support for Taps Affs. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.tapsaff/ +""" +import logging +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) + +REQUIREMENTS = ['tapsaff==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +CONF_LOCATION = 'location' + +DEFAULT_NAME = 'Taps Aff' + +SCAN_INTERVAL = timedelta(minutes=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_LOCATION): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Taps Aff binary sensor.""" + name = config.get(CONF_NAME) + location = config.get(CONF_LOCATION) + + taps_aff_data = TapsAffData(location) + + add_devices([TapsAffSensor(taps_aff_data, name)], True) + + +class TapsAffSensor(BinarySensorDevice): + """Implementation of a Taps Aff binary sensor.""" + + def __init__(self, taps_aff_data, name): + """Initialize the Taps Aff sensor.""" + self.data = taps_aff_data + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return '{}'.format(self._name) + + @property + def is_on(self): + """Return true if taps aff.""" + return self.data.is_taps_aff + + def update(self): + """Get the latest data.""" + self.data.update() + + +class TapsAffData(object): + """Class for handling the data retrieval for pins.""" + + def __init__(self, location): + """Initialize the sensor.""" + from tapsaff import TapsAff + + self._is_taps_aff = None + self.taps_aff = TapsAff(location) + + @property + def is_taps_aff(self): + """Return true if taps aff.""" + return self._is_taps_aff + + def update(self): + """Get the latest data from the Taps Aff API and updates the states.""" + try: + self._is_taps_aff = self.taps_aff.is_taps_aff + except RuntimeError: + _LOGGER.error("Update failed. Check configured location") diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 362202d1bde..67d2e7179ba 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -39,7 +39,6 @@ def setup_platform(hass, config, add_devices, disc_info=None): for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]]) -# pylint: disable=too-many-instance-attributes class GoogleCalendarEventDevice(CalendarEventDevice): """A calendar event device.""" diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py new file mode 100644 index 00000000000..f1c94f79c0b --- /dev/null +++ b/homeassistant/components/camera/onvif.py @@ -0,0 +1,102 @@ +""" +Support for ONVIF Cameras with FFmpeg as decoder. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.onvif/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.ffmpeg import ( + DATA_FFMPEG) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_stream) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['onvif-py3==0.1.3', + 'suds-py3==1.3.3.0', + 'http://github.com/tgaugry/suds-passworddigest-py3' + '/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip' + '#suds-passworddigest-py3==0.1.2a'] +DEPENDENCIES = ['ffmpeg'] +DEFAULT_NAME = 'ONVIF Camera' +DEFAULT_PORT = 5000 +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = '888888' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up a ONVIF camera.""" + if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)): + return + async_add_devices([ONVIFCamera(hass, config)]) + + +class ONVIFCamera(Camera): + """An implementation of an ONVIF camera.""" + + def __init__(self, hass, config): + """Initialize a ONVIF camera.""" + from onvif import ONVIFService + super().__init__() + + self._name = config.get(CONF_NAME) + self._ffmpeg_arguments = '-q:v 2' + media = ONVIFService( + 'http://{}:{}/onvif/device_service'.format( + config.get(CONF_HOST), config.get(CONF_PORT)), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + '{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir) + ) + self._input = media.GetStreamUri().Uri + _LOGGER.debug("ONVIF Camera Using the following URL for %s: %s", + self._name, self._input) + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + from haffmpeg import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame( + self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) + + image = yield from ffmpeg.get_image( + self._input, output_format=IMAGE_JPEG, + extra_cmd=self._ffmpeg_arguments) + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + from haffmpeg import CameraMjpeg + + stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, + loop=self.hass.loop) + yield from stream.open_camera( + self._input, extra_cmd=self._ffmpeg_arguments) + + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py new file mode 100644 index 00000000000..5911486c761 --- /dev/null +++ b/homeassistant/components/climate/flexit.py @@ -0,0 +1,148 @@ +""" +Platform for Flexit AC units with CI66 Modbus adapter. + +Example configuration: + +climate: + - platform: flexit + name: Main AC + slave: 21 + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.flexit/ +""" +import logging +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, + ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) +import homeassistant.components.modbus as modbus +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyflexit==0.3'] +DEPENDENCIES = ['modbus'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)), + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Flexit Platform.""" + modbus_slave = config.get(CONF_SLAVE, None) + name = config.get(CONF_NAME, None) + add_devices([Flexit(modbus_slave, name)], True) + + +class Flexit(ClimateDevice): + """Representation of a Flexit AC unit.""" + + def __init__(self, modbus_slave, name): + """Initialize the unit.""" + from pyflexit import pyflexit + self._name = name + self._slave = modbus_slave + self._target_temperature = None + self._current_temperature = None + self._current_fan_mode = None + self._current_operation = None + self._fan_list = ['Off', 'Low', 'Medium', 'High'] + self._current_operation = None + self._filter_hours = None + self._filter_alarm = None + self._heat_recovery = None + self._heater_enabled = False + self._heating = None + self._cooling = None + self._alarm = False + self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave) + + def update(self): + """Update unit attributes.""" + if not self.unit.update(): + _LOGGER.warning("Modbus read failed") + + self._target_temperature = self.unit.get_target_temp + self._current_temperature = self.unit.get_temp + self._current_fan_mode =\ + self._fan_list[self.unit.get_fan_speed] + self._filter_hours = self.unit.get_filter_hours + # Mechanical heat recovery, 0-100% + self._heat_recovery = self.unit.get_heat_recovery + # Heater active 0-100% + self._heating = self.unit.get_heating + # Cooling active 0-100% + self._cooling = self.unit.get_cooling + # Filter alarm 0/1 + self._filter_alarm = self.unit.get_filter_alarm + # Heater enabled or not. Does not mean it's necessarily heating + self._heater_enabled = self.unit.get_heater_enabled + # Current operation mode + self._current_operation = self.unit.get_operation + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + 'filter_hours': self._filter_hours, + 'filter_alarm': self._filter_alarm, + 'heat_recovery': self._heat_recovery, + 'heating': self._heating, + 'heater_enabled': self._heater_enabled, + 'cooling': self._cooling + } + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._current_fan_mode + + @property + def fan_list(self): + """Return the list of available fan modes.""" + return self._fan_list + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + self._target_temperature = kwargs.get(ATTR_TEMPERATURE) + self.unit.set_temp(self._target_temperature) + + def set_fan_mode(self, fan): + """Set new fan mode.""" + self.unit.set_fan_speed(fan) diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 1be7480a727..e3439bdfc74 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkAC(climate, hass, temp_unit)]) -# pylint: disable=abstract-method,too-many-public-methods, too-many-branches +# pylint: disable=abstract-method class WinkThermostat(WinkDevice, ClimateDevice): """Representation of a Wink thermostat.""" diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 9f6360361f1..591190383a0 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -14,11 +14,13 @@ from homeassistant import core from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import script + REQUIREMENTS = ['fuzzywuzzy==0.15.0'] ATTR_TEXT = 'text' - +ATTR_SENTENCE = 'sentence' DOMAIN = 'conversation' REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') @@ -29,9 +31,12 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({ vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower), }) -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({}), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({ + cv.string: vol.Schema({ + vol.Required(ATTR_SENTENCE): cv.string, + vol.Required('action'): cv.SCRIPT_SCHEMA, + }) +})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): @@ -40,9 +45,30 @@ def setup(hass, config): from fuzzywuzzy import process as fuzzyExtract logger = logging.getLogger(__name__) + config = config.get(DOMAIN, {}) + + choices = {attrs[ATTR_SENTENCE]: script.Script( + hass, + attrs['action'], + name) + for name, attrs in config.items()} def process(service): """Parse text into commands.""" + # if actually configured + if choices: + text = service.data[ATTR_TEXT] + match = fuzzyExtract.extractOne(text, choices.keys()) + scorelimit = 60 # arbitrary value + logging.info( + 'matched up text %s and found %s', + text, + [match[0] if match[1] > scorelimit else 'nothing'] + ) + if match[1] > scorelimit: + choices[match[0]].run() # run respective script + return + text = service.data[ATTR_TEXT] match = REGEX_TURN_COMMAND.match(text) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index f913d126c4a..d4e7d4b0db6 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) DOMAIN = 'cover' +DEPENDENCIES = ['group'] SCAN_INTERVAL = timedelta(seconds=15) GROUP_NAME_ALL_COVERS = 'all covers' diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index ace08f53e3c..8fb003c6649 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -1,5 +1,5 @@ """ -The homematic cover platform. +The HomeMatic cover platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.homematic/ @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HMCover(HMDevice, CoverDevice): - """Representation a Homematic Cover.""" + """Representation a HomeMatic Cover.""" @property def current_cover_position(self): @@ -70,7 +70,6 @@ class HMCover(HMDevice, CoverDevice): self._hmdevice.stop(self._channel) def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata.""" - # Add state to data dict + """Generate a data dictoinary (self._data) from metadata.""" self._state = "LEVEL" self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 5c79540a249..4c862f8c8b8 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -14,9 +14,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.loader as loader -REQUIREMENTS = [ - 'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip' - '#pymyq==0.0.8'] +REQUIREMENTS = ['pymyq==0.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index acf402a0c8a..017bb723ee5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv +from homeassistant.loader import get_component import homeassistant.util as util from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -41,7 +42,7 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) DOMAIN = 'device_tracker' -DEPENDENCIES = ['zone'] +DEPENDENCIES = ['zone', 'group'] GROUP_NAME_ALL_DEVICES = 'all devices' ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') @@ -122,15 +123,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" yaml_path = hass.config.path(YAML_DEVICES) - try: - conf = config.get(DOMAIN, []) - except vol.Invalid as ex: - async_log_exception(ex, DOMAIN, config, hass) - return False - else: - conf = conf[0] if conf else {} - consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) - track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + conf = config.get(DOMAIN, []) + conf = conf[0] if conf else {} + consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) + track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = yield from async_load_config(yaml_path, hass, consider_home) tracker = DeviceTracker(hass, consider_home, track_new, devices) @@ -180,7 +176,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): if setup_tasks: yield from asyncio.wait(setup_tasks, loop=hass.loop) - yield from tracker.async_setup_group() + tracker.async_setup_group() @callback def async_device_tracker_discovered(service, info): @@ -233,7 +229,7 @@ class DeviceTracker(object): self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.consider_home = consider_home self.track_new = track_new - self.group = None # type: group.Group + self.group = None self._is_updating = asyncio.Lock(loop=hass.loop) for dev in devices: @@ -246,18 +242,21 @@ class DeviceTracker(object): def see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, battery: str=None, attributes: dict=None, - source_type: str=SOURCE_TYPE_GPS): + source_type: str=SOURCE_TYPE_GPS, picture: str=None, + icon: str=None): """Notify the device tracker that you see a device.""" self.hass.add_job( self.async_see(mac, dev_id, host_name, location_name, gps, - gps_accuracy, battery, attributes, source_type) + gps_accuracy, battery, attributes, source_type, + picture, icon) ) @asyncio.coroutine def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, battery: str=None, attributes: dict=None, - source_type: str=SOURCE_TYPE_GPS): + source_type: str=SOURCE_TYPE_GPS, picture: str=None, + icon: str=None): """Notify the device tracker that you see a device. This method is a coroutine. @@ -285,7 +284,8 @@ class DeviceTracker(object): dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) device = Device( self.hass, self.consider_home, self.track_new, - dev_id, mac, (host_name or dev_id).replace('_', ' ')) + dev_id, mac, (host_name or dev_id).replace('_', ' '), + picture=picture, icon=icon) self.devices[dev_id] = device if mac is not None: self.mac_to_dev[mac] = device @@ -303,9 +303,10 @@ class DeviceTracker(object): }) # During init, we ignore the group - if self.group is not None: - yield from self.group.async_update_tracked_entity_ids( - list(self.group.tracking) + [device.entity_id]) + if self.group and self.track_new: + self.group.async_set_group( + self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) # lookup mac vendor string to be stored in config yield from device.set_vendor_for_mac() @@ -327,16 +328,19 @@ class DeviceTracker(object): update_config, self.hass.config.path(YAML_DEVICES), dev_id, device) - @asyncio.coroutine + @callback def async_setup_group(self): """Initialize group for all tracked devices. - This method is a coroutine. + This method must be run in the event loop. """ - entity_ids = (dev.entity_id for dev in self.devices.values() - if dev.track) - self.group = yield from group.Group.async_create_group( - self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) + entity_ids = [dev.entity_id for dev in self.devices.values() + if dev.track] + + self.group = get_component('group') + self.group.async_set_group( + self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) @callback def async_update_stale(self, now: dt_util.dt.datetime): diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 40ab48b384a..2f41d8fe0d3 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -116,7 +116,6 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): "key for topic %s", topic) return None - # pylint: disable=too-many-return-statements def validate_payload(topic, payload, data_type): """Validate the OwnTracks payload.""" try: diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 1f21a25359c..36f1ea06fd6 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -57,7 +57,7 @@ class Host(object): def update(self, see): """Update device state by sending one or more ping messages.""" failed = 0 - while failed < self._count: # check more times if host in unreachable + while failed < self._count: # check more times if host is unreachable if self.ping(): see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER) return True diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index b0a2015362e..a3be40036cb 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pysnmp==4.3.7'] +REQUIREMENTS = ['pysnmp==4.3.8'] CONF_COMMUNITY = 'community' CONF_AUTHKEY = 'authkey' diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index b0409e99883..29c997b4dac 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.unifi/ """ import logging -import urllib import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -15,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_VERIFY_SSL -REQUIREMENTS = ['pyunifi==2.12'] +REQUIREMENTS = ['pyunifi==2.13'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' @@ -40,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_scanner(hass, config): """Set up the Unifi device_tracker.""" - from pyunifi.controller import Controller + from pyunifi.controller import Controller, APIError host = config[DOMAIN].get(CONF_HOST) username = config[DOMAIN].get(CONF_USERNAME) @@ -53,7 +52,7 @@ def get_scanner(hass, config): try: ctrl = Controller(host, username, password, port, version='v4', site_id=site_id, ssl_verify=verify_ssl) - except urllib.error.HTTPError as ex: + except APIError as ex: _LOGGER.error("Failed to connect to Unifi: %s", ex) persistent_notification.create( hass, 'Failed to connect to Unifi. ' @@ -77,9 +76,10 @@ class UnifiScanner(DeviceScanner): def _update(self): """Get the clients from the device.""" + from pyunifi.controller import APIError try: clients = self._controller.get_clients() - except urllib.error.HTTPError as ex: + except APIError as ex: _LOGGER.error("Failed to scan clients: %s", ex) clients = [] diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index df1c778dcb6..9bd5727510a 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -7,7 +7,10 @@ https://home-assistant.io/components/device_tracker.volvooncall/ import logging from homeassistant.util import slugify -from homeassistant.components.volvooncall import DOMAIN +from homeassistant.helpers.dispatcher import ( + dispatcher_connect, dispatcher_send) +from homeassistant.components.volvooncall import ( + DATA_KEY, SIGNAL_VEHICLE_SEEN) _LOGGER = logging.getLogger(__name__) @@ -18,19 +21,19 @@ def setup_scanner(hass, config, see, discovery_info=None): return vin, _ = discovery_info - vehicle = hass.data[DOMAIN].vehicles[vin] - - host_name = vehicle.registration_number - dev_id = 'volvo_' + slugify(host_name) + vehicle = hass.data[DATA_KEY].vehicles[vin] def see_vehicle(vehicle): """Handle the reporting of the vehicle position.""" + host_name = vehicle.registration_number + dev_id = 'volvo_{}'.format(slugify(host_name)) see(dev_id=dev_id, host_name=host_name, gps=(vehicle.position['latitude'], - vehicle.position['longitude'])) + vehicle.position['longitude']), + icon='mdi:car') - hass.data[DOMAIN].entities[vin].append(see_vehicle) - see_vehicle(vehicle) + dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle) + dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle) return True diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py new file mode 100644 index 00000000000..eb430582ba7 --- /dev/null +++ b/homeassistant/components/dyson.py @@ -0,0 +1,98 @@ +"""Parent component for Dyson Pure Cool Link devices.""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ + CONF_DEVICES + +REQUIREMENTS = ['libpurecoollink==0.1.5'] + +_LOGGER = logging.getLogger(__name__) + +CONF_LANGUAGE = "language" +CONF_RETRY = "retry" + +DEFAULT_TIMEOUT = 5 +DEFAULT_RETRY = 10 + +DOMAIN = "dyson" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_LANGUAGE): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [dict]), + }) +}, extra=vol.ALLOW_EXTRA) + +DYSON_DEVICES = "dyson_devices" + + +def setup(hass, config): + """Set up the Dyson parent component.""" + _LOGGER.info("Creating new Dyson component") + + if DYSON_DEVICES not in hass.data: + hass.data[DYSON_DEVICES] = [] + + from libpurecoollink.dyson import DysonAccount + dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_LANGUAGE)) + + logged = dyson_account.login() + + timeout = config[DOMAIN].get(CONF_TIMEOUT) + retry = config[DOMAIN].get(CONF_RETRY) + + if not logged: + _LOGGER.error("Not connected to Dyson account. Unable to add devices") + return False + + _LOGGER.info("Connected to Dyson account") + dyson_devices = dyson_account.devices() + if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + dyson_device = next((d for d in dyson_devices if + d.serial == device["device_id"]), None) + if dyson_device: + connected = dyson_device.connect(None, device["device_ip"], + timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", dyson_device) + hass.data[DYSON_DEVICES].append(dyson_device) + else: + _LOGGER.warning("Unable to connect to device %s", + dyson_device) + else: + _LOGGER.warning( + "Unable to find device %s in Dyson account", + device["device_id"]) + else: + # Not yet reliable + for device in dyson_devices: + _LOGGER.info("Trying to connect to device %s with timeout=%i " + "and retry=%i", device, timeout, retry) + connected = device.connect(None, None, timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", device) + hass.data[DYSON_DEVICES].append(device) + else: + _LOGGER.warning("Unable to connect to device %s", device) + + # Start fan/sensors components + if hass.data[DYSON_DEVICES]: + _LOGGER.debug("Starting sensor/fan components") + discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + discovery.load_platform(hass, "fan", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index db8af964fed..40a5d884aed 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.6'] +REQUIREMENTS = ['pyeight==0.0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 54c503c1b9f..4642017ce32 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -25,7 +25,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = 'fan' - +DEPENDENCIES = ['group'] SCAN_INTERVAL = timedelta(seconds=30) GROUP_NAME_ALL_FANS = 'all fans' @@ -73,7 +73,7 @@ FAN_TURN_ON_SCHEMA = vol.Schema({ }) # type: dict FAN_TURN_OFF_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) # type: dict FAN_OSCILLATE_SCHEMA = vol.Schema({ @@ -139,9 +139,7 @@ def turn_on(hass, entity_id: str=None, speed: str=None) -> None: def turn_off(hass, entity_id: str=None) -> None: """Turn all or specified fan off.""" - data = { - ATTR_ENTITY_ID: entity_id, - } + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @@ -218,8 +216,7 @@ def async_setup(hass, config: dict): if not fan.should_poll: continue - update_coro = hass.async_add_job( - fan.async_update_ha_state(True)) + update_coro = hass.async_add_job(fan.async_update_ha_state(True)) if hasattr(fan, 'async_update'): update_tasks.append(update_coro) else: diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index 3a3f255b806..bdb1b784c8b 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -9,31 +9,36 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_OSCILLATE, SUPPORT_DIRECTION) from homeassistant.const import STATE_OFF -FAN_NAME = 'Living Room Fan' -FAN_ENTITY_ID = 'fan.living_room_fan' - -DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION +FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION +LIMITED_SUPPORT = SUPPORT_SET_SPEED # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up the demo fan platform.""" add_devices_callback([ - DemoFan(hass, FAN_NAME, STATE_OFF), + DemoFan(hass, "Living Room Fan", FULL_SUPPORT), + DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), ]) class DemoFan(FanEntity): """A demonstration fan component.""" - def __init__(self, hass, name: str, initial_state: str) -> None: + def __init__(self, hass, name: str, supported_features: int) -> None: """Initialize the entity.""" self.hass = hass - self._speed = initial_state - self.oscillating = False - self.direction = "forward" + self._supported_features = supported_features + self._speed = STATE_OFF + self.oscillating = None + self.direction = None self._name = name + if supported_features & SUPPORT_OSCILLATE: + self.oscillating = False + if supported_features & SUPPORT_DIRECTION: + self.direction = "forward" + @property def name(self) -> str: """Get entity name.""" @@ -88,4 +93,4 @@ class DemoFan(FanEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return DEMO_SUPPORT + return self._supported_features diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py new file mode 100644 index 00000000000..f879c250a16 --- /dev/null +++ b/homeassistant/components/fan/dyson.py @@ -0,0 +1,218 @@ +"""Support for Dyson Pure Cool link fan.""" +import logging +import asyncio +from os import path +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + DOMAIN) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.config import load_yaml_config_file + +DEPENDENCIES = ['dyson'] + +_LOGGER = logging.getLogger(__name__) + + +DYSON_FAN_DEVICES = "dyson_fan_devices" +SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode' + +DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({ + vol.Required('entity_id'): cv.entity_id, + vol.Required('night_mode'): cv.boolean +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Dyson fan components.""" + _LOGGER.info("Creating new Dyson fans") + if DYSON_FAN_DEVICES not in hass.data: + hass.data[DYSON_FAN_DEVICES] = [] + + # Get Dyson Devices from parent component + for device in hass.data[DYSON_DEVICES]: + dyson_entity = DysonPureCoolLinkDevice(hass, device) + hass.data[DYSON_FAN_DEVICES].append(dyson_entity) + + add_devices(hass.data[DYSON_FAN_DEVICES]) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + def service_handle(service): + """Handle dyson services.""" + entity_id = service.data.get('entity_id') + night_mode = service.data.get('night_mode') + fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if + fan.entity_id == entity_id].__iter__(), None) + if fan_device is None: + _LOGGER.warning("Unable to find Dyson fan device %s", + str(entity_id)) + return + + if service.service == SERVICE_SET_NIGHT_MODE: + fan_device.night_mode(night_mode) + + # Register dyson service(s) + hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE, + service_handle, + descriptions.get(SERVICE_SET_NIGHT_MODE), + schema=DYSON_SET_NIGHT_MODE_SCHEMA) + + +class DysonPureCoolLinkDevice(FanEntity): + """Representation of a Dyson fan.""" + + def __init__(self, hass, device): + """Initialize the fan.""" + _LOGGER.info("Creating device %s", device.name) + self.hass = hass + self._device = device + + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.async_add_job( + self._device.add_message_listener(self.on_message)) + + def on_message(self, message): + """Called when new messages received from the fan.""" + _LOGGER.debug( + "Message received for fan device %s : %s", self.name, message) + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the display name of this fan.""" + return self._device.name + + def set_speed(self: ToggleEntity, speed: str) -> None: + """Set the speed of the fan. Never called ??.""" + _LOGGER.debug("Set fan speed to: " + speed) + from libpurecoollink.const import FanSpeed, FanMode + if speed == FanSpeed.FAN_SPEED_AUTO.value: + self._device.set_configuration(fan_mode=FanMode.AUTO) + else: + fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + self._device.set_configuration(fan_mode=FanMode.FAN, + fan_speed=fan_speed) + + def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: + """Turn on the fan.""" + _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) + from libpurecoollink.const import FanSpeed, FanMode + if speed: + if speed == FanSpeed.FAN_SPEED_AUTO.value: + self._device.set_configuration(fan_mode=FanMode.AUTO) + else: + fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + self._device.set_configuration(fan_mode=FanMode.FAN, + fan_speed=fan_speed) + else: + # Speed not set, just turn on + self._device.set_configuration(fan_mode=FanMode.FAN) + + def turn_off(self: ToggleEntity, **kwargs) -> None: + """Turn off the fan.""" + _LOGGER.debug("Turn off fan %s", self.name) + from libpurecoollink.const import FanMode + self._device.set_configuration(fan_mode=FanMode.OFF) + + def oscillate(self: ToggleEntity, oscillating: bool) -> None: + """Turn on/off oscillating.""" + _LOGGER.debug("Turn oscillation %s for device %s", oscillating, + self.name) + from libpurecoollink.const import Oscillation + + if oscillating: + self._device.set_configuration( + oscillation=Oscillation.OSCILLATION_ON) + else: + self._device.set_configuration( + oscillation=Oscillation.OSCILLATION_OFF) + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._device.state and self._device.state.oscillation == "ON" + + @property + def is_on(self): + """Return true if the entity is on.""" + if self._device.state: + return self._device.state.fan_state == "FAN" + return False + + @property + def speed(self) -> str: + """Return the current speed.""" + if self._device.state: + from libpurecoollink.const import FanSpeed + if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: + return self._device.state.speed + else: + return int(self._device.state.speed) + return None + + @property + def current_direction(self): + """Return direction of the fan [forward, reverse].""" + return None + + @property + def is_night_mode(self): + """Return Night mode.""" + return self._device.state.night_mode == "ON" + + def night_mode(self: ToggleEntity, night_mode: bool) -> None: + """Turn fan in night mode.""" + _LOGGER.debug("Set %s night mode %s", self.name, night_mode) + from libpurecoollink.const import NightMode + if night_mode: + self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) + else: + self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF) + + @property + def is_auto_mode(self): + """Return auto mode.""" + return self._device.state.fan_mode == "AUTO" + + def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: + """Turn fan in auto mode.""" + _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) + from libpurecoollink.const import FanMode + if auto_mode: + self._device.set_configuration(fan_mode=FanMode.AUTO) + else: + self._device.set_configuration(fan_mode=FanMode.FAN) + + @property + def speed_list(self: ToggleEntity) -> list: + """Get the list of available speeds.""" + from libpurecoollink.const import FanSpeed + supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value, + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value)] + + return supported_speeds + + @property + def supported_features(self: ToggleEntity) -> int: + """Flag supported features.""" + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 7862aa9a7c3..4a91f49e382 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -58,7 +58,18 @@ set_direction: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' + example: 'fan.living_room' direction: description: The direction to rotate example: 'left' + +dyson_set_night_mode: + description: Set the fan in night mode + + fields: + entity_id: + description: Name(s) of the entities to enable/disable night mode + example: 'fan.living_room' + night_mode: + description: Night mode status + example: true diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py index fe01ae5f3a4..364306ff8dd 100644 --- a/homeassistant/components/fan/zwave.py +++ b/homeassistant/components/fan/zwave.py @@ -36,7 +36,7 @@ SPEED_TO_VALUE = { def get_device(values, **kwargs): - """Create zwave entity device.""" + """Create Z-Wave entity device.""" return ZwaveFan(values) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index fd7f4c921cb..41abfc5eba6 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,8 +3,8 @@ FINGERPRINTS = { "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", "core.js": "d4a7cb8c80c62b536764e0e81385f6aa", - "frontend.html": "ed18c05632c071eb4f7b012382d0f810", - "mdi.html": "f407a5a57addbe93817ee1b244d33fbe", + "frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53", + "mdi.html": "1a5ad9654c1f0e57440e30afd92846a5", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8", "panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1", @@ -18,6 +18,6 @@ FINGERPRINTS = { "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", "panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4", - "panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9", + "panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 936db8dccde..a1608c3b16c 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -441,7 +441,7 @@ window.hassUtil.isComponentLoaded = function (hass, component) { window.hassUtil.computeLocationName = function (hass) { return hass.config.core.location_name; -}; \ 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 db87c3e66e3..8b08356adcf 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 75679e90f2a..81ab4ff8a8e 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 75679e90f2aa11bc1b42188965746217feef0ea6 +Subproject commit 81ab4ff8a8ef7cc4b96b60f63c16472b0427adc7 diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index bf70b13f7e1..853712565bc 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 83641f9059d..2d7ba719580 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-zwave.html b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html index 9cbcaf14c0a..f91812510db 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html @@ -31,7 +31,7 @@ }); this.selectedNodeAttrs = att.sort(); }, -});