diff --git a/.coveragerc b/.coveragerc index b091b3765798b..4f23fd9d8bf3e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -263,6 +263,7 @@ omit = homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/egardia.py + homeassistant/components/alarm_control_panel/ialarm.py homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py @@ -284,9 +285,9 @@ omit = homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py - homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/onvif.py homeassistant/components/camera/ring.py + homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py homeassistant/components/camera/yi.py homeassistant/components/climate/ephember.py @@ -294,6 +295,7 @@ omit = homeassistant/components/climate/flexit.py homeassistant/components/climate/heatmiser.py homeassistant/components/climate/homematic.py + homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py homeassistant/components/climate/oem.py homeassistant/components/climate/proliphix.py @@ -331,10 +333,10 @@ omit = homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/swisscom.py - homeassistant/components/device_tracker/thomson.py - homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tado.py + homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tile.py + homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/ubus.py @@ -352,8 +354,8 @@ omit = homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py homeassistant/components/light/avion.py - homeassistant/components/light/blinkt.py homeassistant/components/light/blinksticklight.py + homeassistant/components/light/blinkt.py homeassistant/components/light/decora.py homeassistant/components/light/decora_wifi.py homeassistant/components/light/flux_led.py @@ -364,8 +366,8 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py homeassistant/components/light/osramlightify.py - homeassistant/components/light/rpi_gpio_pwm.py homeassistant/components/light/piglow.py + homeassistant/components/light/rpi_gpio_pwm.py homeassistant/components/light/sensehat.py homeassistant/components/light/tikteck.py homeassistant/components/light/tplink.py @@ -376,9 +378,9 @@ omit = homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/lock/lockitron.py homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py - homeassistant/components/lock/lockitron.py homeassistant/components/lock/sesame.py homeassistant/components/media_extractor.py homeassistant/components/media_player/anthemav.py @@ -472,6 +474,7 @@ omit = homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py + homeassistant/components/sensor/alpha_vantage.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py @@ -482,8 +485,8 @@ omit = homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/buienradar.py - homeassistant/components/sensor/citybikes.py homeassistant/components/sensor/cert_expiry.py + homeassistant/components/sensor/citybikes.py homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/crimereports.py @@ -621,8 +624,8 @@ omit = homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py - homeassistant/components/switch/tplink.py homeassistant/components/switch/telnet.py + homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* @@ -631,7 +634,9 @@ omit = homeassistant/components/tts/baidu.py homeassistant/components/tts/microsoft.py homeassistant/components/tts/picotts.py + homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/roomba.py + homeassistant/components/vacuum/xiaomi_miio.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/metoffice.py @@ -640,7 +645,6 @@ omit = homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py homeassistant/components/zwave/util.py - homeassistant/components/vacuum/mqtt.py [report] # Regexes for lines to exclude from consideration diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index aa90fe1f88976..c080a136c080e 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -4,30 +4,45 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ +import datetime import homeassistant.components.alarm_control_panel.manual as manual from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, CONF_PENDING_TIME) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME, + CONF_PENDING_TIME, CONF_TRIGGER_TIME) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo alarm control panel platform.""" add_devices([ - manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, { + manual.ManualAlarm(hass, 'Alarm', '1234', None, False, { STATE_ALARM_ARMED_AWAY: { - CONF_PENDING_TIME: 5 + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_HOME: { - CONF_PENDING_TIME: 5 + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_NIGHT: { - CONF_PENDING_TIME: 5 + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_DISARMED: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_CUSTOM_BYPASS: { - CONF_PENDING_TIME: 5 + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_TRIGGERED: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, }), ]) diff --git a/homeassistant/components/alarm_control_panel/ialarm.py b/homeassistant/components/alarm_control_panel/ialarm.py new file mode 100644 index 0000000000000..3fb6e2dcb903e --- /dev/null +++ b/homeassistant/components/alarm_control_panel/ialarm.py @@ -0,0 +1,107 @@ +""" +Interfaces with iAlarm control panels. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.ialarm/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, CONF_HOST, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_NAME) + +REQUIREMENTS = ['pyialarm==0.2'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'iAlarm' + + +def no_application_protocol(value): + """Validate that value is without the application protocol.""" + protocol_separator = "://" + if not value or protocol_separator in value: + raise vol.Invalid( + 'Invalid host, {} is not allowed'.format(protocol_separator)) + + return value + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up an iAlarm control panel.""" + name = config.get(CONF_NAME) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + host = config.get(CONF_HOST) + + url = 'http://{}'.format(host) + ialarm = IAlarmPanel(name, username, password, url) + add_devices([ialarm], True) + + +class IAlarmPanel(alarm.AlarmControlPanel): + """Represent an iAlarm status.""" + + def __init__(self, name, username, password, url): + """Initialize the iAlarm status.""" + from pyialarm import IAlarm + + self._name = name + self._username = username + self._password = password + self._url = url + self._state = None + self._client = IAlarm(username, password, url) + + @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): + """Return the state of the device.""" + status = self._client.get_status() + _LOGGER.debug('iAlarm status: %s', status) + if status: + status = int(status) + + if status == self._client.DISARMED: + state = STATE_ALARM_DISARMED + elif status == self._client.ARMED_AWAY: + state = STATE_ALARM_ARMED_AWAY + elif status == self._client.ARMED_STAY: + state = STATE_ALARM_ARMED_HOME + else: + state = None + + self._state = state + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._client.disarm() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._client.arm_away() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._client.arm_stay() diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 55f3834c06ab1..5ff6092493b38 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -16,24 +16,40 @@ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE, - CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) + CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME, + CONF_DISARM_AFTER_TRIGGER) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time +CONF_CODE_TEMPLATE = 'code_template' + DEFAULT_ALARM_NAME = 'HA Alarm' -DEFAULT_PENDING_TIME = 60 -DEFAULT_TRIGGER_TIME = 120 +DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) +DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) +DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False -SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_CUSTOM_BYPASS] +SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED] + +SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_TRIGGERED] +SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_DISARMED] + +ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' def _state_validator(config): config = copy.deepcopy(config) + for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_DELAY_TIME not in config[state]: + config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME] + if CONF_TRIGGER_TIME not in config[state]: + config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: if CONF_PENDING_TIME not in config[state]: config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] @@ -41,28 +57,44 @@ def _state_validator(config): return config -STATE_SETTING_SCHEMA = vol.Schema({ - vol.Optional(CONF_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)) -}) +def _state_schema(state): + schema = {} + if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + if state in SUPPORTED_PENDING_STATES: + schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + return vol.Schema(schema) PLATFORM_SCHEMA = vol.Schema(vol.All({ vol.Required(CONF_PLATFORM): 'manual', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Exclusive(CONF_CODE, 'code validation'): cv.string, + vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, + vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, - vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, - default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, + vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): + _state_schema(STATE_ALARM_ARMED_AWAY), + vol.Optional(STATE_ALARM_ARMED_HOME, default={}): + _state_schema(STATE_ALARM_ARMED_HOME), + vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): + _state_schema(STATE_ALARM_ARMED_NIGHT), + vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): + _state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS), + vol.Optional(STATE_ALARM_DISARMED, default={}): + _state_schema(STATE_ALARM_DISARMED), + vol.Optional(STATE_ALARM_TRIGGERED, default={}): + _state_schema(STATE_ALARM_TRIGGERED), }, _state_validator)) _LOGGER = logging.getLogger(__name__) @@ -74,8 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), - config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), + config.get(CONF_CODE_TEMPLATE), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config )]) @@ -86,27 +117,37 @@ class ManualAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for 'trigger_time'. After that will be - triggered for 'trigger_time', after that we return to the previous state - or disarm if `disarm_after_trigger` is true. + When triggered, will be pending for the triggering state's 'delay_time' + plus the triggered state's 'pending_time'. + After that will be triggered for 'trigger_time', after that we return to + the previous state or disarm if `disarm_after_trigger` is true. + A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, pending_time, trigger_time, + def __init__(self, hass, name, code, code_template, disarm_after_trigger, config): """Init the manual alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass self._name = name - self._code = str(code) if code else None - self._trigger_time = datetime.timedelta(seconds=trigger_time) + if code_template: + self._code = code_template + self._code.hass = hass + else: + self._code = code or None self._disarm_after_trigger = disarm_after_trigger - self._pre_trigger_state = self._state + self._previous_state = self._state self._state_ts = None - self._pending_time_by_state = {} - for state in SUPPORTED_PENDING_STATES: - self._pending_time_by_state[state] = datetime.timedelta( - seconds=config[state][CONF_PENDING_TIME]) + self._delay_time_by_state = { + state: config[state][CONF_DELAY_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} + self._trigger_time_by_state = { + state: config[state][CONF_TRIGGER_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} + self._pending_time_by_state = { + state: config[state][CONF_PENDING_TIME] + for state in SUPPORTED_PENDING_STATES} @property def should_poll(self): @@ -121,15 +162,16 @@ def name(self): @property def state(self): """Return the state of the device.""" - if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING - elif (self._state_ts + self._pending_time_by_state[self._state] + - self._trigger_time) < dt_util.utcnow(): + trigger_time = self._trigger_time_by_state[self._previous_state] + if (self._state_ts + self._pending_time(self._state) + + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: - self._state = self._pre_trigger_state + self._state = self._previous_state return self._state if self._state in SUPPORTED_PENDING_STATES and \ @@ -138,9 +180,21 @@ def state(self): return self._state - def _within_pending_time(self, state): + @property + def _active_state(self): + if self.state == STATE_ALARM_PENDING: + return self._previous_state + else: + return self._state + + def _pending_time(self, state): pending_time = self._pending_time_by_state[state] - return self._state_ts + pending_time > dt_util.utcnow() + if state == STATE_ALARM_TRIGGERED: + pending_time += self._delay_time_by_state[self._previous_state] + return pending_time + + def _within_pending_time(self, state): + return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property def code_format(self): @@ -185,26 +239,35 @@ def alarm_arm_custom_bypass(self, code=None): self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS) def alarm_trigger(self, code=None): - """Send alarm trigger command. No code needed.""" - self._pre_trigger_state = self._state + """ + Send alarm trigger command. + No code needed, a trigger time of zero for the current state + disables the alarm. + """ + if not self._trigger_time_by_state[self._active_state]: + return self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + if self._state == state: + return + + self._previous_state = self._state self._state = state self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - pending_time = self._pending_time_by_state[state] - - if state == STATE_ALARM_TRIGGERED and self._trigger_time: + pending_time = self._pending_time(state) + if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, self._state_ts + pending_time) + trigger_time = self._trigger_time_by_state[self._previous_state] track_point_in_time( self._hass, self.async_update_ha_state, - self._state_ts + self._trigger_time + pending_time) + self._state_ts + pending_time + trigger_time) elif state in SUPPORTED_PENDING_STATES and pending_time: track_point_in_time( self._hass, self.async_update_ha_state, @@ -212,7 +275,14 @@ def _update_state(self, state): def _validate_code(self, code, state): """Validate given code.""" - check = self._code is None or code == self._code + if self._code is None: + return True + if isinstance(self._code, str): + alarm_code = self._code + else: + alarm_code = self._code.render(from_state=self._state, + to_state=state) + check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) return check @@ -223,6 +293,7 @@ def device_state_attributes(self): state_attr = {} if self.state == STATE_ALARM_PENDING: + state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state state_attr[ATTR_POST_PENDING_STATE] = self._state return state_attr diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 44247616b59ff..9e388806e7349 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -16,8 +16,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, - CONF_DISARM_AFTER_TRIGGER) + CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME, + CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) import homeassistant.components.mqtt as mqtt from homeassistant.helpers.event import async_track_state_change @@ -26,28 +26,44 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time +CONF_CODE_TEMPLATE = 'code_template' + CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' DEFAULT_ALARM_NAME = 'HA Alarm' -DEFAULT_PENDING_TIME = 60 -DEFAULT_TRIGGER_TIME = 120 +DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) +DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) +DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_HOME = 'ARM_HOME' DEFAULT_ARM_NIGHT = 'ARM_NIGHT' DEFAULT_DISARM = 'DISARM' -SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] +SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_TRIGGERED] + +SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_TRIGGERED] + +SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_DISARMED] +ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' def _state_validator(config): config = copy.deepcopy(config) + for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_DELAY_TIME not in config[state]: + config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME] + if CONF_TRIGGER_TIME not in config[state]: + config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: if CONF_PENDING_TIME not in config[state]: config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] @@ -55,27 +71,44 @@ def _state_validator(config): return config -STATE_SETTING_SCHEMA = vol.Schema({ - vol.Optional(CONF_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)) -}) +def _state_schema(state): + schema = {} + if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + if state in SUPPORTED_PENDING_STATES: + schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + return vol.Schema(schema) + DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): 'manual_mqtt', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Exclusive(CONF_CODE, 'code validation'): cv.string, + vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, + vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, - vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, + vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): + _state_schema(STATE_ALARM_ARMED_AWAY), + vol.Optional(STATE_ALARM_ARMED_HOME, default={}): + _state_schema(STATE_ALARM_ARMED_HOME), + vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): + _state_schema(STATE_ALARM_ARMED_NIGHT), + vol.Optional(STATE_ALARM_DISARMED, default={}): + _state_schema(STATE_ALARM_DISARMED), + vol.Optional(STATE_ALARM_TRIGGERED, default={}): + _state_schema(STATE_ALARM_TRIGGERED), vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, @@ -93,8 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), - config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), + config.get(CONF_CODE_TEMPLATE), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(mqtt.CONF_STATE_TOPIC), config.get(mqtt.CONF_COMMAND_TOPIC), @@ -111,13 +143,15 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for 'trigger_time'. After that will be - triggered for 'trigger_time', after that we return to the previous state - or disarm if `disarm_after_trigger` is true. + When triggered, will be pending for the triggering state's 'delay_time' + plus the triggered state's 'pending_time'. + After that will be triggered for 'trigger_time', after that we return to + the previous state or disarm if `disarm_after_trigger` is true. + A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, pending_time, - trigger_time, disarm_after_trigger, + def __init__(self, hass, name, code, code_template, + disarm_after_trigger, state_topic, command_topic, qos, payload_disarm, payload_arm_home, payload_arm_away, payload_arm_night, config): @@ -125,17 +159,24 @@ def __init__(self, hass, name, code, pending_time, self._state = STATE_ALARM_DISARMED self._hass = hass self._name = name - self._code = str(code) if code else None - self._pending_time = datetime.timedelta(seconds=pending_time) - self._trigger_time = datetime.timedelta(seconds=trigger_time) + if code_template: + self._code = code_template + self._code.hass = hass + else: + self._code = code or None self._disarm_after_trigger = disarm_after_trigger - self._pre_trigger_state = self._state + self._previous_state = self._state self._state_ts = None - self._pending_time_by_state = {} - for state in SUPPORTED_PENDING_STATES: - self._pending_time_by_state[state] = datetime.timedelta( - seconds=config[state][CONF_PENDING_TIME]) + self._delay_time_by_state = { + state: config[state][CONF_DELAY_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} + self._trigger_time_by_state = { + state: config[state][CONF_TRIGGER_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} + self._pending_time_by_state = { + state: config[state][CONF_PENDING_TIME] + for state in SUPPORTED_PENDING_STATES} self._state_topic = state_topic self._command_topic = command_topic @@ -158,15 +199,16 @@ def name(self): @property def state(self): """Return the state of the device.""" - if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING - elif (self._state_ts + self._pending_time_by_state[self._state] + - self._trigger_time) < dt_util.utcnow(): + trigger_time = self._trigger_time_by_state[self._previous_state] + if (self._state_ts + self._pending_time(self._state) + + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: - self._state = self._pre_trigger_state + self._state = self._previous_state return self._state if self._state in SUPPORTED_PENDING_STATES and \ @@ -175,9 +217,21 @@ def state(self): return self._state - def _within_pending_time(self, state): + @property + def _active_state(self): + if self.state == STATE_ALARM_PENDING: + return self._previous_state + else: + return self._state + + def _pending_time(self, state): pending_time = self._pending_time_by_state[state] - return self._state_ts + pending_time > dt_util.utcnow() + if state == STATE_ALARM_TRIGGERED: + pending_time += self._delay_time_by_state[self._previous_state] + return pending_time + + def _within_pending_time(self, state): + return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property def code_format(self): @@ -215,26 +269,35 @@ def alarm_arm_night(self, code=None): self._update_state(STATE_ALARM_ARMED_NIGHT) def alarm_trigger(self, code=None): - """Send alarm trigger command. No code needed.""" - self._pre_trigger_state = self._state + """ + Send alarm trigger command. + No code needed, a trigger time of zero for the current state + disables the alarm. + """ + if not self._trigger_time_by_state[self._active_state]: + return self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + if self._state == state: + return + + self._previous_state = self._state self._state = state self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - pending_time = self._pending_time_by_state[state] - - if state == STATE_ALARM_TRIGGERED and self._trigger_time: + pending_time = self._pending_time(state) + if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, self._state_ts + pending_time) + trigger_time = self._trigger_time_by_state[self._previous_state] track_point_in_time( self._hass, self.async_update_ha_state, - self._state_ts + self._trigger_time + pending_time) + self._state_ts + pending_time + trigger_time) elif state in SUPPORTED_PENDING_STATES and pending_time: track_point_in_time( self._hass, self.async_update_ha_state, @@ -242,7 +305,14 @@ def _update_state(self, state): def _validate_code(self, code, state): """Validate given code.""" - check = self._code is None or code == self._code + if self._code is None: + return True + if isinstance(self._code, str): + alarm_code = self._code + else: + alarm_code = self._code.render(from_state=self._state, + to_state=state) + check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) return check @@ -253,6 +323,7 @@ def device_state_attributes(self): state_attr = {} if self.state == STATE_ALARM_PENDING: + state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state state_attr[ATTR_POST_PENDING_STATE] = self._state return state_attr diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index d58acac5373c2..a8054b838ef0b 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -59,8 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): climate_devices = [] for zone in zones: - climate_devices.append(create_climate_device( - tado, hass, zone, zone['name'], zone['id'])) + device = create_climate_device( + tado, hass, zone, zone['name'], zone['id']) + if not device: + continue + climate_devices.append(device) if climate_devices: add_devices(climate_devices, True) @@ -75,8 +78,11 @@ def create_climate_device(tado, hass, zone, name, zone_id): if ac_mode: temperatures = capabilities['HEAT']['temperatures'] - else: + elif 'temperatures' in capabilities: temperatures = capabilities['temperatures'] + else: + _LOGGER.debug("Received zone %s has no temperature; not adding", name) + return min_temp = float(temperatures['celsius']['min']) max_temp = float(temperatures['celsius']['max']) diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 196235f32f4f6..20dc9052e116a 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -11,7 +11,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL) @@ -38,7 +39,7 @@ def get_scanner(hass, config): return None -class LinksysAPDeviceScanner(object): +class LinksysAPDeviceScanner(DeviceScanner): """This class queries a Linksys Access Point.""" def __init__(self, config): diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py index 867bdfafc6b73..633ea1b0c5e9c 100644 --- a/homeassistant/components/dominos.py +++ b/homeassistant/components/dominos.py @@ -58,7 +58,8 @@ vol.Required(ATTR_PHONE): cv.string, vol.Required(ATTR_ADDRESS): cv.string, vol.Optional(ATTR_SHOW_MENU): cv.boolean, - vol.Optional(ATTR_ORDERS): vol.All(cv.ensure_list, [_ORDERS_SCHEMA]), + vol.Optional(ATTR_ORDERS, default=[]): vol.All( + cv.ensure_list, [_ORDERS_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) @@ -81,7 +82,8 @@ def setup(hass, config): order = DominosOrder(order_info, dominos) entities.append(order) - component.add_entities(entities) + if entities: + component.add_entities(entities) # Return boolean to indicate that initialization was successfully. return True @@ -93,7 +95,8 @@ class Dominos(): def __init__(self, hass, config): """Set up main service.""" conf = config[DOMAIN] - from pizzapi import Address, Customer, Store + from pizzapi import Address, Customer + from pizzapi.address import StoreException self.hass = hass self.customer = Customer( conf.get(ATTR_FIRST_NAME), @@ -105,7 +108,10 @@ def __init__(self, hass, config): *self.customer.address.split(','), country=conf.get(ATTR_COUNTRY)) self.country = conf.get(ATTR_COUNTRY) - self.closest_store = Store() + try: + self.closest_store = self.address.closest_store() + except StoreException: + self.closest_store = None def handle_order(self, call): """Handle ordering pizza.""" @@ -123,29 +129,31 @@ def update_closest_store(self): from pizzapi.address import StoreException try: self.closest_store = self.address.closest_store() + return True except StoreException: - self.closest_store = False + self.closest_store = None + return False def get_menu(self): """Return the products from the closest stores menu.""" - if self.closest_store is False: + if self.closest_store is None: _LOGGER.warning('Cannot get menu. Store may be closed') - return - - menu = self.closest_store.get_menu() - product_entries = [] + return [] + else: + menu = self.closest_store.get_menu() + product_entries = [] - for product in menu.products: - item = {} - if isinstance(product.menu_data['Variants'], list): - variants = ', '.join(product.menu_data['Variants']) - else: - variants = product.menu_data['Variants'] - item['name'] = product.name - item['variants'] = variants - product_entries.append(item) + for product in menu.products: + item = {} + if isinstance(product.menu_data['Variants'], list): + variants = ', '.join(product.menu_data['Variants']) + else: + variants = product.menu_data['Variants'] + item['name'] = product.name + item['variants'] = variants + product_entries.append(item) - return product_entries + return product_entries class DominosProductListView(http.HomeAssistantView): @@ -192,7 +200,7 @@ def orderable(self): @property def state(self): """Return the state either closed, orderable or unorderable.""" - if self.dominos.closest_store is False: + if self.dominos.closest_store is None: return 'closed' else: return 'orderable' if self._orderable else 'unorderable' @@ -217,6 +225,11 @@ def update(self): def order(self): """Create the order object.""" from pizzapi import Order + from pizzapi.address import StoreException + + if self.dominos.closest_store is None: + raise StoreException + order = Order( self.dominos.closest_store, self.dominos.customer, diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b71a650804938..e1121fd0c4e68 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,7 +23,7 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20171130.0', 'user-agents==1.1.0'] +REQUIREMENTS = ['home-assistant-frontend==20171204.0', 'user-agents==1.1.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index cfa1693f571db..ebabcdb0e7955 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -264,7 +264,7 @@ def post(self, request): # return self.json_message(humanize_error(request.json, ex), # HTTP_BAD_REQUEST) - data[ATTR_LAST_SEEN_AT] = datetime.datetime.now() + data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() name = data.get(ATTR_DEVICE_ID) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 7fffc09696ce5..0a03af0e1bf03 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.5.4'] +REQUIREMENTS = ['denonavr==0.5.5'] _LOGGER = logging.getLogger(__name__) @@ -102,12 +102,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if config.get(CONF_HOST) is None and discovery_info is None: d_receivers = denonavr.discover() # More than one receiver could be discovered by that method - if d_receivers is not None: - for d_receiver in d_receivers: - host = d_receiver["host"] - name = d_receiver["friendlyName"] - new_hosts.append( - NewHost(host=host, name=name)) + for d_receiver in d_receivers: + host = d_receiver["host"] + name = d_receiver["friendlyName"] + new_hosts.append( + NewHost(host=host, name=name)) for entry in new_hosts: # Check if host not in cache, append it and save for later diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 0153eb687fffc..721b095c083b2 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -6,6 +6,7 @@ """ import logging import socket +from datetime import timedelta import voluptuous as vol @@ -17,6 +18,7 @@ CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT, CONF_MAC) import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==0.2.2'] @@ -100,6 +102,9 @@ def __init__(self, host, port, name, timeout, mac): self._playing = True self._state = STATE_UNKNOWN self._remote = None + # Mark the end of a shutdown command (need to wait 15 seconds before + # sending the next command to avoid turning the TV back ON). + self._end_of_power_off = None # Generate a configuration for the Samsung library self._config = { 'name': 'HomeAssistant', @@ -118,7 +123,7 @@ def __init__(self, host, port, name, timeout, mac): def update(self): """Retrieve the latest data.""" # Send an empty key to see if we are still connected - return self.send_key('KEY') + self.send_key('KEY') def get_remote(self): """Create or return a remote control instance.""" @@ -130,6 +135,10 @@ def get_remote(self): def send_key(self, key): """Send a key to the tv and handles exceptions.""" + if self._power_off_in_progress() \ + and not (key == 'KEY_POWER' or key == 'KEY_POWEROFF'): + _LOGGER.info("TV is powering off, not sending command: %s", key) + return try: self.get_remote().control(key) self._state = STATE_ON @@ -139,13 +148,16 @@ def send_key(self, key): # BrokenPipe can occur when the commands is sent to fast self._state = STATE_ON self._remote = None - return False + return except (self._exceptions_class.ConnectionClosed, OSError): self._state = STATE_OFF self._remote = None - return False + if self._power_off_in_progress(): + self._state = STATE_OFF - return True + def _power_off_in_progress(self): + return self._end_of_power_off is not None and \ + self._end_of_power_off > dt_util.utcnow() @property def name(self): @@ -171,6 +183,8 @@ def supported_features(self): def turn_off(self): """Turn off media player.""" + self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) + if self._config['method'] == 'websocket': self.send_key('KEY_POWER') else: diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 6c4f7e49ddea6..1fa8f1dab78b6 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -4,8 +4,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.nfandroidtv/ """ -import os import logging +import io +import base64 import requests import voluptuous as vol @@ -31,6 +32,9 @@ DEFAULT_COLOR = 'grey' DEFAULT_INTERRUPT = False DEFAULT_TIMEOUT = 5 +DEFAULT_ICON = ( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApo' + 'cMXEAAAAASUVORK5CYII=') ATTR_DURATION = 'duration' ATTR_POSITION = 'position' @@ -110,16 +114,13 @@ def __init__(self, remoteip, duration, position, transparency, color, self._default_color = color self._default_interrupt = interrupt self._timeout = timeout - self._icon_file = os.path.join( - os.path.dirname(__file__), '..', 'frontend', 'www_static', 'icons', - 'favicon-192x192.png') + self._icon_file = io.BytesIO(base64.b64decode(DEFAULT_ICON)) def send_message(self, message="", **kwargs): """Send a message to a Android TV device.""" _LOGGER.debug("Sending notification to: %s", self._target) - payload = dict(filename=('icon.png', - open(self._icon_file, 'rb'), + payload = dict(filename=('icon.png', self._icon_file, 'application/octet-stream', {'Expires': '0'}), type='0', title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), @@ -129,7 +130,7 @@ def send_message(self, message="", **kwargs): transparency='%i' % TRANSPARENCIES.get( self._default_transparency), offset='0', app=ATTR_TITLE_DEFAULT, force='true', - interrupt='%i' % self._default_interrupt) + interrupt='%i' % self._default_interrupt,) data = kwargs.get(ATTR_DATA) if data: diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 0396cafd4ffaa..0ecfa50ee6360 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -14,12 +14,13 @@ from homeassistant.components import recorder from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, TEMP_CELSIUS, - EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN) + EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, + ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) from homeassistant import core as hacore from homeassistant.helpers import state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius -REQUIREMENTS = ['prometheus_client==0.0.19'] +REQUIREMENTS = ['prometheus_client==0.0.21'] _LOGGER = logging.getLogger(__name__) @@ -159,6 +160,26 @@ def _handle_lock(self, state): value = state_helper.state_as_number(state) metric.labels(**self._labels(state)).set(value) + def _handle_climate(self, state): + temp = state.attributes.get(ATTR_TEMPERATURE) + if temp: + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit == TEMP_FAHRENHEIT: + temp = fahrenheit_to_celsius(temp) + metric = self._metric( + 'temperature_c', self.prometheus_client.Gauge, + 'Temperature in degrees Celsius') + metric.labels(**self._labels(state)).set(temp) + + metric = self._metric( + 'climate_state', self.prometheus_client.Gauge, + 'State of the thermostat (0/1)') + try: + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + def _handle_sensor(self, state): _sensor_types = { TEMP_CELSIUS: ( @@ -189,9 +210,17 @@ def _handle_sensor(self, state): 'electricity_usage_w', self.prometheus_client.Gauge, 'Currently reported electricity draw in Watts', ), + 'min': ( + 'sensor_min', self.prometheus_client.Gauge, + 'Time in minutes reported by a sensor' + ), + 'Events': ( + 'sensor_event_count', self.prometheus_client.Gauge, + 'Number of events for a sensor' + ), } - unit = state.attributes.get('unit_of_measurement') + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) metric = _sensor_types.get(unit) if metric is not None: @@ -212,12 +241,25 @@ def _handle_switch(self, state): self.prometheus_client.Gauge, 'State of the switch (0/1)', ) - value = state_helper.state_as_number(state) - metric.labels(**self._labels(state)).set(value) + + try: + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass def _handle_zwave(self, state): self._battery(state) + def _handle_automation(self, state): + metric = self._metric( + 'automation_triggered_count', + self.prometheus_client.Counter, + 'Count of times an automation has been triggered', + ) + + metric.labels(**self._labels(state)).inc() + class PrometheusView(HomeAssistantView): """Handle Prometheus requests.""" diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py new file mode 100644 index 0000000000000..88ead3301b6fd --- /dev/null +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -0,0 +1,110 @@ +""" +Stock market information from Alpha Vantage. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.alpha_vantage/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['alpha_vantage==1.3.6'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_CLOSE = 'close' +ATTR_HIGH = 'high' +ATTR_LOW = 'low' +ATTR_VOLUME = 'volume' + +CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage." +CONF_SYMBOLS = 'symbols' + +DEFAULT_SYMBOL = 'GOOGL' + +ICON = 'mdi:currency-usd' + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]): + vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Alpha Vantage sensor.""" + from alpha_vantage.timeseries import TimeSeries + + api_key = config.get(CONF_API_KEY) + symbols = config.get(CONF_SYMBOLS) + + timeseries = TimeSeries(key=api_key) + + dev = [] + for symbol in symbols: + try: + timeseries.get_intraday(symbol) + except ValueError: + _LOGGER.error( + "API Key is not valid or symbol '%s' not known", symbol) + return + dev.append(AlphaVantageSensor(timeseries, symbol)) + + add_devices(dev, True) + + +class AlphaVantageSensor(Entity): + """Representation of a Alpha Vantage sensor.""" + + def __init__(self, timeseries, symbol): + """Initialize the sensor.""" + self._name = symbol + self._timeseries = timeseries + self._symbol = symbol + self.values = None + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._symbol + + @property + def state(self): + """Return the state of the sensor.""" + return self.values['1. open'] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.values is not None: + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_CLOSE: self.values['4. close'], + ATTR_HIGH: self.values['2. high'], + ATTR_LOW: self.values['3. low'], + ATTR_VOLUME: self.values['5. volume'], + } + + @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 states.""" + all_values, _ = self._timeseries.get_intraday(self._symbol) + self.values = next(iter(all_values.values())) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index dc879fe0d3e26..3e736ed719f25 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_CHANNEL_ID): cv.positive_int, + vol.Required(CONF_CHANNEL_ID): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 2ae1c3674eae6..86362e8f2d9f9 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/sensor.rest/ """ import logging +import json import voluptuous as vol import requests @@ -25,6 +26,7 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True +CONF_JSON_ATTRS = 'json_attributes' METHODS = ['POST', 'GET'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -32,6 +34,7 @@ vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_HEADERS): {cv.string: cv.string}, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -55,6 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + json_attrs = config.get(CONF_JSON_ATTRS) + if value_template is not None: value_template.hass = hass @@ -68,13 +73,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest.update() - add_devices([RestSensor(hass, rest, name, unit, value_template)], True) + add_devices([RestSensor( + hass, rest, name, unit, value_template, json_attrs)], True) class RestSensor(Entity): """Implementation of a REST sensor.""" - def __init__(self, hass, rest, name, unit_of_measurement, value_template): + def __init__(self, hass, rest, name, + unit_of_measurement, value_template, json_attrs): """Initialize the REST sensor.""" self._hass = hass self.rest = rest @@ -82,6 +89,8 @@ def __init__(self, hass, rest, name, unit_of_measurement, value_template): self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._json_attrs = json_attrs + self._attributes = None @property def name(self): @@ -108,6 +117,20 @@ def update(self): self.rest.update() value = self.rest.data + if self._json_attrs: + self._attributes = {} + try: + json_dict = json.loads(value) + if isinstance(json_dict, dict): + attrs = {k: json_dict[k] for k in self._json_attrs + if k in json_dict} + self._attributes = attrs + else: + _LOGGER.warning("JSON result was not a dictionary") + except ValueError: + _LOGGER.warning("REST result could not be parsed as JSON") + _LOGGER.debug("Erroneous JSON: %s", value) + if value is None: value = STATE_UNKNOWN elif self._value_template is not None: @@ -116,6 +139,11 @@ def update(self): self._state = value + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + class RestData(object): """Class for handling the data retrieval.""" diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py index 824fec4158027..3f36a1128d6de 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/sensor/tesla.py @@ -39,7 +39,7 @@ class TeslaSensor(TeslaDevice, Entity): def __init__(self, tesla_device, controller, sensor_type=None): """Initialisation of the sensor.""" self.current_value = None - self._temperature_units = None + self._unit = None self.last_changed_time = None self.type = sensor_type super().__init__(tesla_device, controller) @@ -59,7 +59,7 @@ def state(self): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return self._temperature_units + return self._unit def update(self): """Update the state from the sensor.""" @@ -74,8 +74,9 @@ def update(self): tesla_temp_units = self.tesla_device.measurement if tesla_temp_units == 'F': - self._temperature_units = TEMP_FAHRENHEIT + self._unit = TEMP_FAHRENHEIT else: - self._temperature_units = TEMP_CELSIUS + self._unit = TEMP_CELSIUS else: self.current_value = self.tesla_device.battery_level() + self._unit = "%" diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 8fa6493862c65..6e8c1a6b9bb93 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/switch.tplink/ """ import logging - import time import voluptuous as vol @@ -47,6 +46,7 @@ def __init__(self, smartplug, name): self.smartplug = smartplug self._name = name self._state = None + self._available = True # Set up emeter cache self._emeter_params = {} @@ -55,6 +55,11 @@ def name(self): """Return the name of the Smart Plug, if any.""" return self._name + @property + def available(self) -> bool: + """Return if switch is available.""" + return self._available + @property def is_on(self): """Return true if switch is on.""" @@ -77,6 +82,7 @@ def update(self): """Update the TP-Link switch's state.""" from pyHS100 import SmartDeviceException try: + self._available = True self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON @@ -100,8 +106,9 @@ def update(self): self._emeter_params[ATTR_DAILY_CONSUMPTION] \ = "%.2f kW" % emeter_statics[int(time.strftime("%e"))] except KeyError: - # device returned no daily history + # Device returned no daily history pass except (SmartDeviceException, OSError) as ex: - _LOGGER.warning('Could not read state for %s: %s', self.name, ex) + _LOGGER.warning("Could not read state for %s: %s", self.name, ex) + self._available = False diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index ba7c1afd286b3..28bf65bc4c505 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -24,7 +24,7 @@ DOMAIN = 'tellduslive' -REQUIREMENTS = ['tellduslive==0.10.3'] +REQUIREMENTS = ['tellduslive==0.10.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index f875edef31029..678ead981c11b 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -219,7 +219,9 @@ def device_state_attributes(self): def push_data(self, data): """Push from Hub.""" _LOGGER.debug("PUSH >> %s: %s", self, data) - if self.parse_data(data) or self.parse_voltage(data): + is_data = self.parse_data(data) + is_voltage = self.parse_voltage(data) + if is_data or is_voltage: self.schedule_update_ha_state() def parse_voltage(self, data): diff --git a/homeassistant/const.py b/homeassistant/const.py index f46058b186c15..85047f0482e6e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 59 +MINOR_VERSION = 60 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) @@ -52,6 +52,7 @@ CONF_CUSTOMIZE = 'customize' CONF_CUSTOMIZE_DOMAIN = 'customize_domain' CONF_CUSTOMIZE_GLOB = 'customize_glob' +CONF_DELAY_TIME = 'delay_time' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DEVICES = 'devices' diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 8b98bfadb68e8..254a48c3d0a8e 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -21,7 +21,8 @@ ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, STATE_HEAT, STATE_COOL, + STATE_IDLE) from homeassistant.components.climate.ecobee import ( ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) @@ -210,10 +211,11 @@ def state_as_number(state): Raises ValueError if this is not possible. """ if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, - STATE_OPEN, STATE_HOME): + STATE_OPEN, STATE_HOME, STATE_HEAT, STATE_COOL): return 1 elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, - STATE_BELOW_HORIZON, STATE_CLOSED, STATE_NOT_HOME): + STATE_BELOW_HORIZON, STATE_CLOSED, STATE_NOT_HOME, + STATE_IDLE): return 0 return float(state.state) diff --git a/requirements_all.txt b/requirements_all.txt index 42f9d175377a5..838e8e4200804 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -83,6 +83,9 @@ aiopvapi==1.5.4 # homeassistant.components.alarmdecoder alarmdecoder==0.12.3 +# homeassistant.components.sensor.alpha_vantage +alpha_vantage==1.3.6 + # homeassistant.components.amcrest amcrest==1.2.1 @@ -197,7 +200,7 @@ defusedxml==0.5.0 deluge-client==1.0.5 # homeassistant.components.media_player.denonavr -denonavr==0.5.4 +denonavr==0.5.5 # homeassistant.components.media_player.directv directpy==0.2 @@ -331,7 +334,7 @@ hipnotify==1.0.8 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171130.0 +home-assistant-frontend==20171204.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a @@ -557,7 +560,7 @@ pocketcasts==0.1 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.0.19 +prometheus_client==0.0.21 # homeassistant.components.sensor.systemmonitor psutil==5.4.1 @@ -682,6 +685,9 @@ pyhomematic==0.1.35 # homeassistant.components.sensor.hydroquebec pyhydroquebec==1.3.1 +# homeassistant.components.alarm_control_panel.ialarm +pyialarm==0.2 + # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 @@ -1063,7 +1069,7 @@ tellcore-net==0.3 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellduslive==0.10.3 +tellduslive==0.10.4 # homeassistant.components.sensor.temper temperusb==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b02d80ad0e38d..b858c8a1c0e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -74,7 +74,7 @@ hbmqtt==0.9.1 holidays==0.8.1 # homeassistant.components.frontend -home-assistant-frontend==20171130.0 +home-assistant-frontend==20171204.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb @@ -113,7 +113,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.0.19 +prometheus_client==0.0.21 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index d65568b08447e..c47ed941b6577 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -140,6 +140,32 @@ def test_arm_away_no_pending(self): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) + def test_arm_home_with_template_code(self): + """Attempt to arm with a template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code_template': '{{ "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + def test_arm_away_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -257,6 +283,13 @@ def test_arm_night_with_pending(self): state = self.hass.states.get(entity_id) assert state.state == STATE_ALARM_ARMED_NIGHT + # Do not go to the pending state when updating to the same state + alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + def test_arm_night_with_invalid_code(self): """Attempt to night home without a valid code.""" self.assertTrue(setup_component( @@ -311,6 +344,93 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + + def test_trigger_zero_trigger_time(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'pending_time': 0, + 'trigger_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_zero_trigger_time_with_pending(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'pending_time': 2, + 'trigger_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -355,6 +475,203 @@ def test_trigger_with_pending(self): state = self.hass.states.get(entity_id) assert state.state == STATE_ALARM_DISARMED + def test_trigger_with_unused_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 5, + 'pending_time': 0, + 'armed_home': { + 'delay_time': 10 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + def test_armed_home_with_specific_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -518,6 +835,101 @@ def test_trigger_with_disarm_after_trigger(self): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_trigger_with_zero_specific_trigger_time(self): + """Test trigger method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5, + 'disarmed': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_unused_zero_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5, + 'armed_home': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'disarmed': { + 'trigger_time': 5 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_no_disarm_after_trigger(self): """Test disarm after trigger.""" self.assertTrue(setup_component( @@ -684,6 +1096,45 @@ def test_disarm_during_trigger_with_invalid_code(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_disarm_with_template_code(self): + """Attempt to disarm with a valid or invalid template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code_template': + '{{ "" if from_state == "disarmed" else "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_DISARMED, state.state) + def test_arm_custom_bypass_no_pending(self): """Test arm custom bypass method.""" self.assertTrue(setup_component( @@ -795,3 +1246,75 @@ def test_armed_custom_bypass_with_specific_pending(self): self.assertEqual(STATE_ALARM_ARMED_CUSTOM_BYPASS, self.hass.states.get(entity_id).state) + + def test_arm_away_after_disabled_disarmed(self): + """Test pending state with and without zero trigger time.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'delay_time': 1, + 'armed_away': { + 'pending_time': 1, + }, + 'disarmed': { + 'trigger_time': 0 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index e56b6865e6e7b..83254d9104f9a 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -162,6 +162,34 @@ def test_arm_away_no_pending(self): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) + def test_arm_home_with_template_code(self): + """Attempt to arm with a template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code_template': '{{ "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + def test_arm_away_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -287,6 +315,13 @@ def test_arm_night_with_pending(self): self.assertEqual(STATE_ALARM_ARMED_NIGHT, self.hass.states.get(entity_id).state) + # Do not go to the pending state when updating to the same state + alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + def test_arm_night_with_invalid_code(self): """Attempt to arm night without a valid code.""" self.assertTrue(setup_component( @@ -345,6 +380,99 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + + def test_trigger_zero_trigger_time(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 0, + 'trigger_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_zero_trigger_time_with_pending(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 2, + 'trigger_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -425,6 +553,107 @@ def test_trigger_with_disarm_after_trigger(self): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_trigger_with_zero_specific_trigger_time(self): + """Test trigger method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'disarmed': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_unused_zero_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'armed_home': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'disarmed': { + 'trigger_time': 5 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_back_to_back_trigger_with_no_disarm_after_trigger(self): """Test no disarm after back to back trigger.""" self.assertTrue(setup_component( @@ -559,6 +788,211 @@ def test_disarm_during_trigger_with_invalid_code(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_unused_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 5, + 'pending_time': 0, + 'armed_home': { + 'delay_time': 10 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + def test_armed_home_with_specific_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -674,45 +1108,145 @@ def test_trigger_with_specific_pending(self): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) + alarm_control_panel.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - future = dt_util.utcnow() + timedelta(seconds=10) + future = dt_util.utcnow() + timedelta(seconds=2) with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' 'dt_util.utcnow'), return_value=future): fire_time_changed(self.hass, future) self.hass.block_till_done() - self.assertEqual(STATE_ALARM_ARMED_HOME, + self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) - self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() - self.assertEqual(STATE_ALARM_PENDING, + self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - future = dt_util.utcnow() + timedelta(seconds=2) + def test_arm_away_after_disabled_disarmed(self): + """Test pending state with and without zero trigger time.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'delay_time': 1, + 'armed_away': { + 'pending_time': 1, + }, + 'disarmed': { + 'trigger_time': 0 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' 'dt_util.utcnow'), return_value=future): fire_time_changed(self.hass, future) self.hass.block_till_done() - self.assertEqual(STATE_ALARM_TRIGGERED, - self.hass.states.get(entity_id).state) + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) - future = dt_util.utcnow() + timedelta(seconds=5) + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future += timedelta(seconds=1) with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' 'dt_util.utcnow'), return_value=future): fire_time_changed(self.hass, future) self.hass.block_till_done() - self.assertEqual(STATE_ALARM_ARMED_HOME, + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + + def test_disarm_with_template_code(self): + """Attempt to disarm with a valid or invalid template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code_template': + '{{ "" if from_state == "disarmed" else "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + alarm_control_panel.alarm_arm_home(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_DISARMED, state.state) + def test_arm_home_via_command_topic(self): """Test arming home via command topic.""" assert setup_component(self.hass, alarm_control_panel.DOMAIN, { diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index a083dbfb1a281..1bda8ab82f39d 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -133,9 +133,9 @@ def setUp(self): self.value_template = template('{{ value_json.key }}') self.value_template.hass = self.hass - self.sensor = rest.RestSensor( - self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template, []) def tearDown(self): """Stop everything that was started.""" @@ -181,12 +181,62 @@ def test_update_with_no_template(self): self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( 'plain_state')) - self.sensor = rest.RestSensor( - self.hass, self.rest, self.name, self.unit_of_measurement, None) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, []) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) self.assertTrue(self.sensor.available) + def test_update_with_json_attrs(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "some_json_value" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, ['key']) + self.sensor.update() + self.assertEqual('some_json_value', + self.sensor.device_state_attributes['key']) + + @patch('homeassistant.components.sensor.rest._LOGGER') + def test_update_with_json_attrs_not_dict(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '["list", "of", "things"]')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.rest._LOGGER') + def test_update_with_json_attrs_bad_JSON(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + 'This is text rather than JSON data.')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + self.assertTrue(mock_logger.debug.called) + + def test_update_with_json_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "json_state_updated_value" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template, ['key']) + self.sensor.update() + + self.assertEqual('json_state_updated_value', self.sensor.state) + self.assertEqual('json_state_updated_value', + self.sensor.device_state_attributes['key']) + class TestRestData(unittest.TestCase): """Tests for RestData.""" diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index dd8cbfe55e096..052292b015d33 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -30,4 +30,6 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name assert len(body) > 3 # At least two comment lines and a metric for line in body: if line: - assert line.startswith('# ') or line.startswith('process_') + assert line.startswith('# ') \ + or line.startswith('process_') \ + or line.startswith('python_info')