diff --git a/.coveragerc b/.coveragerc index 403bf9d70eb615..f9f03ac2215adc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -77,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 @@ -210,6 +213,7 @@ omit = homeassistant/components/camera/rpi_camera.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 @@ -315,6 +319,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 @@ -346,7 +351,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 @@ -406,6 +410,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 @@ -444,6 +449,7 @@ omit = 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 @@ -505,6 +511,7 @@ omit = 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/homeassistant/__main__.py b/homeassistant/__main__.py index 219d413db12238..c02d8c8bfc6be6 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -74,7 +74,7 @@ def add(self, other): 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 +215,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 +329,7 @@ def open_browser(event): 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 +361,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) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 17d08265a5bf24..29ad5847b322ac 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 @@ 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 @@ def is_on(self): @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/calendar/google.py b/homeassistant/components/calendar/google.py index 362202d1bdea57..67d2e7179ba8db 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/climate/flexit.py b/homeassistant/components/climate/flexit.py new file mode 100644 index 00000000000000..5911486c761508 --- /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 1be7480a727656..e3439bdfc74048 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 9f6360361f15e9..591190383a074c 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -14,11 +14,13 @@ 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 @@ 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/homematic.py b/homeassistant/components/cover/homematic.py index ace08f53e3cea3..8fb003c6649541 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 @@ def stop_cover(self, **kwargs): 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/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 40ab48b384a12b..2f41d8fe0d30af 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -116,7 +116,6 @@ def decrypt_payload(topic, ciphertext): "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 1f21a25359cc64..36f1ea06fd639e 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -57,7 +57,7 @@ def ping(self): 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/fan/__init__.py b/homeassistant/components/fan/__init__.py index 54c503c1b9f136..f2ade5354264ea 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -73,7 +73,7 @@ }) # 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_handle_fan_service(service): 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 3a3f255b806d02..bdb1b784c8bea3 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -9,31 +9,36 @@ 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 @@ def current_direction(self) -> str: @property def supported_features(self) -> int: """Flag supported features.""" - return DEMO_SUPPORT + return self._supported_features diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py index fe01ae5f3a4204..364306ff8ddd2a 100644 --- a/homeassistant/components/fan/zwave.py +++ b/homeassistant/components/fan/zwave.py @@ -36,7 +36,7 @@ def get_device(values, **kwargs): - """Create zwave entity device.""" + """Create Z-Wave entity device.""" return ZwaveFan(values) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 07627a84e4e84a..aca3fd9a949d2c 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -1,5 +1,5 @@ """ -Support for Homematic devices. +Support for HomeMatic devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ @@ -21,7 +21,7 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.config import load_yaml_config_file -REQUIREMENTS = ['pyhomematic==0.1.27'] +REQUIREMENTS = ['pyhomematic==0.1.28'] DOMAIN = 'homematic' @@ -228,7 +228,7 @@ def set_var_value(hass, entity_id, value): def set_dev_value(hass, address, channel, param, value, proxy=None): - """Send virtual keypress to the Homematic controlller.""" + """Call setValue XML-RPC method of supplied proxy.""" data = { ATTR_ADDRESS: address, ATTR_CHANNEL: channel, @@ -245,16 +245,15 @@ def reconnect(hass): hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) -# pylint: disable=unused-argument def setup(hass, config): """Set up the Homematic component.""" from pyhomematic import HMConnection hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY) hass.data[DATA_DEVINIT] = {} - hass.data[DATA_STORE] = [] + hass.data[DATA_STORE] = set() - # Create hosts list for pyhomematic + # Create hosts-dictionary for pyhomematic remotes = {} hosts = {} for rname, rconfig in config[DOMAIN][CONF_HOSTS].items(): @@ -286,10 +285,10 @@ def setup(hass, config): interface_id='homeassistant' ) - # Start server thread, connect to peer, initialize to receive events + # Start server thread, connect to hosts, initialize to receive events hass.data[DATA_HOMEMATIC].start() - # Stops server when Homeassistant is shutting down + # Stops server when HASS is shutting down hass.bus.listen_once( EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) @@ -299,12 +298,12 @@ def setup(hass, config): entity_hubs.append(HMHub( hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES])) - # Register Homematic services + # Register HomeMatic services descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) def _hm_service_virtualkey(service): - """Service handle virtualkey services.""" + """Service to handle virtualkey servicecalls.""" address = service.data.get(ATTR_ADDRESS) channel = service.data.get(ATTR_CHANNEL) param = service.data.get(ATTR_PARAM) @@ -315,18 +314,18 @@ def _hm_service_virtualkey(service): _LOGGER.error("%s not found for service virtualkey!", address) return - # If param exists for this device + # Parameter doesn't exist for device if param not in hmdevice.ACTIONNODE: _LOGGER.error("%s not datapoint in hm device %s", param, address) return - # Channel exists? + # Channel doesn't exist for device if channel not in hmdevice.ACTIONNODE[param]: _LOGGER.error("%i is not a channel in hm device %s", channel, address) return - # Call key + # Call parameter hmdevice.actionNodeData(param, True, channel) hass.services.register( @@ -335,7 +334,7 @@ def _hm_service_virtualkey(service): schema=SCHEMA_SERVICE_VIRTUALKEY) def _service_handle_value(service): - """Set value on homematic variable.""" + """Service to call setValue method for HomeMatic system variable.""" entity_ids = service.data.get(ATTR_ENTITY_ID) name = service.data[ATTR_NAME] value = service.data[ATTR_VALUE] @@ -347,7 +346,7 @@ def _service_handle_value(service): entities = entity_hubs if not entities: - _LOGGER.error("Homematic controller not found!") + _LOGGER.error("No HomeMatic hubs available") return for hub in entities: @@ -359,7 +358,7 @@ def _service_handle_value(service): schema=SCHEMA_SERVICE_SET_VAR_VALUE) def _service_handle_reconnect(service): - """Reconnect to all homematic hubs.""" + """Service to reconnect all HomeMatic hubs.""" hass.data[DATA_HOMEMATIC].reconnect() hass.services.register( @@ -368,7 +367,7 @@ def _service_handle_reconnect(service): schema=SCHEMA_SERVICE_RECONNECT) def _service_handle_device(service): - """Service handle set_dev_value services.""" + """Service to call setValue method for HomeMatic devices.""" address = service.data.get(ATTR_ADDRESS) channel = service.data.get(ATTR_CHANNEL) param = service.data.get(ATTR_PARAM) @@ -380,7 +379,6 @@ def _service_handle_device(service): _LOGGER.error("%s not found!", address) return - # Call key hmdevice.setValue(param, value, channel) hass.services.register( @@ -392,10 +390,9 @@ def _service_handle_device(service): def _system_callback_handler(hass, config, src, *args): - """Handle the callback.""" + """System callback handler.""" + # New devices available at hub if src == 'newDevices': - _LOGGER.debug("newDevices with: %s", args) - # pylint: disable=unused-variable (interface_id, dev_descriptions) = args proxy = interface_id.split('-')[-1] @@ -403,34 +400,25 @@ def _system_callback_handler(hass, config, src, *args): if not hass.data[DATA_DEVINIT][proxy]: return - # Get list of all keys of the devices (ignoring channels) - key_dict = {} + addresses = [] for dev in dev_descriptions: - key_dict[dev['ADDRESS'].split(':')[0]] = True - - # Remove device they allready init by HA - tmp_devs = key_dict.copy() - for dev in tmp_devs: - if dev in hass.data[DATA_STORE]: - del key_dict[dev] - else: - hass.data[DATA_STORE].append(dev) + address = dev['ADDRESS'].split(':')[0] + if address not in hass.data[DATA_STORE]: + hass.data[DATA_STORE].add(address) + addresses.append(address) # Register EVENTS - # Search all device with a EVENTNODE that include data + # Search all devices with an EVENTNODE that includes data bound_event_callback = partial(_hm_event_handler, hass, proxy) - for dev in key_dict: + for dev in addresses: hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev) - # Have events? if hmdevice.EVENTNODE: - _LOGGER.debug("Register Events from %s", dev) hmdevice.setEventCallback( callback=bound_event_callback, bequeath=True) - # If configuration allows autodetection of devices, - # all devices not configured are added. - if key_dict: + # Create HASS entities + if addresses: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), @@ -440,18 +428,18 @@ def _system_callback_handler(hass, config, src, *args): ('climate', DISCOVER_CLIMATE)): # Get all devices of a specific type found_devices = _get_devices( - hass, discovery_type, key_dict, proxy) + hass, discovery_type, addresses, proxy) # When devices of this type are found - # they are setup in HA and an event is fired + # they are setup in HASS and an discovery event is fired if found_devices: - # Fire discovery event discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config) + # Homegear error message elif src == 'error': - _LOGGER.debug("Error: %s", args) + _LOGGER.error("Error: %s", args) (interface_id, errorcode, message) = args hass.bus.fire(EVENT_ERROR, { ATTR_ERRORCODE: errorcode, @@ -460,7 +448,7 @@ def _system_callback_handler(hass, config, src, *args): def _get_devices(hass, discovery_type, keys, proxy): - """Get the Homematic devices for given discovery_type.""" + """Get the HomeMatic devices for given discovery_type.""" device_arr = [] for key in keys: @@ -468,11 +456,11 @@ def _get_devices(hass, discovery_type, keys, proxy): class_name = device.__class__.__name__ metadata = {} - # Class supported by discovery type + # Class not supported by discovery type if class_name not in HM_DEVICE_TYPES[discovery_type]: continue - # Load metadata if needed to generate a param list + # Load metadata needed to generate a parameter list if discovery_type == DISCOVER_SENSORS: metadata.update(device.SENSORNODE) elif discovery_type == DISCOVER_BINARY_SENSORS: @@ -480,45 +468,41 @@ def _get_devices(hass, discovery_type, keys, proxy): else: metadata.update({None: device.ELEMENT}) - if metadata: - # Generate options for 1...n elements with 1...n params - for param, channels in metadata.items(): - if param in HM_IGNORE_DISCOVERY_NODE: - continue - - # Add devices - _LOGGER.debug("%s: Handling %s: %s: %s", - discovery_type, key, param, channels) - for channel in channels: - name = _create_ha_name( - name=device.NAME, channel=channel, param=param, - count=len(channels) - ) - device_dict = { - CONF_PLATFORM: "homematic", - ATTR_ADDRESS: key, - ATTR_PROXY: proxy, - ATTR_NAME: name, - ATTR_CHANNEL: channel - } - if param is not None: - device_dict[ATTR_PARAM] = param - - # Add new device - try: - DEVICE_SCHEMA(device_dict) - device_arr.append(device_dict) - except vol.MultipleInvalid as err: - _LOGGER.error("Invalid device config: %s", - str(err)) - else: - _LOGGER.debug("Got no params for %s", key) - _LOGGER.debug("%s autodiscovery done: %s", discovery_type, str(device_arr)) + # Generate options for 1...n elements with 1...n parameters + for param, channels in metadata.items(): + if param in HM_IGNORE_DISCOVERY_NODE: + continue + + # Add devices + _LOGGER.debug("%s: Handling %s: %s: %s", + discovery_type, key, param, channels) + for channel in channels: + name = _create_ha_name( + name=device.NAME, channel=channel, param=param, + count=len(channels) + ) + device_dict = { + CONF_PLATFORM: "homematic", + ATTR_ADDRESS: key, + ATTR_PROXY: proxy, + ATTR_NAME: name, + ATTR_CHANNEL: channel + } + if param is not None: + device_dict[ATTR_PARAM] = param + + # Add new device + try: + DEVICE_SCHEMA(device_dict) + device_arr.append(device_dict) + except vol.MultipleInvalid as err: + _LOGGER.error("Invalid device config: %s", + str(err)) return device_arr def _create_ha_name(name, channel, param, count): - """Generate a unique object name.""" + """Generate a unique entity id.""" # HMDevice is a simple device if count == 1 and param is None: return name @@ -527,11 +511,11 @@ def _create_ha_name(name, channel, param, count): if count > 1 and param is None: return "{} {}".format(name, channel) - # With multiple param first elements + # With multiple parameters on first channel if count == 1 and param is not None: return "{} {}".format(name, param) - # Multiple param on object with multiple elements + # Multiple parameters with multiple channels if count > 1 and param is not None: return "{} {} {}".format(name, channel, param) @@ -546,14 +530,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value): _LOGGER.error("Event handling channel convert error!") return - # is not a event? + # Return if not an event supported by device if attribute not in hmdevice.EVENTNODE: return _LOGGER.debug("Event %s for %s channel %i", attribute, hmdevice.NAME, channel) - # keypress event + # Keypress event if attribute in HM_PRESS_EVENTS: hass.bus.fire(EVENT_KEYPRESS, { ATTR_NAME: hmdevice.NAME, @@ -562,7 +546,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value): }) return - # impulse event + # Impulse event if attribute in HM_IMPULSE_EVENTS: hass.bus.fire(EVENT_IMPULSE, { ATTR_NAME: hmdevice.NAME, @@ -574,7 +558,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value): def _device_from_servicecall(hass, service): - """Extract homematic device from service call.""" + """Extract HomeMatic device from service call.""" address = service.data.get(ATTR_ADDRESS) proxy = service.data.get(ATTR_PROXY) if address == 'BIDCOS-RF': @@ -589,10 +573,10 @@ def _device_from_servicecall(hass, service): class HMHub(Entity): - """The Homematic hub. I.e. CCU2/HomeGear.""" + """The HomeMatic hub. (CCU2/HomeGear).""" def __init__(self, hass, name, use_variables): - """Initialize Homematic hub.""" + """Initialize HomeMatic hub.""" self.hass = hass self.entity_id = "{}.{}".format(DOMAIN, name.lower()) self._homematic = hass.data[DATA_HOMEMATIC] @@ -601,7 +585,7 @@ def __init__(self, hass, name, use_variables): self._state = STATE_UNKNOWN self._use_variables = use_variables - # load data + # Load data track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB) self._update_hub(None) @@ -617,7 +601,7 @@ def name(self): @property def should_poll(self): - """Return false. Homematic Hub object update variable.""" + """Return false. HomeMatic Hub object updates variables.""" return False @property @@ -660,7 +644,7 @@ def _update_variables(self, now): self.schedule_update_ha_state() def hm_set_variable(self, name, value): - """Set variable on homematic controller.""" + """Set variable value on CCU/Homegear.""" if name not in self._variables: _LOGGER.error("Variable %s not found on %s", name, self.name) return @@ -676,10 +660,10 @@ def hm_set_variable(self, name, value): class HMDevice(Entity): - """The Homematic device base object.""" + """The HomeMatic device base object.""" def __init__(self, hass, config): - """Initialize a generic Homematic device.""" + """Initialize a generic HomeMatic device.""" self.hass = hass self._homematic = hass.data[DATA_HOMEMATIC] self._name = config.get(ATTR_NAME) @@ -692,13 +676,13 @@ def __init__(self, hass, config): self._connected = False self._available = False - # Set param to uppercase + # Set parameter to uppercase if self._state: self._state = self._state.upper() @property def should_poll(self): - """Return false. Homematic states are pushed by the XML RPC Server.""" + """Return false. HomeMatic states are pushed by the XML-RPC Server.""" return False @property @@ -721,49 +705,44 @@ def device_state_attributes(self): """Return device specific state attributes.""" attr = {} - # no data available to create + # No data available if not self.available: return attr - # Generate an attributes list + # Generate a dictionary with attributes for node, data in HM_ATTRIBUTE_SUPPORT.items(): - # Is an attributes and exists for this object + # Is an attribute and exists for this object if node in self._data: value = data[1].get(self._data[node], self._data[node]) attr[data[0]] = value - # static attributes + # Static attributes attr['id'] = self._hmdevice.ADDRESS attr['proxy'] = self._proxy return attr def link_homematic(self): - """Connect to Homematic.""" - # Device is already linked + """Connect to HomeMatic.""" if self._connected: return True - # Init + # Initialize self._hmdevice = self._homematic.devices[self._proxy][self._address] self._connected = True - # Check if Homematic class is okay for HA class - _LOGGER.info("Start linking %s to %s", self._address, self._name) try: - # Init datapoints of this object + # Initialize datapoints of this object self._init_data() if self.hass.data[DATA_DELAY]: - # We delay / pause loading of data to avoid overloading - # of CCU / Homegear when doing auto detection + # We optionally delay / pause loading of data to avoid + # overloading of CCU / Homegear time.sleep(self.hass.data[DATA_DELAY]) self._load_data_from_hm() - _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) - # Link events from pyhomatic + # Link events from pyhomematic self._subscribe_homematic_events() self._available = not self._hmdevice.UNREACH - _LOGGER.debug("%s linking done", self._name) # pylint: disable=broad-except except Exception as err: self._connected = False @@ -774,29 +753,28 @@ def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) - have_change = False + has_changed = False # Is data needed for this instance? if attribute in self._data: # Did data change? if self._data[attribute] != value: self._data[attribute] = value - have_change = True + has_changed = True - # If available it has changed + # Availability has changed if attribute == 'UNREACH': self._available = bool(value) - have_change = True + has_changed = True - # If it has changed data point, update HA - if have_change: - _LOGGER.debug("%s update_ha_state after '%s'", self._name, - attribute) + # If it has changed data point, update HASS + if has_changed: self.schedule_update_ha_state() def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" - channels_to_sub = {0: True} # add channel 0 for UNREACH + channels_to_sub = set() + channels_to_sub.add(0) # Add channel 0 for UNREACH # Push data to channels_to_sub from hmdevice metadata for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, @@ -814,8 +792,7 @@ def _subscribe_homematic_events(self): # Prepare for subscription try: - if int(channel) >= 0: - channels_to_sub.update({int(channel): True}) + channels_to_sub.add(int(channel)) except (ValueError, TypeError): _LOGGER.error("Invalid channel in metadata from %s", self._name) @@ -858,14 +835,14 @@ def _hm_get_state(self): return None def _init_data(self): - """Generate a data dict (self._data) from the Homematic metadata.""" - # Add all attributes to data dict + """Generate a data dict (self._data) from the HomeMatic metadata.""" + # Add all attributes to data dictionary for data_note in self._hmdevice.ATTRIBUTENODE: self._data.update({data_note: STATE_UNKNOWN}) - # init device specified data + # Initialize device specific data self._init_data_struct() def _init_data_struct(self): - """Generate a data dict from the Homematic device metadata.""" + """Generate a data dictionary from the HomeMatic device metadata.""" raise NotImplementedError diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index dc5e59f4155b2d..d8647dea0c3ab9 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -51,7 +51,7 @@ CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' -# TLS configuation follows the best-practice guidelines specified here: +# TLS configuration follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS # Intermediate guidelines are followed. SSL_VERSION = ssl.PROTOCOL_SSLv23 @@ -339,7 +339,7 @@ def start(self): @asyncio.coroutine def stop(self): - """Stop the wsgi server.""" + """Stop the WSGI server.""" if self.server: self.server.close() yield from self.server.wait_closed() diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d2b9fa402ef96e..aa01ccde8d763c 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -19,6 +19,8 @@ KEY_FAILED_LOGIN_ATTEMPTS) from .util import get_real_ip +_LOGGER = logging.getLogger(__name__) + NOTIFICATION_ID_BAN = 'ip-ban' NOTIFICATION_ID_LOGIN = 'http-login' @@ -29,8 +31,6 @@ vol.Optional('banned_at'): vol.Any(None, cv.datetime) }) -_LOGGER = logging.getLogger(__name__) - @asyncio.coroutine def ban_middleware(app, handler): diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 625bc24c461f44..5922042e4fb577 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -6,7 +6,7 @@ KEY_BANS_ENABLED = 'ha_bans_enabled' KEY_BANNED_IPS = 'ha_banned_ips' KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts' -KEY_LOGIN_THRESHOLD = 'ha_login_treshold' +KEY_LOGIN_THRESHOLD = 'ha_login_threshold' KEY_DEVELOPMENT = 'ha_development' HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For' diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e6cdd0fcef9b95..f19d50300b894f 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -7,22 +7,56 @@ from datetime import timedelta import logging +import requests + +import voluptuous as vol + from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingEntity, PLATFORM_SCHEMA) -from homeassistant.components.opencv import ( - ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID, - CONF_NAME, process_image) + CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA, + ImageProcessingEntity) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['numpy==1.12.0'] _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['opencv'] +ATTR_MATCHES = 'matches' +ATTR_TOTAL_MATCHES = 'total_matches' + +CASCADE_URL = \ + 'https://raw.githubusercontent.com/opencv/opencv/master/data/' + \ + 'lbpcascades/lbpcascade_frontalface.xml' +CONF_CLASSIFIER = 'classifer' +CONF_FILE = 'file' +CONF_MIN_SIZE = 'min_size' +CONF_NEIGHBORS = 'neighbors' +CONF_SCALE = 'scale' + +DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml' +DEFAULT_MIN_SIZE = (30, 30) +DEFAULT_NEIGHBORS = 4 +DEFAULT_SCALE = 1.1 DEFAULT_TIMEOUT = 10 SCAN_INTERVAL = timedelta(seconds=2) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_CLASSIFIER, default=None): { + cv.string: vol.Any( + cv.isfile, + vol.Schema({ + vol.Required(CONF_FILE): cv.isfile, + vol.Optional(CONF_SCALE, DEFAULT_SCALE): float, + vol.Optional(CONF_NEIGHBORS, DEFAULT_NEIGHBORS): + cv.positive_int, + vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE): + vol.Schema((int, int)) + }) + ) + } +}) def _create_processor_from_config(hass, camera_entity, config): @@ -37,41 +71,63 @@ def _create_processor_from_config(hass, camera_entity, config): return processor +def _get_default_classifier(dest_path): + """Download the default OpenCV classifier.""" + _LOGGER.info('Downloading default classifier') + req = requests.get(CASCADE_URL, stream=True) + with open(dest_path, 'wb') as fil: + for chunk in req.iter_content(chunk_size=1024): + if chunk: # filter out keep-alive new chunks + fil.write(chunk) + + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the OpenCV image processing platform.""" - if discovery_info is None: + try: + # Verify opencv python package is preinstalled + # pylint: disable=unused-import,unused-variable + import cv2 # noqa + except ImportError: + _LOGGER.error("No opencv library found! " + + "Install or compile for your system " + + "following instructions here: " + + "http://opencv.org/releases.html") return - devices = [] - for camera_entity in discovery_info[CONF_ENTITY_ID]: - devices.append( - _create_processor_from_config(hass, camera_entity, discovery_info)) + entities = [] + if config[CONF_CLASSIFIER] is None: + dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH) + _get_default_classifier(dest_path) + config[CONF_CLASSIFIER] = { + 'Face': dest_path + } + + for camera in config[CONF_SOURCE]: + entities.append(OpenCVImageProcessor( + hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), + config[CONF_CLASSIFIER] + )) - add_devices(devices) + add_devices(entities) class OpenCVImageProcessor(ImageProcessingEntity): """Representation of an OpenCV image processor.""" - def __init__(self, hass, camera_entity, name, classifier_configs): + def __init__(self, hass, camera_entity, name, classifiers): """Initialize the OpenCV entity.""" self.hass = hass self._camera_entity = camera_entity - self._name = name - self._classifier_configs = classifier_configs + if name: + self._name = name + else: + self._name = "OpenCV {0}".format( + split_entity_id(camera_entity)[1]) + self._classifiers = classifiers self._matches = {} + self._total_matches = 0 self._last_image = None - @property - def last_image(self): - """Return the last image.""" - return self._last_image - - @property - def matches(self): - """Return the matches it found.""" - return self._matches - @property def camera_entity(self): """Return camera entity id from process pictures.""" @@ -85,20 +141,54 @@ def name(self): @property def state(self): """Return the state of the entity.""" - total_matches = 0 - for group in self._matches.values(): - total_matches += len(group) - return total_matches + return self._total_matches @property def state_attributes(self): """Return device specific state attributes.""" return { - ATTR_MATCHES: self._matches + ATTR_MATCHES: self._matches, + ATTR_TOTAL_MATCHES: self._total_matches } def process_image(self, image): """Process the image.""" - self._last_image = image - self._matches = process_image( - image, self._classifier_configs, False) + import cv2 # pylint: disable=import-error + import numpy + + # pylint: disable=no-member + cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), + cv2.IMREAD_UNCHANGED) + + for name, classifier in self._classifiers.items(): + scale = DEFAULT_SCALE + neighbors = DEFAULT_NEIGHBORS + min_size = DEFAULT_MIN_SIZE + if isinstance(classifier, dict): + path = classifier[CONF_FILE] + scale = classifier.get(CONF_SCALE, scale) + neighbors = classifier.get(CONF_NEIGHBORS, neighbors) + min_size = classifier.get(CONF_MIN_SIZE, min_size) + else: + path = classifier + + # pylint: disable=no-member + cascade = cv2.CascadeClassifier(path) + + detections = cascade.detectMultiScale( + cv_image, + scaleFactor=scale, + minNeighbors=neighbors, + minSize=min_size) + matches = {} + total_matches = 0 + regions = [] + # pylint: disable=invalid-name + for (x, y, w, h) in detections: + regions.append((int(x), int(y), int(w), int(h))) + total_matches += 1 + + matches[name] = regions + + self._matches = matches + self._total_matches = total_matches diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py index 9b9c327f82253d..d91f4666046371 100644 --- a/homeassistant/components/image_processing/seven_segments.py +++ b/homeassistant/components/image_processing/seven_segments.py @@ -20,7 +20,9 @@ _LOGGER = logging.getLogger(__name__) CONF_DIGITS = 'digits' +CONF_EXTRA_ARGUMENTS = 'extra_arguments' CONF_HEIGHT = 'height' +CONF_ROTATE = 'rotate' CONF_SSOCR_BIN = 'ssocr_bin' CONF_THRESHOLD = 'threshold' CONF_WIDTH = 'width' @@ -30,10 +32,12 @@ DEFAULT_BINARY = 'ssocr' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_EXTRA_ARGUMENTS, default=''): cv.string, vol.Optional(CONF_DIGITS, default=-1): cv.positive_int, vol.Optional(CONF_HEIGHT, default=0): cv.positive_int, vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string, vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int, + vol.Optional(CONF_ROTATE, default=0): cv.positive_int, vol.Optional(CONF_WIDTH, default=0): cv.positive_int, vol.Optional(CONF_X_POS, default=0): cv.string, vol.Optional(CONF_Y_POS, default=0): cv.positive_int, @@ -65,14 +69,18 @@ def __init__(self, hass, camera_entity, config, name): self._name = "SevenSegement OCR {0}".format( split_entity_id(camera_entity)[1]) self._state = None + self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png') - self._command = [ - config[CONF_SSOCR_BIN], 'erosion', 'make_mono', 'crop', - str(config[CONF_X_POS]), str(config[CONF_Y_POS]), - str(config[CONF_WIDTH]), str(config[CONF_HEIGHT]), '-t', - str(config[CONF_THRESHOLD]), '-d', str(config[CONF_DIGITS]), - self.filepath - ] + crop = ['crop', str(config[CONF_X_POS]), str(config[CONF_Y_POS]), + str(config[CONF_WIDTH]), str(config[CONF_HEIGHT])] + digits = ['-d', str(config[CONF_DIGITS])] + rotate = ['rotate', str(config[CONF_ROTATE])] + threshold = ['-t', str(config[CONF_THRESHOLD])] + extra_arguments = config[CONF_EXTRA_ARGUMENTS].split(' ') + + self._command = [config[CONF_SSOCR_BIN]] + crop + digits + threshold +\ + rotate + extra_arguments + self._command.append(self.filepath) @property def device_class(self): diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index abd02554ac08f9..7ac3f4dcdce46a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -96,7 +96,7 @@ def setup(hass, config): try: influx = InfluxDBClient(**kwargs) - influx.query("SHOW DIAGNOSTICS;", database=conf[CONF_DB_NAME]) + influx.query("SHOW SERIES LIMIT 1;", database=conf[CONF_DB_NAME]) except exceptions.InfluxDBClientError as exc: _LOGGER.error("Database host is not accessible due to '%s', please " "check your entries in the configuration file and that " diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index 1a0e8d7d9edda7..68d36058f8f770 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ -import colorsys import logging import asyncio import sys @@ -24,8 +23,6 @@ SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, preprocess_turn_on_alternatives) from homeassistant.config import load_yaml_config_file -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired) from homeassistant import util from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -37,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.4.7'] +REQUIREMENTS = ['aiolifx==0.4.8'] UDP_BROADCAST_PORT = 56700 @@ -49,16 +46,15 @@ SERVICE_LIFX_SET_STATE = 'lifx_set_state' ATTR_HSBK = 'hsbk' +ATTR_INFRARED = 'infrared' ATTR_POWER = 'power' -BYTE_MAX = 255 -SHORT_MAX = 65535 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ + ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_POWER: cv.boolean, }) @@ -200,15 +196,14 @@ def wait(self, method): return self.message -def convert_rgb_to_hsv(rgb): - """Convert Home Assistant RGB values to HSV values.""" - red, green, blue = [_ / BYTE_MAX for _ in rgb] +def convert_8_to_16(value): + """Scale an 8 bit level into 16 bits.""" + return (value << 8) | value - hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue) - return [int(hue * SHORT_MAX), - int(saturation * SHORT_MAX), - int(brightness * SHORT_MAX)] +def convert_16_to_8(value): + """Scale a 16 bit level into 8 bits.""" + return value >> 8 class LIFXLight(Light): @@ -260,14 +255,14 @@ def rgb_color(self): @property def brightness(self): """Return the brightness of this light between 0..255.""" - brightness = int(self._bri / (BYTE_MAX + 1)) + brightness = convert_16_to_8(self._bri) _LOGGER.debug("brightness: %d", brightness) return brightness @property def color_temp(self): """Return the color temperature.""" - temperature = color_temperature_kelvin_to_mired(self._kel) + temperature = color_util.color_temperature_kelvin_to_mired(self._kel) _LOGGER.debug("color_temp: %d", temperature) return temperature @@ -280,7 +275,7 @@ def min_mireds(self): kelvin = 6500 else: kelvin = 9000 - return math.floor(color_temperature_kelvin_to_mired(kelvin)) + return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) @property def max_mireds(self): @@ -290,7 +285,7 @@ def max_mireds(self): kelvin = 2700 else: kelvin = 2500 - return math.ceil(color_temperature_kelvin_to_mired(kelvin)) + return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) @property def is_on(self): @@ -370,6 +365,9 @@ def async_set_state(self, **kwargs): yield from lifx_effects.default_effect(self, **kwargs) return + if ATTR_INFRARED in kwargs: + self.device.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED])) + if ATTR_TRANSITION in kwargs: fade = int(kwargs[ATTR_TRANSITION] * 1000) else: @@ -446,7 +444,9 @@ def find_hsbk(self, **kwargs): if ATTR_RGB_COLOR in kwargs: hue, saturation, brightness = \ - convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) + color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) + saturation = convert_8_to_16(saturation) + brightness = convert_8_to_16(brightness) changed_color = True else: hue = self._hue @@ -455,12 +455,12 @@ def find_hsbk(self, **kwargs): if ATTR_XY_COLOR in kwargs: hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) - saturation = saturation * (BYTE_MAX + 1) + saturation = convert_8_to_16(saturation) changed_color = True # When color or temperature is set, use a default value for the other if ATTR_COLOR_TEMP in kwargs: - kelvin = int(color_temperature_mired_to_kelvin( + kelvin = int(color_util.color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP])) if not changed_color: saturation = 0 @@ -472,7 +472,7 @@ def find_hsbk(self, **kwargs): kelvin = self._kel if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) changed_color = True else: brightness = self._bri @@ -491,12 +491,8 @@ def set_color(self, hue, sat, bri, kel): self._bri = bri self._kel = kel - red, green, blue = colorsys.hsv_to_rgb( - hue / SHORT_MAX, sat / SHORT_MAX, bri / SHORT_MAX) - - red = int(red * BYTE_MAX) - green = int(green * BYTE_MAX) - blue = int(blue * BYTE_MAX) + red, green, blue = color_util.color_hsv_to_RGB( + hue, convert_16_to_8(sat), convert_16_to_8(bri)) _LOGGER.debug("set_color: %d %d %d %d [%d %d %d]", hue, sat, bri, kel, red, green, blue) diff --git a/homeassistant/components/light/lifx/services.yaml b/homeassistant/components/light/lifx/services.yaml index a907a665753f42..fb374f360d083c 100644 --- a/homeassistant/components/light/lifx/services.yaml +++ b/homeassistant/components/light/lifx/services.yaml @@ -9,6 +9,10 @@ lifx_set_state: '...': description: All turn_on parameters can be used to specify a color + infrared: + description: Automatic infrared level (0..255) when light brightness is low + example: 255 + transition: description: Duration in seconds it takes to get to the final state example: 10 diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py index 47cadec0f7d803..34d6cba7cb8b09 100644 --- a/homeassistant/components/light/lutron.py +++ b/homeassistant/components/light/lutron.py @@ -11,14 +11,14 @@ from homeassistant.components.lutron import ( LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER) -DEPENDENCIES = ['lutron'] - _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['lutron'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Lutron lights.""" + """Set up the Lutron lights.""" devs = [] for (area_name, device) in hass.data[LUTRON_DEVICES]['light']: dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER]) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index bc0cfacda1adfe..b2c39ee2fc5304 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -157,8 +157,6 @@ def effect_list(self): def turn_on(self, **kwargs): """Turn the device on.""" - self._luminary.set_onoff(1) - if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) _LOGGER.debug("turn_on requested transition time for light: " @@ -168,6 +166,16 @@ def turn_on(self, **kwargs): _LOGGER.debug("turn_on requested transition time for light: " "%s is: %s", self._name, transition) + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", + self._name, self._brightness) + self._luminary.set_luminance( + int(self._brightness / 2.55), + transition) + else: + self._luminary.set_onoff(1) + if ATTR_RGB_COLOR in kwargs: red, green, blue = kwargs[ATTR_RGB_COLOR] _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:" @@ -191,14 +199,6 @@ def turn_on(self, **kwargs): "%s: %s", self._name, kelvin) self._luminary.set_temperature(kelvin, transition) - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", - self._name, self._brightness) - self._luminary.set_luminance( - int(self._brightness / 2.55), - transition) - if ATTR_EFFECT in kwargs: effect = kwargs.get(ATTR_EFFECT) if effect == EFFECT_RANDOM: diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 6854fac550e2b4..07703d6c067d7f 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -59,7 +59,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template = device_config[CONF_VALUE_TEMPLATE] on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] - level_action = device_config[CONF_LEVEL_ACTION] + level_action = device_config.get(CONF_LEVEL_ACTION) level_template = device_config[CONF_LEVEL_TEMPLATE] template_entity_ids = set() @@ -108,7 +108,9 @@ def __init__(self, hass, device_id, friendly_name, state_template, self._template = state_template self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) - self._level_script = Script(hass, level_action) + self._level_script = None + if level_action is not None: + self._level_script = Script(hass, level_action) self._level_template = level_template self._state = False diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 70432de6644b34..98e127acc986e7 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,7 +7,8 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS) + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) @@ -15,7 +16,7 @@ DEPENDENCIES = ['vera'] -SUPPORT_VERA = SUPPORT_BRIGHTNESS +SUPPORT_VERA = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR # pylint: disable=unused-argument @@ -40,6 +41,12 @@ def brightness(self): if self.vera_device.is_dimmable: return self.vera_device.get_brightness() + @property + def rgb_color(self): + """Return the color of the light.""" + if self.vera_device.is_dimmable: + return self.vera_device.get_color() + @property def supported_features(self): """Flag supported features.""" @@ -47,7 +54,9 @@ def supported_features(self): def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: + if ATTR_RGB_COLOR in kwargs and self.vera_device.is_dimmable: + self.vera_device.set_color(kwargs[ATTR_RGB_COLOR]) + elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS]) else: self.vera_device.switch_on() diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 316f4f0f5bc074..02b049618d207d 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -10,10 +10,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_EMAIL, CONF_PASSWORD, STATE_LOCKED, STATE_UNLOCKED) + ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, + STATE_LOCKED, STATE_UNLOCKED) from homeassistant.helpers.typing import ConfigType -REQUIREMENTS = ['pysesame==0.0.2'] +REQUIREMENTS = ['pysesame==0.1.0'] + +ATTR_DEVICE_ID = 'device_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_EMAIL): cv.string, @@ -37,12 +40,10 @@ def setup_platform(hass, config: ConfigType, class SesameDevice(LockDevice): """Representation of a Sesame device.""" - device_id = None _sesame = None def __init__(self, sesame: object) -> None: """Initialize the Sesame device.""" - self.device_id = sesame.device_id self._sesame = sesame @property @@ -50,6 +51,11 @@ def name(self) -> str: """Return the name of the device.""" return self._sesame.nickname + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._sesame.api_enabled + @property def is_locked(self) -> bool: """Return True if the device is currently locked, else False.""" @@ -73,3 +79,11 @@ def unlock(self, **kwargs) -> None: def update(self) -> None: """Update the internal state of the device.""" self._sesame.update_state() + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + attributes = {} + attributes[ATTR_DEVICE_ID] = self._sesame.device_id + attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery + return attributes diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index af0175bbbf4d8c..d9b943762dcd18 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -7,6 +7,10 @@ import asyncio import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity @@ -19,6 +23,14 @@ LUTRON_CONTROLLER = 'lutron_controller' LUTRON_DEVICES = 'lutron_devices' +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + def setup(hass, base_config): """Set up the Lutron component.""" @@ -29,13 +41,11 @@ def setup(hass, base_config): config = base_config.get(DOMAIN) hass.data[LUTRON_CONTROLLER] = Lutron( - config['lutron_host'], - config['lutron_user'], - config['lutron_password'] - ) + config[CONF_HOST], config[CONF_USERNAME], config[CONF_USERNAME]) + hass.data[LUTRON_CONTROLLER].load_xml_db() hass.data[LUTRON_CONTROLLER].connect() - _LOGGER.info("Connected to Main Repeater at %s", config['lutron_host']) + _LOGGER.info("Connected to main repeater at %s", config[CONF_HOST]) # Sort our devices into types for area in hass.data[LUTRON_CONTROLLER].areas: diff --git a/homeassistant/components/mailgun.py b/homeassistant/components/mailgun.py new file mode 100644 index 00000000000000..ec480ac12d6061 --- /dev/null +++ b/homeassistant/components/mailgun.py @@ -0,0 +1,51 @@ +""" +Support for Mailgun. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mailgun/ +""" +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_API_KEY, CONF_DOMAIN +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView + + +DOMAIN = 'mailgun' +API_PATH = '/api/{}'.format(DOMAIN) +DATA_MAILGUN = DOMAIN +DEPENDENCIES = ['http'] +MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN) +CONF_SANDBOX = 'sandbox' +DEFAULT_SANDBOX = False + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DOMAIN): cv.string, + vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Mailgun component.""" + hass.data[DATA_MAILGUN] = config[DOMAIN] + hass.http.register_view(MailgunReceiveMessageView()) + return True + + +class MailgunReceiveMessageView(HomeAssistantView): + """Handle data from Mailgun inbound messages.""" + + url = API_PATH + name = 'api:{}'.format(DOMAIN) + + @callback + def post(self, request): # pylint: disable=no-self-use + """Handle Mailgun message POST.""" + hass = request.app['hass'] + data = yield from request.post() + hass.bus.async_fire(MESSAGE_RECEIVED, dict(data)) + return diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 2d51cacadffaa8..ae6d9e04643b1e 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -108,7 +108,6 @@ def wrapper(obj, *args, **kwargs): class SharpAquosTVDevice(MediaPlayerDevice): """Representation of a Aquos TV.""" - # pylint: disable=too-many-public-methods def __init__(self, name, remote, power_on_enabled=False): """Initialize the aquos device.""" global SUPPORT_SHARPTV diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 5fdfbcfb864e79..ea8b7e389ec1fd 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -19,11 +19,13 @@ CONF_NAME, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.4.2'] +REQUIREMENTS = ['denonavr==0.4.4'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = None +DEFAULT_SHOW_SOURCES = False +CONF_SHOW_ALL_SOURCES = 'show_all_sources' KEY_DENON_CACHE = 'denonavr_hosts' SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ @@ -37,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): + cv.boolean, }) @@ -52,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): cache = hass.data[KEY_DENON_CACHE] = set() # Start assignment of host and name + show_all_sources = config.get(CONF_SHOW_ALL_SOURCES) # 1. option: manual setting if config.get(CONF_HOST) is not None: host = config.get(CONF_HOST) @@ -60,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if host not in cache: cache.add(host) receivers.append( - DenonDevice(denonavr.DenonAVR(host, name))) + DenonDevice(denonavr.DenonAVR(host, name, show_all_sources))) _LOGGER.info("Denon receiver at host %s initialized", host) # 2. option: discovery using netdisco if discovery_info is not None: @@ -70,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if host not in cache: cache.add(host) receivers.append( - DenonDevice(denonavr.DenonAVR(host, name))) + DenonDevice(denonavr.DenonAVR(host, name, show_all_sources))) _LOGGER.info("Denon receiver at host %s initialized", host) # 3. option: discovery using denonavr library if config.get(CONF_HOST) is None and discovery_info is None: @@ -85,7 +90,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if host not in cache: cache.add(host) receivers.append( - DenonDevice(denonavr.DenonAVR(host, name))) + DenonDevice( + denonavr.DenonAVR(host, name, show_all_sources))) _LOGGER.info("Denon receiver at host %s initialized", host) # Add all freshly discovered receivers diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index 3eda559c219568..6f2b9ac4a71c58 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -17,7 +17,7 @@ CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['nad_receiver==0.0.5'] +REQUIREMENTS = ['nad_receiver==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/nadtcp.py b/homeassistant/components/media_player/nadtcp.py new file mode 100644 index 00000000000000..0b04f173c48150 --- /dev/null +++ b/homeassistant/components/media_player/nadtcp.py @@ -0,0 +1,181 @@ +""" +Support for NAD digital amplifiers which can be remote controlled via tcp/ip. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.nadtcp/ +""" +import logging +import voluptuous as vol +from homeassistant.components.media_player import ( + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, + PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_NAME, STATE_OFF, STATE_ON) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['nad_receiver==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'NAD amplifier' +DEFAULT_MIN_VOLUME = -60 +DEFAULT_MAX_VOLUME = -10 +DEFAULT_VOLUME_STEP = 4 + +SUPPORT_NAD = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | \ + SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE + +CONF_MIN_VOLUME = 'min_volume' +CONF_MAX_VOLUME = 'max_volume' +CONF_VOLUME_STEP = 'volume_step' +CONF_HOST = 'host' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MIN_VOLUME, default=DEFAULT_MIN_VOLUME): int, + vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): int, + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): int, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the NAD platform.""" + from nad_receiver import NADReceiverTCP + add_devices([NADtcp( + NADReceiverTCP(config.get(CONF_HOST)), + config.get(CONF_NAME), + config.get(CONF_MIN_VOLUME), + config.get(CONF_MAX_VOLUME), + config.get(CONF_VOLUME_STEP), + )]) + + +class NADtcp(MediaPlayerDevice): + """Representation of a NAD Digital amplifier.""" + + def __init__(self, nad_device, name, min_volume, max_volume, volume_step): + """Initialize the amplifier.""" + self._name = name + self.nad_device = nad_device + self._min_vol = (min_volume + 90) * 2 # from dB to nad vol (0-200) + self._max_vol = (max_volume + 90) * 2 # from dB to nad vol (0-200) + self._volume_step = volume_step + self._state = None + self._mute = None + self._nad_volume = None + self._volume = None + self._source = None + self._source_list = self.nad_device.available_sources() + + self.update() + + @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 + + def update(self): + """Get the latest details from the device.""" + try: + nad_status = self.nad_device.status() + except OSError: + return + if nad_status is None: + return + + # Update on/off state + if nad_status['power']: + self._state = STATE_ON + else: + self._state = STATE_OFF + + # Update current volume + self._volume = self.nad_vol_to_internal_vol(nad_status['volume']) + self._nad_volume = nad_status['volume'] + + # Update muted state + self._mute = nad_status['muted'] + + # Update current source + self._source = nad_status['source'] + + def nad_vol_to_internal_vol(self, nad_volume): + """Convert nad volume range (0-200) to internal volume range. + + Takes into account configured min and max volume. + """ + if nad_volume < self._min_vol: + volume_internal = 0.0 + if nad_volume > self._max_vol: + volume_internal = 1.0 + else: + volume_internal = (nad_volume - self._min_vol) / \ + (self._max_vol - self._min_vol) + return volume_internal + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_NAD + + def turn_off(self): + """Turn the media player off.""" + self.nad_device.power_off() + + def turn_on(self): + """Turn the media player on.""" + self.nad_device.power_on() + + def volume_up(self): + """Step volume up in the configured increments.""" + self.nad_device.set_volume(self._nad_volume + 2 * self._volume_step) + + def volume_down(self): + """Step volume down in the configured increments.""" + self.nad_device.set_volume(self._nad_volume - 2 * self._volume_step) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + nad_volume_to_set = \ + int(round(volume * (self._max_vol - self._min_vol) + + self._min_vol)) + self.nad_device.set_volume(nad_volume_to_set) + + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + if mute: + self.nad_device.mute() + else: + self.nad_device.unmute() + + def select_source(self, source): + """Select input source.""" + self.nad_device.select_source(source) + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self.nad_device.available_sources() + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._mute diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index 25d6390bf083d7..b2242bfecad554 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -14,7 +14,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF) -REQUIREMENTS = ['openhomedevice==0.4.0'] +REQUIREMENTS = ['openhomedevice==0.4.2'] SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \ SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ @@ -60,7 +60,6 @@ def __init__(self, hass, device): self._track_information = {} self._in_standby = None self._transport_state = None - self._track_information = None self._volume_level = None self._volume_muted = None self._supported_features = SUPPORT_OPENHOME @@ -173,27 +172,29 @@ def source_list(self): @property def media_image_url(self): """Image url of current playing media.""" - return self._track_information["albumArtwork"] + return self._track_information.get('albumArtwork') @property def media_artist(self): """Artist of current playing media, music track only.""" - return self._track_information["artist"][0] + artists = self._track_information.get('artist') + if artists: + return artists[0] @property def media_album_name(self): """Album name of current playing media, music track only.""" - return self._track_information["albumTitle"] + return self._track_information.get('albumTitle') @property def media_title(self): """Title of current playing media.""" - return self._track_information["title"] + return self._track_information.get('title') @property def source(self): """Name of the current input source.""" - return self._source["name"] + return self._source.get('name') @property def volume_level(self): diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index e2f322a8ec89bd..f4b0ca1c0d6344 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -134,7 +134,6 @@ def setup_plexserver(host, token, hass, config, add_devices_callback): track_utc_time_change(hass, lambda now: update_devices(), second=30) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - # pylint: disable=too-many-branches def update_devices(): """Update the devices objects.""" try: @@ -231,11 +230,9 @@ def plex_configuration_callback(data): }]) -# pylint: disable=too-many-instance-attributes, too-many-public-methods class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" - # pylint: disable=too-many-arguments def __init__(self, config, device, session, plex_sessions, update_devices, update_sessions): """Initialize the Plex device.""" @@ -299,7 +296,6 @@ def __init__(self, config, device, session, plex_sessions, 'media_player', prefix, self.name.lower().replace('-', '_')) - # pylint: disable=too-many-branches, too-many-statements def refresh(self, device, session): """Refresh key device data.""" # new data refresh diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index a81f9330ab8911..aac6b1a228db01 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -125,8 +125,7 @@ def name(self): """Return the name of the device.""" if self.device_info.userdevicename: return self.device_info.userdevicename - else: - return "roku_" + self.roku.device_info.sernum + return "Roku {}".format(self.device_info.sernum) @property def state(self): @@ -158,8 +157,7 @@ def media_content_type(self): return None elif self.current_app.name == "Roku": return None - else: - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_VIDEO @property def media_image_url(self): diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index fcb650ce1459a1..b26f0f48a77d1a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -524,7 +524,7 @@ def update(self): support_previous_track = False support_next_track = False support_play = False - support_stop = False + support_stop = True support_pause = False if is_playing_tv: @@ -925,8 +925,8 @@ def source(self): @soco_error def turn_off(self): """Turn off media player.""" - if self._support_pause: - self.media_pause() + if self._support_stop: + self.media_stop() @soco_error @soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 4992a398b2d616..52cce424e0f4e5 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -161,16 +161,16 @@ def update(self): player_devices = self._player.devices() if player_devices is not None: devices = player_devices.get('devices') - if devices is not None: - old_devices = self._devices - self._devices = {self._aliases.get(device.get('id'), - device.get('name')): - device.get('id') - for device in devices} - device_diff = {name: id for name, id in self._devices.items() - if old_devices.get(name, None) is None} - if len(device_diff) > 0: - _LOGGER.info("New Devices: %s", str(device_diff)) + if devices is not None: + old_devices = self._devices + self._devices = {self._aliases.get(device.get('id'), + device.get('name')): + device.get('id') + for device in devices} + device_diff = {name: id for name, id in self._devices.items() + if old_devices.get(name, None) is None} + if len(device_diff) > 0: + _LOGGER.info("New Devices: %s", str(device_diff)) # Current playback state current = self._player.current_playback() if current is None: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f4b57f0c803a32..16ac00e3b7b4ac 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -102,7 +102,7 @@ def valid_discovery_topic(value): _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ - 'the mqtt broker config' + 'the MQTT broker configuration' MQTT_WILL_BIRTH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, @@ -126,9 +126,8 @@ def valid_discovery_topic(value): vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth', msg=CLIENT_KEY_AUTH_MSG): cv.isfile, vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION, - default=DEFAULT_TLS_PROTOCOL): vol.Any('auto', '1.0', - '1.1', '1.2'), + vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): + vol.Any('auto', '1.0', '1.1', '1.2'), vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA, @@ -237,9 +236,7 @@ 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, encoding), - hass.loop + async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop ).result() def remove(): diff --git a/homeassistant/components/notify/mailgun.py b/homeassistant/components/notify/mailgun.py index 0e4254ae0838d1..59c6a50ffc9c87 100644 --- a/homeassistant/components/notify/mailgun.py +++ b/homeassistant/components/notify/mailgun.py @@ -8,40 +8,37 @@ import voluptuous as vol +from homeassistant.components.mailgun import CONF_SANDBOX, DATA_MAILGUN from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA) from homeassistant.const import ( - CONF_TOKEN, CONF_DOMAIN, CONF_RECIPIENT, CONF_SENDER) -import homeassistant.helpers.config_validation as cv + CONF_API_KEY, CONF_DOMAIN, CONF_RECIPIENT, CONF_SENDER) _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mailgun'] REQUIREMENTS = ['pymailgunner==1.4'] # Images to attach to notification ATTR_IMAGES = 'images' -CONF_SANDBOX = 'sandbox' - DEFAULT_SENDER = 'hass@{domain}' DEFAULT_SANDBOX = False # pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_RECIPIENT): vol.Email(), - vol.Optional(CONF_DOMAIN): cv.string, - vol.Optional(CONF_SENDER): vol.Email(), - vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean, + vol.Optional(CONF_SENDER): vol.Email() }) def get_service(hass, config, discovery_info=None): """Get the Mailgun notification service.""" + data = hass.data[DATA_MAILGUN] mailgun_service = MailgunNotificationService( - config.get(CONF_DOMAIN), config.get(CONF_SANDBOX), - config.get(CONF_TOKEN), config.get(CONF_SENDER), + data.get(CONF_DOMAIN), data.get(CONF_SANDBOX), + data.get(CONF_API_KEY), config.get(CONF_SENDER), config.get(CONF_RECIPIENT)) if mailgun_service.connection_is_valid(): return mailgun_service @@ -52,19 +49,19 @@ def get_service(hass, config, discovery_info=None): class MailgunNotificationService(BaseNotificationService): """Implement a notification service for the Mailgun mail service.""" - def __init__(self, domain, sandbox, token, sender, recipient): + def __init__(self, domain, sandbox, api_key, sender, recipient): """Initialize the service.""" self._client = None # Mailgun API client self._domain = domain self._sandbox = sandbox - self._token = token + self._api_key = api_key self._sender = sender self._recipient = recipient def initialize_client(self): """Initialize the connection to Mailgun.""" from pymailgunner import Client - self._client = Client(self._token, self._domain, self._sandbox) + self._client = Client(self._api_key, self._domain, self._sandbox) _LOGGER.debug("Mailgun domain: %s", self._client.domain) self._domain = self._client.domain if not self._sender: diff --git a/homeassistant/components/opencv.py b/homeassistant/components/opencv.py deleted file mode 100644 index 634e8e156a1576..00000000000000 --- a/homeassistant/components/opencv.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Support for OpenCV image/video processing. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/opencv/ -""" -import logging -import os -import voluptuous as vol - -import requests - -from homeassistant.const import ( - CONF_NAME, - CONF_ENTITY_ID, - CONF_FILE_PATH -) -from homeassistant.helpers import ( - discovery, - config_validation as cv, -) - -REQUIREMENTS = ['opencv-python==3.2.0.6', 'numpy==1.12.0'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_MATCHES = 'matches' - -BASE_PATH = os.path.realpath(__file__) - -CASCADE_URL = \ - 'https://raw.githubusercontent.com/opencv/opencv/master/data/' +\ - 'lbpcascades/lbpcascade_frontalface.xml' - -CONF_CLASSIFIER = 'classifier' -CONF_COLOR = 'color' -CONF_GROUPS = 'classifier_group' -CONF_MIN_SIZE = 'min_size' -CONF_NEIGHBORS = 'neighbors' -CONF_SCALE = 'scale' - -DATA_CLASSIFIER_GROUPS = 'classifier_groups' - -DEFAULT_COLOR = (255, 255, 0) -DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml' -DEFAULT_NAME = 'OpenCV' -DEFAULT_MIN_SIZE = (30, 30) -DEFAULT_NEIGHBORS = 4 -DEFAULT_SCALE = 1.1 - -DOMAIN = 'opencv' - -CLASSIFIER_GROUP_CONFIG = { - vol.Required(CONF_CLASSIFIER): vol.All( - cv.ensure_list, - [vol.Schema({ - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): - vol.Schema((int, int, int)), - vol.Optional(CONF_FILE_PATH, default=None): cv.isfile, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): - cv.string, - vol.Optional(CONF_MIN_SIZE, default=DEFAULT_MIN_SIZE): - vol.Schema((int, int)), - vol.Optional(CONF_NEIGHBORS, default=DEFAULT_NEIGHBORS): - cv.positive_int, - vol.Optional(CONF_SCALE, default=DEFAULT_SCALE): - float - })]), - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, -} -CLASSIFIER_GROUP_SCHEMA = vol.Schema(CLASSIFIER_GROUP_CONFIG) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_GROUPS): vol.All( - cv.ensure_list, - [CLASSIFIER_GROUP_SCHEMA] - ), - }) -}, extra=vol.ALLOW_EXTRA) - - -# NOTE: -# pylint cannot find any of the members of cv2, using disable=no-member -# to pass linting - - -def cv_image_to_bytes(cv_image): - """Convert OpenCV image to bytes.""" - import cv2 # pylint: disable=import-error - - # pylint: disable=no-member - encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90] - # pylint: disable=no-member - success, data = cv2.imencode('.jpg', cv_image, encode_param) - - if success: - return data.tobytes() - - return None - - -def cv_image_from_bytes(image): - """Convert image bytes to OpenCV image.""" - import cv2 # pylint: disable=import-error - import numpy - - # pylint: disable=no-member - return cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - - -def process_image(image, classifier_group, is_camera): - """Process the image given a classifier group.""" - import cv2 # pylint: disable=import-error - import numpy - - # pylint: disable=no-member - cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), - cv2.IMREAD_UNCHANGED) - group_matches = {} - for classifier_config in classifier_group: - classifier_path = classifier_config[CONF_FILE_PATH] - classifier_name = classifier_config[CONF_NAME] - color = classifier_config[CONF_COLOR] - scale = classifier_config[CONF_SCALE] - neighbors = classifier_config[CONF_NEIGHBORS] - min_size = classifier_config[CONF_MIN_SIZE] - - # pylint: disable=no-member - classifier = cv2.CascadeClassifier(classifier_path) - - detections = classifier.detectMultiScale(cv_image, - scaleFactor=scale, - minNeighbors=neighbors, - minSize=min_size) - regions = [] - # pylint: disable=invalid-name - for (x, y, w, h) in detections: - if is_camera: - # pylint: disable=no-member - cv2.rectangle(cv_image, - (x, y), - (x + w, y + h), - color, - 2) - else: - regions.append((int(x), int(y), int(w), int(h))) - group_matches[classifier_name] = regions - - if is_camera: - return cv_image_to_bytes(cv_image) - else: - return group_matches - - -def setup(hass, config): - """Set up the OpenCV platform entities.""" - default_classifier = hass.config.path(DEFAULT_CLASSIFIER_PATH) - - if not os.path.isfile(default_classifier): - _LOGGER.info('Downloading default classifier') - - req = requests.get(CASCADE_URL, stream=True) - with open(default_classifier, 'wb') as fil: - for chunk in req.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - fil.write(chunk) - - for group in config[DOMAIN][CONF_GROUPS]: - grp = {} - - for classifier, config in group.items(): - config = dict(config) - - if config[CONF_FILE_PATH] is None: - config[CONF_FILE_PATH] = default_classifier - - grp[classifier] = config - - discovery.load_platform(hass, 'image_processing', DOMAIN, grp) - - return True diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 5e36c4715620f6..212b2e7e7dad6c 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -26,6 +26,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SERVICE_CREATE = 'create' +SERVICE_DISMISS = 'dismiss' SCHEMA_SERVICE_CREATE = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.template, @@ -33,6 +34,10 @@ vol.Optional(ATTR_NOTIFICATION_ID): cv.string, }) +SCHEMA_SERVICE_DISMISS = vol.Schema({ + vol.Required(ATTR_NOTIFICATION_ID): cv.string, +}) + DEFAULT_OBJECT_ID = 'notification' _LOGGER = logging.getLogger(__name__) @@ -43,6 +48,11 @@ def create(hass, message, title=None, notification_id=None): hass.add_job(async_create, hass, message, title, notification_id) +def dismiss(hass, notification_id): + """Remove a notification.""" + hass.add_job(async_dismiss, hass, notification_id) + + @callback def async_create(hass, message, title=None, notification_id=None): """Generate a notification.""" @@ -57,6 +67,14 @@ def async_create(hass, message, title=None, notification_id=None): hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) +@callback +def async_dismiss(hass, notification_id): + """Remove a notification.""" + data = {ATTR_NOTIFICATION_ID: notification_id} + + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) + + @asyncio.coroutine def async_setup(hass, config): """Set up the persistent notification component.""" @@ -92,12 +110,25 @@ def create_service(call): hass.states.async_set(entity_id, message, attr) + @callback + def dismiss_service(call): + """Handle the dismiss notification service call.""" + notification_id = call.data.get(ATTR_NOTIFICATION_ID) + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + + hass.states.async_remove(entity_id) + descriptions = yield from hass.async_add_job( load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml') ) + hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service, descriptions[DOMAIN][SERVICE_CREATE], SCHEMA_SERVICE_CREATE) + hass.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service, + descriptions[DOMAIN][SERVICE_DISMISS], + SCHEMA_SERVICE_DISMISS) + return True diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py new file mode 100644 index 00000000000000..72346a23922b79 --- /dev/null +++ b/homeassistant/components/python_script.py @@ -0,0 +1,86 @@ +"""Component to allow running Python scripts.""" +import glob +import os +import logging + +import voluptuous as vol + +DOMAIN = 'python_script' +REQUIREMENTS = ['restrictedpython==4.0a2'] +FOLDER = 'python_scripts' +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema(dict) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Initialize the python_script component.""" + path = hass.config.path(FOLDER) + + if not os.path.isdir(path): + _LOGGER.warning('Folder %s not found in config folder', FOLDER) + return False + + def service_handler(call): + """Handle python script service calls.""" + filename = '{}.py'.format(call.service) + with open(hass.config.path(FOLDER, filename)) as fil: + execute(hass, filename, fil.read(), call.data) + + for fil in glob.iglob(os.path.join(path, '*.py')): + name = os.path.splitext(os.path.basename(fil))[0] + hass.services.register(DOMAIN, name, service_handler) + + return True + + +def execute(hass, filename, source, data): + """Execute a script.""" + from RestrictedPython import compile_restricted_exec + from RestrictedPython.Guards import safe_builtins, full_write_guard + + compiled = compile_restricted_exec(source, filename=filename) + + if compiled.errors: + _LOGGER.error('Error loading script %s: %s', filename, + ', '.join(compiled.errors)) + return + + if compiled.warnings: + _LOGGER.warning('Warning loading script %s: %s', filename, + ', '.join(compiled.warnings)) + + restricted_globals = { + '__builtins__': safe_builtins, + '_print_': StubPrinter, + '_getattr_': getattr, + '_write_': full_write_guard, + } + local = { + 'hass': hass, + 'data': data, + 'logger': logging.getLogger('{}.{}'.format(__name__, filename)) + } + + try: + _LOGGER.info('Executing %s: %s', filename, data) + # pylint: disable=exec-used + exec(compiled.code, restricted_globals, local) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception('Error executing script %s: %s', filename, err) + + +class StubPrinter: + """Class to handle printing inside scripts.""" + + def __init__(self, _getattr_): + """Initialize our printer.""" + pass + + def _call_print(self, *objects, **kwargs): + """Print text.""" + # pylint: disable=no-self-use + _LOGGER.warning( + "Don't use print() inside scripts. Use logger.info() instead.") diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 04edbb0e472523..332cfe7ba15fb1 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -6,7 +6,6 @@ """ import logging from datetime import timedelta -import json from urllib.error import HTTPError import voluptuous as vol @@ -16,7 +15,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_CURRENCY from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==2.0.1'] +REQUIREMENTS = ['coinmarketcap==3.0.1'] _LOGGER = logging.getLogger(__name__) @@ -103,8 +102,7 @@ def device_state_attributes(self): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._ticker = json.loads( - self.data.ticker.decode('utf-8').strip('\n '))[0] + self._ticker = self.data.ticker[0] class CoinMarketCapData(object): @@ -118,5 +116,4 @@ def __init__(self, currency): def update(self): """Get the latest data from blockchain.info.""" from coinmarketcap import Market - - self.ticker = Market().ticker(self.currency) + self.ticker = Market().ticker(self.currency, limit=1) diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index 9c5ea10e54c7df..25b7bba506c932 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['py-cpuinfo==3.2.0'] +REQUIREMENTS = ['py-cpuinfo==3.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 21b259863a739a..b28a4f4ea0df84 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -6,7 +6,6 @@ """ from datetime import timedelta import logging -from urllib.error import URLError import voluptuous as vol @@ -49,9 +48,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: _LOGGER.debug("Probing for access to ELIQ Online API") api.get_data_now(channelid=channel_id) - except URLError: + except OSError as error: _LOGGER.error("Could not access the ELIQ Online API. " - "Is the configuration valid?") + "Is the configuration valid? %s", error) return False add_devices([EliqSensor(api, channel_id, name)]) @@ -94,5 +93,6 @@ def update(self): response = self._api.get_data_now(channelid=self._channel_id) self._state = int(response.power) _LOGGER.debug("Updated power from server %d W", self._state) - except URLError: - _LOGGER.warning("Could not connect to the ELIQ Online API") + except OSError as error: + _LOGGER.warning("Could not connect to the ELIQ Online API: %s", + error) diff --git a/homeassistant/components/sensor/gitter.py b/homeassistant/components/sensor/gitter.py new file mode 100644 index 00000000000000..5a41046a948aa3 --- /dev/null +++ b/homeassistant/components/sensor/gitter.py @@ -0,0 +1,103 @@ +""" +Support for displaying details about a Gitter.im chat room. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.gitter/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_API_KEY +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['gitterpy==0.1.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_MENTION = 'mention' +ATTR_ROOM = 'room' +ATTR_USERNAME = 'username' + +CONF_ROOM = 'room' + +DEFAULT_NAME = 'Gitter messages' +DEFAULT_ROOM = 'home-assistant/home-assistant' + +ICON = 'mdi:message-settings-variant' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROOM, default=DEFAULT_ROOM): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Gitter sensor.""" + from gitterpy.client import GitterClient + from gitterpy.errors import GitterTokenError + + name = config.get(CONF_NAME) + api_key = config.get(CONF_API_KEY) + room = config.get(CONF_ROOM) + + gitter = GitterClient(api_key) + try: + username = gitter.auth.get_my_id['name'] + except GitterTokenError: + _LOGGER.error("Token is not valid") + return False + + add_devices([GitterSensor(gitter, room, name, username)], True) + + +class GitterSensor(Entity): + """Representation of a Gitter sensor.""" + + def __init__(self, data, room, name, username): + """Initialize the sensor.""" + self._name = name + self._data = data + self._room = room + self._username = username + self._state = None + self._mention = 0 + self._unit_of_measurement = 'Msg' + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_USERNAME: self._username, + ATTR_ROOM: self._room, + ATTR_MENTION: self._mention, + } + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data and updates the state.""" + data = self._data.user.unread_items(self._room) + self._mention = len(data['mention']) + self._state = len(data['chat']) diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py index 08546e3f0c8f32..fa000a75875bd3 100644 --- a/homeassistant/components/sensor/history_stats.py +++ b/homeassistant/components/sensor/history_stats.py @@ -179,7 +179,7 @@ def update(self): end = dt_util.as_utc(end) p_start = dt_util.as_utc(p_start) p_end = dt_util.as_utc(p_end) - now = dt_util.as_utc(datetime.datetime.now()) + now = datetime.datetime.now() # Compute integer timestamps start_timestamp = math.floor(dt_util.as_timestamp(start)) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 44ba7dfa753f4a..30db91bc8b0127 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -1,5 +1,5 @@ """ -The homematic sensor platform. +The HomeMatic sensor platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.homematic/ @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Homematic platform.""" + """Set up the HomeMatic platform.""" if discovery_info is None: return @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HMSensor(HMDevice): - """Represents various Homematic sensors in Home Assistant.""" + """Represents various HomeMatic sensors in Home Assistant.""" @property def state(self): @@ -89,11 +89,8 @@ def icon(self): return HM_ICON_HA_CAST.get(self._state, None) def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata.""" - # Add state to data dict + """Generate a data dictionary (self._data) from metadata.""" if self._state: - _LOGGER.debug("%s init datadict with main node %s", self._name, - self._state) self._data.update({self._state: STATE_UNKNOWN}) else: - _LOGGER.critical("Can't correctly init sensor %s", self._name) + _LOGGER.critical("Can't initialize sensor %s", self._name) diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index c1ffb01d212046..961b4692e937cb 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -141,7 +141,7 @@ def device_state_attributes(self): attr['Sensor Id'] = self._condition attr['Site Id'] = self.site.id attr['Site Name'] = self.site.name - attr['Last Update'] = self.data.lastupdate + attr['Last Update'] = self.data.data.date attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr diff --git a/homeassistant/components/sensor/openevse.py b/homeassistant/components/sensor/openevse.py index 8751dda83fdf75..f3b12506c84435 100644 --- a/homeassistant/components/sensor/openevse.py +++ b/homeassistant/components/sensor/openevse.py @@ -7,7 +7,6 @@ import logging from requests import RequestException - import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -16,8 +15,10 @@ from homeassistant.const import CONF_MONITORED_VARIABLES from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['openevsewifi==0.4'] + +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES = { 'status': ['Charging Status', None], 'charge_time': ['Charge Time Elapsed', 'minutes'], @@ -54,7 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenEVSESensor(Entity): """Implementation of an OpenEVSE sensor.""" - # pylint: disable=too-many-arguments def __init__(self, sensor_type, charger): """Initialize the sensor.""" self._name = SENSOR_TYPES[sensor_type][0] diff --git a/homeassistant/components/sensor/ripple.py b/homeassistant/components/sensor/ripple.py new file mode 100644 index 00000000000000..6f92a1a3390d7f --- /dev/null +++ b/homeassistant/components/sensor/ripple.py @@ -0,0 +1,74 @@ +""" +Support for Ripple sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.ripple/ +""" +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['python-ripple-api==0.0.2'] + +CONF_ADDRESS = 'address' +CONF_ATTRIBUTION = "Data provided by ripple.com" + +DEFAULT_NAME = 'Ripple Balance' + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ripple.com sensors.""" + address = config.get(CONF_ADDRESS) + name = config.get(CONF_NAME) + + add_devices([RippleSensor(name, address)], True) + + +class RippleSensor(Entity): + """Representation of an Ripple.com sensor.""" + + def __init__(self, name, address): + """Initialize the sensor.""" + self._name = name + self.address = address + self._state = None + self._unit_of_measurement = 'XRP' + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + + def update(self): + """Get the latest state of the sensor.""" + from pyripple import get_balance + self._state = get_balance(self.address) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 678509573882bb..5a6ce8f4bd870c 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -21,9 +21,11 @@ _LOGGER = logging.getLogger(__name__) -ATTR_MIN_VALUE = 'min_value' -ATTR_MAX_VALUE = 'max_value' +ATTR_AVERAGE_CHANGE = 'average_change' +ATTR_CHANGE = 'change' ATTR_COUNT = 'count' +ATTR_MAX_VALUE = 'max_value' +ATTR_MIN_VALUE = 'min_value' ATTR_MEAN = 'mean' ATTR_MEDIAN = 'median' ATTR_VARIANCE = 'variance' @@ -76,6 +78,7 @@ def __init__(self, hass, entity_id, name, sampling_size): self.states = deque(maxlen=self._sampling_size) self.median = self.mean = self.variance = self.stdev = 0 self.min = self.max = self.total = self.count = 0 + self.average_change = self.change = 0 @callback # pylint: disable=invalid-name @@ -130,6 +133,8 @@ def device_state_attributes(self): ATTR_STANDARD_DEVIATION: self.stdev, ATTR_TOTAL: self.total, ATTR_VARIANCE: self.variance, + ATTR_CHANGE: self.change, + ATTR_AVERAGE_CHANGE: self.average_change, } @property @@ -154,5 +159,10 @@ def async_update(self): self.total = round(sum(self.states), 2) self.min = min(self.states) self.max = max(self.states) + self.change = self.states[-1] - self.states[0] + self.average_change = self.change + if len(self.states) > 1: + self.average_change /= len(self.states) - 1 else: self.min = self.max = self.total = STATE_UNKNOWN + self.average_change = self.change = STATE_UNKNOWN diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 6d556c8232c3b0..01bdab24af9d8c 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -32,7 +32,7 @@ CONF_LOCATIONS = 'locations' CONF_STATIONS = 'stations' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(minutes=5) SENSOR_TYPES = { 'aqi': ['AQI', '0-300+', 'mdi:cloud'] @@ -63,8 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): (waqi_sensor.station_name in station_filter): dev.append(WaqiSensor(WaqiData(station, token), station)) - print("#### Locations", locations) - print("### Stations", station_filter) add_devices(dev, True) diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index 3144e812870949..4919d75e8dae90 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -9,15 +9,14 @@ import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_NAME, STATE_UNKNOWN, ATTR_ATTRIBUTION) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -REQUIREMENTS = ["yahooweather==0.8"] +REQUIREMENTS = ['yahooweather==0.8'] _LOGGER = logging.getLogger(__name__) @@ -27,18 +26,18 @@ DEFAULT_NAME = 'Yweather' -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) +SCAN_INTERVAL = timedelta(minutes=10) SENSOR_TYPES = { 'weather_current': ['Current', None], 'weather': ['Condition', None], - 'temperature': ['Temperature', "temperature"], - 'temp_min': ['Temperature min', "temperature"], - 'temp_max': ['Temperature max', "temperature"], - 'wind_speed': ['Wind speed', "speed"], - 'humidity': ['Humidity', "%"], - 'pressure': ['Pressure', "pressure"], - 'visibility': ['Visibility', "distance"], + 'temperature': ['Temperature', 'temperature'], + 'temp_min': ['Temperature min', 'temperature'], + 'temp_max': ['Temperature max', 'temperature'], + 'wind_speed': ['Wind speed', 'speed'], + 'humidity': ['Humidity', '%'], + 'pressure': ['Pressure', 'pressure'], + 'visibility': ['Visibility', 'distance'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setnup the Yahoo! weather sensor.""" + """Set up the Yahoo! weather sensor.""" from yahooweather import get_woeid, UNIT_C, UNIT_F unit = hass.config.units.temperature_unit @@ -62,14 +61,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F - SENSOR_TYPES["temperature"][1] = unit - SENSOR_TYPES["temp_min"][1] = unit - SENSOR_TYPES["temp_max"][1] = unit + SENSOR_TYPES['temperature'][1] = unit + SENSOR_TYPES['temp_min'][1] = unit + SENSOR_TYPES['temp_max'][1] = unit - # If not exists a customer woeid / calc from HA + # If not exists a customer WOEID/calculation from Home Assistant if woeid is None: woeid = get_woeid(hass.config.latitude, hass.config.longitude) - # receive a error? if woeid is None: _LOGGER.critical("Can't retrieve WOEID from yahoo!") return False @@ -77,11 +75,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): yahoo_api = YahooWeatherData(woeid, yunit) if not yahoo_api.update(): - _LOGGER.critical("Can't retrieve weather data from yahoo!") + _LOGGER.critical("Can't retrieve weather data from Yahoo!") return False if forecast >= len(yahoo_api.yahoo.Forecast): - _LOGGER.error("Yahoo! only support %d days forcast!", + _LOGGER.error("Yahoo! only support %d days forecast!", len(yahoo_api.yahoo.Forecast)) return False @@ -89,11 +87,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config[CONF_MONITORED_CONDITIONS]: dev.append(YahooWeatherSensor(yahoo_api, name, forecast, variable)) - add_devices(dev) + add_devices(dev, True) class YahooWeatherSensor(Entity): - """Implementation of an Yahoo! weather sensor.""" + """Implementation of the Yahoo! weather sensor.""" def __init__(self, weather_data, name, forecast, sensor_type): """Initialize the sensor.""" @@ -106,9 +104,6 @@ def __init__(self, weather_data, name, forecast, sensor_type): self._forecast = forecast self._code = None - # update data - self.update() - @property def name(self): """Return the name of the sensor.""" @@ -143,13 +138,13 @@ def update(self): """Get the latest data from Yahoo! and updates the states.""" self._data.update() if not self._data.yahoo.RawData: - _LOGGER.info("Don't receive weather data from yahoo!") + _LOGGER.info("Don't receive weather data from Yahoo!") return - # default code for weather image + # Default code for weather image self._code = self._data.yahoo.Now['code'] - # read data + # Read data if self._type == 'weather_current': self._state = self._data.yahoo.Now['text'] elif self._type == 'weather': @@ -174,21 +169,18 @@ def update(self): class YahooWeatherData(object): - """Handle yahoo api object and limit updates.""" + """Handle Yahoo! API object and limit updates.""" def __init__(self, woeid, temp_unit): """Initialize the data object.""" from yahooweather import YahooWeather - - # init yahoo api object self._yahoo = YahooWeather(woeid, temp_unit) @property def yahoo(self): - """Return yahoo api object.""" + """Return Yahoo! API object.""" return self._yahoo - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Get the latest data from yahoo. True is success.""" + """Get the latest data from Yahoo!.""" return self._yahoo.updateWeather() diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 0807eb617eebed..8c211a190c2514 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -72,6 +72,14 @@ persistent_notification: notification_id: description: Target ID of the notification, will replace a notification with the same Id. [Optional] example: 1234 + + dismiss: + description: Remove a notification from the frontend + + fields: + notification_id: + description: Target ID of the notification, which should be removed. [Required] + example: 1234 homematic: virtualkey: diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index a95f414bb1b0ed..e67f293525c6c5 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -1,5 +1,5 @@ """ -Support for Homematic switches. +Support for HomeMatic switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.homematic/ @@ -15,7 +15,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Homematic switch platform.""" + """Set up the HomeMatic switch platform.""" if discovery_info is None: return @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HMSwitch(HMDevice, SwitchDevice): - """Representation of a Homematic switch.""" + """Representation of a HomeMatic switch.""" @property def is_on(self): @@ -59,8 +59,7 @@ def turn_off(self, **kwargs): self._hmdevice.off(self._channel) def _init_data_struct(self): - """Generate a data dict (self._data) from the Homematic metadata.""" - # Use STATE + """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index 73183da11288cb..6acbea8e95ceac 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -8,9 +8,7 @@ from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['https://github.com/Klikini/rachiopy' - '/archive/2c8996fcfa97a9f361a789e0c998797ed2805281.zip' - '#rachiopy==0.1.1'] +REQUIREMENTS = ['rachiopy==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -19,7 +17,7 @@ CONF_MANUAL_RUN_MINS = 'manual_run_mins' DEFAULT_MANUAL_RUN_MINS = 10 -MIN_UPDATE_INTERVAL = timedelta(minutes=5) +MIN_UPDATE_INTERVAL = timedelta(seconds=30) MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -29,7 +27,6 @@ }) -# noinspection PyUnusedLocal def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the component.""" # Get options @@ -217,14 +214,11 @@ def update(self): def turn_on(self): """Start the zone.""" - # Convert minutes to seconds - seconds = self._manual_run_secs * 60 - # Stop other zones first self.turn_off() - _LOGGER.info("Watering %s for %d sec", self.name, seconds) - self.rachio.zone.start(self.zone_id, seconds) + _LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs) + self.rachio.zone.start(self.zone_id, self._manual_run_secs) def turn_off(self): """Stop all zones.""" diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 419f4028def57e..31d4f0f3e06172 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -12,41 +12,48 @@ import voluptuous as vol from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT) +from homeassistant.const import ( + CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT, CONF_METHOD) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import Template +_LOGGER = logging.getLogger(__name__) + CONF_BODY_OFF = 'body_off' CONF_BODY_ON = 'body_on' CONF_IS_ON_TEMPLATE = 'is_on_template' +DEFAULT_METHOD = 'post' DEFAULT_BODY_OFF = Template('OFF') DEFAULT_BODY_ON = Template('ON') DEFAULT_NAME = 'REST Switch' DEFAULT_TIMEOUT = 10 +SUPPORT_REST_METHODS = ['post', 'put'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_IS_ON_TEMPLATE): cv.template, + vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): + vol.All(vol.Lower, vol.In(SUPPORT_REST_METHODS)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) -_LOGGER = logging.getLogger(__name__) - -# pylint: disable=unused-argument, +# pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the RESTful switch.""" - name = config.get(CONF_NAME) - resource = config.get(CONF_RESOURCE) - body_on = config.get(CONF_BODY_ON) body_off = config.get(CONF_BODY_OFF) + body_on = config.get(CONF_BODY_ON) is_on_template = config.get(CONF_IS_ON_TEMPLATE) + method = config.get(CONF_METHOD) + name = config.get(CONF_NAME) + resource = config.get(CONF_RESOURCE) websession = async_get_clientsession(hass) if is_on_template is not None: @@ -74,20 +81,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return False async_add_devices( - [RestSwitch(hass, name, resource, body_on, body_off, + [RestSwitch(hass, name, resource, method, body_on, body_off, is_on_template, timeout)]) class RestSwitch(SwitchDevice): """Representation of a switch that can be toggled using REST.""" - def __init__(self, hass, name, resource, body_on, body_off, + def __init__(self, hass, name, resource, method, body_on, body_off, is_on_template, timeout): """Initialize the REST switch.""" self._state = None self.hass = hass self._name = name self._resource = resource + self._method = method self._body_on = body_on self._body_off = body_off self._is_on_template = is_on_template @@ -111,7 +119,7 @@ def async_turn_on(self, **kwargs): try: with async_timeout.timeout(self._timeout, loop=self.hass.loop): - request = yield from websession.post( + request = yield from getattr(websession, self._method)( self._resource, data=bytes(body_on_t, 'utf-8')) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn on %s", self._resource) @@ -131,7 +139,7 @@ def async_turn_off(self, **kwargs): try: with async_timeout.timeout(self._timeout, loop=self.hass.loop): - request = yield from websession.post( + request = yield from getattr(websession, self._method)( self._resource, data=bytes(body_off_t, 'utf-8')) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while turn off %s", self._resource) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index b59c9ae84e8fb6..434c03fb181a76 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -543,9 +543,10 @@ def _get_message_data(self, msg_data): data = { ATTR_USER_ID: msg_data['from']['id'], - ATTR_FROM_FIRST: msg_data['from']['first_name'], - ATTR_FROM_LAST: msg_data['from']['last_name'] + ATTR_FROM_FIRST: msg_data['from']['first_name'] } + if 'last_name' in msg_data['from']: + data[ATTR_FROM_LAST] = msg_data['from']['last_name'] if 'chat' in msg_data: data[ATTR_CHAT_ID] = msg_data['chat']['id'] diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index e24ada194efe96..4e26dfe323800d 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -79,7 +79,7 @@ def stop_polling(self): def get_updates(self, offset): """Bypass the default long polling method to enable asyncio.""" resp = None - _json = [] # The actual value to be returned. + _json = {'result': [], 'ok': True} # Empty result. if offset: self.post_data['offset'] = offset @@ -89,9 +89,11 @@ def get_updates(self, offset): self.update_url, data=self.post_data, headers={'connection': 'keep-alive'} ) - if resp.status != 200: + if resp.status == 200: + _json = yield from resp.json() + else: _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): @@ -106,9 +108,13 @@ def get_updates(self, offset): 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) + _updates = _updates.get('result') + if _updates is None: + _LOGGER.error("Incorrect result received.") + else: + for update in _updates: + self.update_id = update['update_id'] + 1 + self.process_message(update) @asyncio.coroutine def check_incoming(self): diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 001105374d7c17..00b2421bdd57b2 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -20,7 +20,7 @@ EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.31'] +REQUIREMENTS = ['pyvera==0.2.32'] _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ def setup(hass, base_config): def stop_subscription(event): """Shutdown Vera subscriptions and subscription thread on exit.""" - _LOGGER.info("Shutting down subscriptions.") + _LOGGER.info("Shutting down subscriptions") VERA_CONTROLLER.stop() config = base_config.get(DOMAIN) @@ -100,7 +100,6 @@ def stop_subscription(event): return True -# pylint: disable=too-many-return-statements def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" import pyvera as veraApi diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py index da419ff0ab3026..a1c2469d3b98c0 100644 --- a/homeassistant/components/volvooncall.py +++ b/homeassistant/components/volvooncall.py @@ -26,6 +26,7 @@ CONF_UPDATE_INTERVAL = 'update_interval' MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) +CONF_SERVICE_URL = 'service_url' RESOURCES = {'position': ('device_tracker',), 'lock': ('lock', 'Lock'), @@ -51,16 +52,19 @@ {cv.slug: cv.string}), vol.Optional(CONF_RESOURCES): vol.All( cv.ensure_list, [vol.In(RESOURCES)]), + vol.Optional(CONF_SERVICE_URL): cv.string, }), }, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up the Volvo On Call component.""" + from volvooncall import DEFAULT_SERVICE_URL from volvooncall import Connection connection = Connection( config[DOMAIN].get(CONF_USERNAME), - config[DOMAIN].get(CONF_PASSWORD)) + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_SERVICE_URL, DEFAULT_SERVICE_URL)) interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 9b0daf10efbc05..17a47fbc52238b 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,16 +22,17 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_CONDITION_CLASS = 'condition_class' +ATTR_FORECAST = 'forecast' +ATTR_FORECAST_TEMP = 'temperature' +ATTR_FORECAST_TIME = 'datetime' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' ATTR_WEATHER_OZONE = 'ozone' ATTR_WEATHER_PRESSURE = 'pressure' ATTR_WEATHER_TEMPERATURE = 'temperature' +ATTR_WEATHER_VISIBILITY = 'visibility' ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' -ATTR_FORECAST = 'forecast' -ATTR_FORECAST_TEMP = 'temperature' -ATTR_FORECAST_TIME = 'datetime' @asyncio.coroutine @@ -45,7 +46,7 @@ def async_setup(hass, config): # pylint: disable=no-member, no-self-use class WeatherEntity(Entity): - """ABC for a weather data.""" + """ABC for weather data.""" @property def temperature(self): @@ -87,6 +88,11 @@ def attribution(self): """Return the attribution.""" return None + @property + def visibility(self): + """Return the visibility.""" + return None + @property def forecast(self): """Return the forecast.""" @@ -116,6 +122,10 @@ def state_attributes(self): if wind_speed is not None: data[ATTR_WEATHER_WIND_SPEED] = wind_speed + visibility = self.visibility + if visibility is not None: + data[ATTR_WEATHER_VISIBILITY] = visibility + attribution = self.attribution if attribution is not None: data[ATTR_WEATHER_ATTRIBUTION] = attribution diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py new file mode 100644 index 00000000000000..0e216273d65a25 --- /dev/null +++ b/homeassistant/components/weather/yweather.py @@ -0,0 +1,186 @@ +""" +Support for the Yahoo! Weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.yweather/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP) +from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN) + +REQUIREMENTS = ["yahooweather==0.8"] + +_LOGGER = logging.getLogger(__name__) + +ATTR_FORECAST_CONDITION = 'condition' +ATTRIBUTION = "Weather details provided by Yahoo! Inc." + +CONF_FORECAST = 'forecast' +CONF_WOEID = 'woeid' + +DEFAULT_NAME = 'Yweather' + +SCAN_INTERVAL = timedelta(minutes=10) + +CONDITION_CLASSES = { + 'cloudy': [26, 27, 28, 29, 30], + 'fog': [19, 20, 21, 22, 23], + 'hail': [17, 18, 35], + 'lightning': [37], + 'lightning-rainy': [38, 39], + 'partlycloudy': [44], + 'pouring': [40, 45], + 'rainy': [9, 11, 12], + 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], + 'snowy-rainy': [5, 6, 7, 10, 46, 47], + 'sunny': [32], + 'windy': [24], + 'windy-variant': [], + 'exceptional': [0, 1, 2, 3, 4, 25, 36], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_WOEID, default=None): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_FORECAST, default=0): + vol.All(vol.Coerce(int), vol.Range(min=0, max=5)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Yahoo! weather platform.""" + from yahooweather import get_woeid, UNIT_C, UNIT_F + + unit = hass.config.units.temperature_unit + woeid = config.get(CONF_WOEID) + forecast = config.get(CONF_FORECAST) + name = config.get(CONF_NAME) + + yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F + + # If not exists a customer WOEID/calculation from Home Assistant + if woeid is None: + woeid = get_woeid(hass.config.latitude, hass.config.longitude) + if woeid is None: + _LOGGER.warning("Can't retrieve WOEID from Yahoo!") + return False + + yahoo_api = YahooWeatherData(woeid, yunit) + + if not yahoo_api.update(): + _LOGGER.critical("Can't retrieve weather data from Yahoo!") + return False + + if forecast >= len(yahoo_api.yahoo.Forecast): + _LOGGER.error("Yahoo! only support %d days forecast", + len(yahoo_api.yahoo.Forecast)) + return False + + add_devices([YahooWeatherWeather(yahoo_api, name, forecast)], True) + + +class YahooWeatherWeather(WeatherEntity): + """Representation of Yahoo! weather data.""" + + def __init__(self, weather_data, name, forecast): + """Initialize the sensor.""" + self._name = name + self._data = weather_data + self._forecast = forecast + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def condition(self): + """Return the current condition.""" + try: + return [k for k, v in CONDITION_CLASSES.items() if + int(self._data.yahoo.Now['code']) in v][0] + except IndexError: + return STATE_UNKNOWN + + @property + def temperature(self): + """Return the temperature.""" + return self._data.yahoo.Now['temp'] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the pressure.""" + return self._data.yahoo.Atmosphere['pressure'] + + @property + def humidity(self): + """Return the humidity.""" + return self._data.yahoo.Atmosphere['humidity'] + + @property + def visibility(self): + """Return the visibility.""" + return self._data.yahoo.Atmosphere['visibility'] + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._data.yahoo.Wind['speed'] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + try: + forecast_condition = \ + [k for k, v in CONDITION_CLASSES.items() if + int(self._data.yahoo.Forecast[self._forecast]['code']) + in v][0] + except IndexError: + return STATE_UNKNOWN + + return [{ + ATTR_FORECAST_CONDITION: forecast_condition, + ATTR_FORECAST_TEMP: + self._data.yahoo.Forecast[self._forecast]['high'], + }] + + def update(self): + """Get the latest data from Yahoo! and updates the states.""" + self._data.update() + if not self._data.yahoo.RawData: + _LOGGER.info("Don't receive weather data from Yahoo!") + return + + +class YahooWeatherData(object): + """Handle the Yahoo! API object and limit updates.""" + + def __init__(self, woeid, temp_unit): + """Initialize the data object.""" + from yahooweather import YahooWeather + self._yahoo = YahooWeather(woeid, temp_unit) + + @property + def yahoo(self): + """Return Yahoo! API object.""" + return self._yahoo + + def update(self): + """Get the latest data from Yahoo!.""" + return self._yahoo.updateWeather() diff --git a/homeassistant/const.py b/homeassistant/const.py index bb0d491ab1f2cc..c50fa3034c613a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -142,6 +142,7 @@ CONF_SENSOR_CLASS = 'sensor_class' CONF_SENSOR_TYPE = 'sensor_type' CONF_SENSORS = 'sensors' +CONF_SLAVE = 'slave' CONF_SSL = 'ssl' CONF_STATE = 'state' CONF_STRUCTURE = 'structure' diff --git a/homeassistant/core.py b/homeassistant/core.py index 8f6c7df1f2177a..5a9b185372e43d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -113,7 +113,13 @@ def __init__(self, loop=None): else: self.loop = loop or asyncio.get_event_loop() - self.executor = ThreadPoolExecutor(max_workers=EXECUTOR_POOL_SIZE) + executor_opts = { + 'max_workers': EXECUTOR_POOL_SIZE + } + if sys.version_info[:2] >= (3, 6): + executor_opts['thread_name_prefix'] = 'SyncWorker' + + self.executor = ThreadPoolExecutor(**executor_opts) self.loop.set_default_executor(self.executor) self.loop.set_exception_handler(async_loop_exception_handler) self._pending_tasks = [] diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 9aac258684f85b..a8b18351021c21 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -125,7 +125,7 @@ def async_aiohttp_proxy_stream(hass, request, stream, content_type, @callback # pylint: disable=invalid-name def _async_register_clientsession_shutdown(hass, clientsession): - """Register ClientSession close on homeassistant shutdown. + """Register ClientSession close on Home Assistant shutdown. This method must be run in the event loop. """ diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 7d3d7d0e823af5..c3e4b2b494222a 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -1,7 +1,7 @@ """Helper methods to help with platform discovery. There are two different types of discoveries that can be fired/listened for. - - listen/discover is for services. These are targetted at a component. + - listen/discover is for services. These are targeted at a component. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index d5fbadec88301b..a426f2de855fbd 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -10,7 +10,7 @@ def dispatcher_connect(hass, signal, target): - """Connect a callable function to a singal.""" + """Connect a callable function to a signal.""" async_unsub = run_callback_threadsafe( hass.loop, async_dispatcher_connect, hass, signal, target).result() @@ -23,7 +23,7 @@ def remove_dispatcher(): @callback def async_dispatcher_connect(hass, signal, target): - """Connect a callable function to a singal. + """Connect a callable function to a signal. This method must be run in the event loop. """ diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 58c12bf043e695..08acfe668516b7 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -151,8 +151,10 @@ def _async_setup_platform(self, platform_type, platform_config, entity_platform.async_schedule_add_entities, discovery_info ) else: - task = self.hass.async_add_job( - platform.setup_platform, self.hass, platform_config, + # This should not be replaced with hass.async_add_job because + # we don't want to track this task in case it blocks startup. + task = self.hass.loop.run_in_executor( + None, platform.setup_platform, self.hass, platform_config, entity_platform.schedule_add_entities, discovery_info ) yield from asyncio.wait_for( diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 157225c9903a9b..5ad4f06fdf1a76 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -9,7 +9,7 @@ @callback def get_astral_location(hass): - """Get an astral location for the current hass configuration.""" + """Get an astral location for the current Home Assistant configuration.""" from astral import Location latitude = hass.config.latitude diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 396b8a63601d68..d76816cfbb8800 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -264,6 +264,13 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[int, int, int]: return (int(fHSV[0]*65536), int(fHSV[1]*255), int(fHSV[2]*255)) +# pylint: disable=invalid-sequence-index +def color_hsv_to_RGB(iH: int, iS: int, iV: int) -> Tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + fRGB = colorsys.hsv_to_rgb(iH/65536, iS/255, iV/255) + return (int(fRGB[0]*255), int(fRGB[1]*255), int(fRGB[2]*255)) + + # pylint: disable=invalid-sequence-index def color_xy_to_hs(vX: float, vY: float) -> Tuple[int, int]: """Convert an xy color to its hs representation.""" diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index ed533a3872fd68..a5a863b0880806 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -11,6 +11,7 @@ import pkg_resources _LOGGER = logging.getLogger(__name__) + INSTALL_LOCK = threading.Lock() @@ -26,7 +27,7 @@ def install_package(package: str, upgrade: bool=True, if check_package_exists(package, target): return True - _LOGGER.info('Attempting install of %s', package) + _LOGGER.info("Attempting install of %s", package) args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] if upgrade: args.append('--upgrade') @@ -39,7 +40,7 @@ def install_package(package: str, upgrade: bool=True, process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) _, stderr = process.communicate() if process.returncode != 0: - _LOGGER.error('Unable to install package %s: %s', + _LOGGER.error("Unable to install package %s: %s", package, stderr.decode('utf-8').lstrip().strip()) return False diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index ae3630c27a9ce3..31b76365da4f01 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -2,14 +2,14 @@ import logging from numbers import Number + from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_CENTIMETERS, LENGTH_METERS, LENGTH_KILOMETERS, LENGTH_INCHES, LENGTH_FEET, LENGTH_YARD, LENGTH_MILES, VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, MASS_GRAMS, MASS_KILOGRAMS, MASS_OUNCES, MASS_POUNDS, - CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL, LENGTH, MASS, VOLUME, TEMPERATURE, - UNIT_NOT_RECOGNIZED_TEMPLATE) + CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH, MASS, VOLUME, + TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE) from homeassistant.util import temperature as temperature_util from homeassistant.util import distance as distance_util diff --git a/requirements_all.txt b/requirements_all.txt index cd8188fcc0deb5..50d099ae65ba7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.4.7 +aiolifx==0.4.8 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.4 @@ -121,7 +121,7 @@ buienradar==0.4 ciscosparkapi==0.4.2 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==2.0.1 +coinmarketcap==3.0.1 # homeassistant.scripts.check_config colorlog>2.1,<3 @@ -144,7 +144,7 @@ datapoint==0.4.3 # decora==0.4 # homeassistant.components.media_player.denonavr -denonavr==0.4.2 +denonavr==0.4.4 # homeassistant.components.media_player.directv directpy==0.1 @@ -231,6 +231,9 @@ gTTS-token==1.1.1 # homeassistant.components.device_tracker.bluetooth_le_tracker # gattlib==0.20150805 +# homeassistant.components.sensor.gitter +gitterpy==0.1.5 + # homeassistant.components.notify.gntp gntp==1.0.3 @@ -264,9 +267,6 @@ hikvision==0.4 # homeassistant.components.binary_sensor.workday holidays==0.8.1 -# homeassistant.components.switch.rachio -https://github.com/Klikini/rachiopy/archive/2c8996fcfa97a9f361a789e0c998797ed2805281.zip#rachiopy==0.1.1 - # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.4.zip#pyW215==0.4 @@ -379,7 +379,8 @@ mutagen==1.37.0 myusps==1.1.1 # homeassistant.components.media_player.nad -nad_receiver==0.0.5 +# homeassistant.components.media_player.nadtcp +nad_receiver==0.0.6 # homeassistant.components.discovery netdisco==1.0.1 @@ -387,7 +388,7 @@ netdisco==1.0.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 -# homeassistant.components.opencv +# homeassistant.components.image_processing.opencv numpy==1.12.0 # homeassistant.components.google @@ -399,14 +400,11 @@ oemthermostat==1.1 # homeassistant.components.media_player.onkyo onkyo-eiscp==1.1 -# homeassistant.components.opencv -# opencv-python==3.2.0.6 - # homeassistant.components.sensor.openevse openevsewifi==0.4 # homeassistant.components.media_player.openhome -openhomedevice==0.4.0 +openhomedevice==0.4.2 # homeassistant.components.switch.orvibo orvibo==1.1.1 @@ -475,7 +473,7 @@ pwaqi==3.0 pwmled==1.1.1 # homeassistant.components.sensor.cpuspeed -py-cpuinfo==3.2.0 +py-cpuinfo==3.3.0 # homeassistant.components.hdmi_cec pyCEC==0.4.13 @@ -538,6 +536,9 @@ pyenvisalink==2.1 # homeassistant.components.sensor.fido pyfido==1.0.1 +# homeassistant.components.climate.flexit +pyflexit==0.3 + # homeassistant.components.ifttt pyfttt==0.3 @@ -551,7 +552,7 @@ pyharmony==1.0.16 pyhik==0.1.2 # homeassistant.components.homematic -pyhomematic==0.1.27 +pyhomematic==0.1.28 # homeassistant.components.sensor.hydroquebec pyhydroquebec==1.1.0 @@ -635,7 +636,7 @@ pysensibo==1.0.1 pyserial==3.1.1 # homeassistant.components.lock.sesame -pysesame==0.0.2 +pysesame==0.1.0 # homeassistant.components.sensor.sma pysma==0.1.3 @@ -698,6 +699,9 @@ python-nmap==0.6.1 # homeassistant.components.notify.pushover python-pushover==0.2 +# homeassistant.components.sensor.ripple +python-ripple-api==0.0.2 + # homeassistant.components.media_player.roku python-roku==3.1.3 @@ -732,7 +736,7 @@ pyunifi==2.12 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.31 +pyvera==0.2.32 # homeassistant.components.notify.html5 pywebpush==1.0.4 @@ -746,12 +750,18 @@ pyzabbix==0.7.4 # homeassistant.components.sensor.qnap qnapstats==0.2.4 +# homeassistant.components.switch.rachio +rachiopy==0.1.1 + # homeassistant.components.climate.radiotherm radiotherm==1.2 # homeassistant.components.raspihats # raspihats==2.2.1 +# homeassistant.components.python_script +restrictedpython==4.0a2 + # homeassistant.components.rflink rflink==0.0.34 @@ -895,6 +905,7 @@ xmltodict==0.11.0 yahoo-finance==1.4.0 # homeassistant.components.sensor.yweather +# homeassistant.components.weather.yweather yahooweather==0.8 # homeassistant.components.light.yeelight diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e70a6c5be38b6..21cfb74380fdfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,6 +100,9 @@ python-forecastio==1.3.5 # homeassistant.components.notify.html5 pywebpush==1.0.4 +# homeassistant.components.python_script +restrictedpython==4.0a2 + # homeassistant.components.rflink rflink==0.0.34 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 614411fbde2738..d25c1f887804a3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -65,6 +65,7 @@ 'gTTS-token', 'pywebpush', 'PyJWT', + 'restrictedpython', ) IGNORE_PACKAGES = ( diff --git a/tests/components/fan/test_demo.py b/tests/components/fan/test_demo.py index 078dd56bf1be4e..0d066af8cf6001 100644 --- a/tests/components/fan/test_demo.py +++ b/tests/components/fan/test_demo.py @@ -4,11 +4,12 @@ from homeassistant.setup import setup_component from homeassistant.components import fan -from homeassistant.components.fan.demo import FAN_ENTITY_ID from homeassistant.const import STATE_OFF, STATE_ON from tests.common import get_test_home_assistant +FAN_ENTITY_ID = 'fan.living_room_fan' + class TestDemoFan(unittest.TestCase): """Test the fan demo platform.""" @@ -55,6 +56,18 @@ def test_turn_off(self): self.hass.block_till_done() self.assertEqual(STATE_OFF, self.get_entity().state) + def test_turn_off_without_entity_id(self): + """Test turning off all fans.""" + self.assertEqual(STATE_OFF, self.get_entity().state) + + fan.turn_on(self.hass, FAN_ENTITY_ID) + self.hass.block_till_done() + self.assertNotEqual(STATE_OFF, self.get_entity().state) + + fan.turn_off(self.hass) + self.hass.block_till_done() + self.assertEqual(STATE_OFF, self.get_entity().state) + def test_set_direction(self): """Test setting the direction of the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 2d3a752fafa6bc..b4576b174d6574 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -71,6 +71,15 @@ def test_turn_off(self): self.hass.block_till_done() self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) + def test_turn_off_without_entity_id(self): + """Test light turn off all lights.""" + light.turn_on(self.hass, ENTITY_LIGHT) + self.hass.block_till_done() + self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) + light.turn_off(self.hass) + self.hass.block_till_done() + self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) + @asyncio.coroutine def test_restore_state(hass): diff --git a/tests/components/sensor/test_statistics.py b/tests/components/sensor/test_statistics.py index d51c88b85d5428..753b18f137fa90 100644 --- a/tests/components/sensor/test_statistics.py +++ b/tests/components/sensor/test_statistics.py @@ -22,6 +22,8 @@ def setup_method(self, method): self.median = round(statistics.median(self.values), 2) self.deviation = round(statistics.stdev(self.values), 2) self.variance = round(statistics.variance(self.values), 2) + self.change = self.values[-1] - self.values[0] + self.average_change = self.change / (len(self.values) - 1) def teardown_method(self, method): """Stop everything that was started.""" @@ -74,6 +76,9 @@ def test_sensor_source(self): self.assertEqual(self.count, state.attributes.get('count')) self.assertEqual(self.total, state.attributes.get('total')) self.assertEqual('°C', state.attributes.get('unit_of_measurement')) + self.assertEqual(self.change, state.attributes.get('change')) + self.assertEqual(self.average_change, + state.attributes.get('average_change')) def test_sampling_size(self): """Test rotation.""" diff --git a/tests/components/switch/test_rest.py b/tests/components/switch/test_rest.py index 5a9ce679edfc05..97911fccbfd6a0 100644 --- a/tests/components/switch/test_rest.py +++ b/tests/components/switch/test_rest.py @@ -97,11 +97,13 @@ def setup_method(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.name = 'foo' + self.method = 'post' self.resource = 'http://localhost/' self.body_on = Template('on', self.hass) self.body_off = Template('off', self.hass) - self.switch = rest.RestSwitch(self.hass, self.name, self.resource, - self.body_on, self.body_off, None, 10) + self.switch = rest.RestSwitch( + self.hass, self.name, self.resource, self.method, self.body_on, + self.body_off, None, 10) def teardown_method(self): """Stop everything that was started.""" diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 76d582c88560e7..65ce95ee8e99b2 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -117,3 +117,46 @@ def test_bad_request_notext(self, mock_logger, mock_call): conversation.DOMAIN, 'process', event_data, True)) self.assertTrue(mock_logger.called) self.assertFalse(mock_call.called) + + +class TestConfiguration(unittest.TestCase): + """Test the conversation configuration component.""" + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.assertTrue(setup_component(self.hass, conversation.DOMAIN, { + conversation.DOMAIN: { + 'test_2': { + 'sentence': 'switch boolean', + 'action': { + 'service': 'input_boolean.toggle' + } + } + } + })) + + # pylint: disable=invalid-name + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_custom(self): + """Setup and perform good turn on requests.""" + calls = [] + + @callback + def record_call(service): + """Recorder for a call.""" + calls.append(service) + + self.hass.services.register('input_boolean', 'toggle', record_call) + + event_data = {conversation.ATTR_TEXT: 'switch boolean'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + + call = calls[-1] + self.assertEqual('input_boolean', call.domain) + self.assertEqual('toggle', call.service) diff --git a/tests/components/test_persistent_notification.py b/tests/components/test_persistent_notification.py index 55c7867685843b..75caae0015c7a7 100644 --- a/tests/components/test_persistent_notification.py +++ b/tests/components/test_persistent_notification.py @@ -64,3 +64,16 @@ def test_create_template_error(self): state = self.hass.states.get(entity_ids[0]) assert state.state == '{{ message + 1 }}' assert state.attributes.get('title') == '{{ title + 1 }}' + + def test_dismiss_notification(self): + """Ensure removal of specific notification.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'test', notification_id='Beer 2') + self.hass.block_till_done() + + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 1 + pn.dismiss(self.hass, notification_id='Beer 2') + self.hass.block_till_done() + + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 diff --git a/tests/components/test_python_script.py b/tests/components/test_python_script.py new file mode 100644 index 00000000000000..31ee24587e0414 --- /dev/null +++ b/tests/components/test_python_script.py @@ -0,0 +1,123 @@ +"""Test the python_script component.""" +import asyncio +import logging +from unittest.mock import patch, mock_open + +from homeassistant.setup import async_setup_component +from homeassistant.components.python_script import execute + + +@asyncio.coroutine +def test_setup(hass): + """Test we can discover scripts.""" + scripts = [ + '/some/config/dir/python_scripts/hello.py', + '/some/config/dir/python_scripts/world_beer.py' + ] + with patch('homeassistant.components.python_script.os.path.isdir', + return_value=True), \ + patch('homeassistant.components.python_script.glob.iglob', + return_value=scripts): + res = yield from async_setup_component(hass, 'python_script', {}) + + assert res + assert hass.services.has_service('python_script', 'hello') + assert hass.services.has_service('python_script', 'world_beer') + + with patch('homeassistant.components.python_script.open', + mock_open(read_data='fake source'), create=True), \ + patch('homeassistant.components.python_script.execute') as mock_ex: + yield from hass.services.async_call( + 'python_script', 'hello', {'some': 'data'}, blocking=True) + + assert len(mock_ex.mock_calls) == 1 + hass, script, source, data = mock_ex.mock_calls[0][1] + + assert hass is hass + assert script == 'hello.py' + assert source == 'fake source' + assert data == {'some': 'data'} + + +@asyncio.coroutine +def test_setup_fails_on_no_dir(hass, caplog): + """Test we fail setup when no dir found.""" + with patch('homeassistant.components.python_script.os.path.isdir', + return_value=False): + res = yield from async_setup_component(hass, 'python_script', {}) + + assert not res + assert 'Folder python_scripts not found in config folder' in caplog.text + + +@asyncio.coroutine +def test_execute_with_data(hass, caplog): + """Test executing a script.""" + caplog.set_level(logging.WARNING) + source = """ +hass.states.set('test.entity', data.get('name', 'not set')) + """ + + hass.async_add_job(execute, hass, 'test.py', source, {'name': 'paulus'}) + yield from hass.async_block_till_done() + + assert hass.states.is_state('test.entity', 'paulus') + + # No errors logged = good + assert caplog.text == '' + + +@asyncio.coroutine +def test_execute_warns_print(hass, caplog): + """Test print triggers warning.""" + caplog.set_level(logging.WARNING) + source = """ +print("This triggers warning.") + """ + + hass.async_add_job(execute, hass, 'test.py', source, {}) + yield from hass.async_block_till_done() + + assert "Don't use print() inside scripts." in caplog.text + + +@asyncio.coroutine +def test_execute_logging(hass, caplog): + """Test logging works.""" + caplog.set_level(logging.INFO) + source = """ +logger.info('Logging from inside script') + """ + + hass.async_add_job(execute, hass, 'test.py', source, {}) + yield from hass.async_block_till_done() + + assert "Logging from inside script" in caplog.text + + +@asyncio.coroutine +def test_execute_compile_error(hass, caplog): + """Test compile error logs error.""" + caplog.set_level(logging.ERROR) + source = """ +this is not valid Python + """ + + hass.async_add_job(execute, hass, 'test.py', source, {}) + yield from hass.async_block_till_done() + + assert "Error loading script test.py" in caplog.text + + +@asyncio.coroutine +def test_execute_runtime_error(hass, caplog): + """Test compile error logs error.""" + caplog.set_level(logging.ERROR) + source = """ +raise Exception('boom') + """ + + hass.async_add_job(execute, hass, 'test.py', source, {}) + yield from hass.async_block_till_done() + + assert "Error executing script test.py" in caplog.text diff --git a/tests/testing_config/automations.yaml b/tests/testing_config/automations.yaml new file mode 100644 index 00000000000000..0637a088a01e8d --- /dev/null +++ b/tests/testing_config/automations.yaml @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 43b5904a895a9a..dfb2cd0733c565 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -56,6 +56,23 @@ def test_color_RGB_to_hsv(self): self.assertEqual((0, 255, 255), color_util.color_RGB_to_hsv(255, 0, 0)) + def test_color_hsv_to_RGB(self): + """Test color_RGB_to_hsv.""" + self.assertEqual((0, 0, 0), + color_util.color_hsv_to_RGB(0, 0, 0)) + + self.assertEqual((255, 255, 255), + color_util.color_hsv_to_RGB(0, 0, 255)) + + self.assertEqual((0, 0, 255), + color_util.color_hsv_to_RGB(43690, 255, 255)) + + self.assertEqual((0, 255, 0), + color_util.color_hsv_to_RGB(21845, 255, 255)) + + self.assertEqual((255, 0, 0), + color_util.color_hsv_to_RGB(0, 255, 255)) + def test_color_xy_to_hs(self): """Test color_xy_to_hs.""" self.assertEqual((8609, 255),