Compare commits

...

10 Commits

Author SHA1 Message Date
Paulus Schoutsen 7d334783de Bumped version to 0.87.0b5 2019-02-03 15:27:14 -08:00
David Lie 027fcf269b Revert pyfoscam back to libpyfoscam (#20727)
* Change foscam python library to pyfoscam, which is more up to date and has several critical bug fixes.

* Update requirements_all.txt to match.

* Inserting automatically generated requirements.txt

* Revert changes until pyfoscam captures recent bug fixes. The pyfoscam version pulled by pip is currently broken.

* Updated requirements_all.txt based on changing pyfoscam back to libpyfoscam.
2019-02-03 15:26:19 -08:00
Aaron Bach e1509bcc0c Fix temperature unit conversion in Ambient PWS (#20723) 2019-02-03 15:26:18 -08:00
Andrew Sayre 9a13aafeea Add SmartThings button support via events (#20707)
* Add event support for buttons

* binary_sensor test clean-up
2019-02-03 15:26:17 -08:00
Paulus Schoutsen 5f2d209dec Updated frontend to 20190203.0 2019-02-03 11:32:26 -08:00
Paulus Schoutsen b0200cdbfe Bumped version to 0.87.0b4 2019-02-02 20:28:03 -08:00
Diogo Gomes c9f64af85a fix test commented in #20678 (#20680) 2019-02-02 17:05:40 -08:00
Paulus Schoutsen e4d45bf53a Test is broken 2019-02-02 17:04:48 -08:00
Paulus Schoutsen e984868762 Bumped version to 0.87.0b3 2019-02-02 16:32:26 -08:00
Paulus Schoutsen e5835eb7c8 Remove fingerprint middleware (#20682)
* Remove fingerprint middleware

* Lint
2019-02-02 16:32:20 -08:00
16 changed files with 150 additions and 128 deletions
@@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS,
CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_STOP)
EVENT_HOMEASSISTANT_STOP)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -19,8 +19,7 @@ from homeassistant.helpers.event import async_call_later
from .config_flow import configured_instances
from .const import (
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI,
UNITS_US)
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
REQUIREMENTS = ['aioambient==0.1.0']
_LOGGER = logging.getLogger(__name__)
@@ -28,36 +27,36 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_SOCKET_MIN_RETRY = 15
SENSOR_TYPES = {
'24hourrainin': ['24 Hr Rain', 'in'],
'baromabsin': ['Abs Pressure', 'inHg'],
'baromrelin': ['Rel Pressure', 'inHg'],
'battout': ['Battery', ''],
'co2': ['co2', 'ppm'],
'dailyrainin': ['Daily Rain', 'in'],
'dewPoint': ['Dew Point', ['°F', '°C']],
'eventrainin': ['Event Rain', 'in'],
'feelsLike': ['Feels Like', ['°F', '°C']],
'hourlyrainin': ['Hourly Rain Rate', 'in/hr'],
'humidity': ['Humidity', '%'],
'humidityin': ['Humidity In', '%'],
'lastRain': ['Last Rain', ''],
'maxdailygust': ['Max Gust', 'mph'],
'monthlyrainin': ['Monthly Rain', 'in'],
'solarradiation': ['Solar Rad', 'W/m^2'],
'tempf': ['Temp', ['°F', '°C']],
'tempinf': ['Inside Temp', ['°F', '°C']],
'totalrainin': ['Lifetime Rain', 'in'],
'uv': ['uv', 'Index'],
'weeklyrainin': ['Weekly Rain', 'in'],
'winddir': ['Wind Dir', '°'],
'winddir_avg10m': ['Wind Dir Avg 10m', '°'],
'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'],
'windgustdir': ['Gust Dir', '°'],
'windgustmph': ['Wind Gust', 'mph'],
'windspdmph_avg10m': ['Wind Avg 10m', 'mph'],
'windspdmph_avg2m': ['Wind Avg 2m', 'mph'],
'windspeedmph': ['Wind Speed', 'mph'],
'yearlyrainin': ['Yearly Rain', 'in'],
'24hourrainin': ('24 Hr Rain', 'in'),
'baromabsin': ('Abs Pressure', 'inHg'),
'baromrelin': ('Rel Pressure', 'inHg'),
'battout': ('Battery', ''),
'co2': ('co2', 'ppm'),
'dailyrainin': ('Daily Rain', 'in'),
'dewPoint': ('Dew Point', '°F'),
'eventrainin': ('Event Rain', 'in'),
'feelsLike': ('Feels Like', '°F'),
'hourlyrainin': ('Hourly Rain Rate', 'in/hr'),
'humidity': ('Humidity', '%'),
'humidityin': ('Humidity In', '%'),
'lastRain': ('Last Rain', ''),
'maxdailygust': ('Max Gust', 'mph'),
'monthlyrainin': ('Monthly Rain', 'in'),
'solarradiation': ('Solar Rad', 'W/m^2'),
'tempf': ('Temp', '°F'),
'tempinf': ('Inside Temp', '°F'),
'totalrainin': ('Lifetime Rain', 'in'),
'uv': ('uv', 'Index'),
'weeklyrainin': ('Weekly Rain', 'in'),
'winddir': ('Wind Dir', '°'),
'winddir_avg10m': ('Wind Dir Avg 10m', '°'),
'winddir_avg2m': ('Wind Dir Avg 2m', 'mph'),
'windgustdir': ('Gust Dir', '°'),
'windgustmph': ('Wind Gust', 'mph'),
'windspdmph_avg10m': ('Wind Avg 10m', 'mph'),
'windspdmph_avg2m': ('Wind Avg 2m', 'mph'),
'windspeedmph': ('Wind Speed', 'mph'),
'yearlyrainin': ('Yearly Rain', 'in'),
}
CONFIG_SCHEMA = vol.Schema({
@@ -70,8 +69,6 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(
CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_UNIT_SYSTEM):
vol.In([UNITS_SI, UNITS_US]),
})
}, extra=vol.ALLOW_EXTRA)
@@ -111,8 +108,7 @@ async def async_setup_entry(hass, config_entry):
config_entry.data[CONF_API_KEY],
config_entry.data[CONF_APP_KEY], session),
config_entry.data.get(
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)),
config_entry.data.get(CONF_UNIT_SYSTEM))
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)))
hass.loop.create_task(ambient.ws_connect())
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
except WebsocketConnectionError as err:
@@ -139,9 +135,7 @@ async def async_unload_entry(hass, config_entry):
class AmbientStation:
"""Define a class to handle the Ambient websocket."""
def __init__(
self, hass, config_entry, client, monitored_conditions,
unit_system):
def __init__(self, hass, config_entry, client, monitored_conditions):
"""Initialize."""
self._config_entry = config_entry
self._hass = hass
@@ -149,7 +143,6 @@ class AmbientStation:
self.client = client
self.monitored_conditions = monitored_conditions
self.stations = {}
self.unit_system = unit_system
async def ws_connect(self):
"""Register handlers and connect to the websocket."""
@@ -8,6 +8,3 @@ CONF_APP_KEY = 'app_key'
DATA_CLIENT = 'data_client'
TOPIC_UPDATE = 'update'
UNITS_SI = 'si'
UNITS_US = 'us'
@@ -12,14 +12,11 @@ from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, UNITS_US)
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE
DEPENDENCIES = ['ambient_station']
_LOGGER = logging.getLogger(__name__)
UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1}
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
@@ -31,20 +28,10 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up an Ambient PWS sensor based on a config entry."""
ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
if ambient.unit_system:
sys_units = ambient.unit_system
elif hass.config.units.is_metric:
sys_units = UNITS_SI
else:
sys_units = UNITS_US
sensor_list = []
for mac_address, station in ambient.stations.items():
for condition in ambient.monitored_conditions:
name, unit = SENSOR_TYPES[condition]
if isinstance(unit, list):
unit = unit[UNIT_SYSTEM[sys_units]]
sensor_list.append(
AmbientWeatherSensor(
ambient, mac_address, station[ATTR_NAME], condition, name,
@@ -58,7 +45,7 @@ class AmbientWeatherSensor(Entity):
def __init__(
self, ambient, mac_address, station_name, sensor_type, sensor_name,
units):
unit):
"""Initialize the sensor."""
self._ambient = ambient
self._async_unsub_dispatcher_connect = None
@@ -67,7 +54,7 @@ class AmbientWeatherSensor(Entity):
self._sensor_type = sensor_type
self._state = None
self._station_name = station_name
self._units = units
self._unit = unit
@property
def name(self):
@@ -87,7 +74,7 @@ class AmbientWeatherSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._units
return self._unit
@property
def unique_id(self):
+2 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyfoscam==1.2']
REQUIREMENTS = ['libpyfoscam==1.0']
CONF_IP = 'ip'
@@ -43,7 +43,7 @@ class FoscamCam(Camera):
def __init__(self, device_info):
"""Initialize a Foscam camera."""
from foscam import FoscamCamera
from libpyfoscam import FoscamCamera
super(FoscamCam, self).__init__()
@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20190202.0']
REQUIREMENTS = ['home-assistant-frontend==20190203.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
+2 -4
View File
@@ -25,8 +25,7 @@ from .auth import setup_auth
from .ban import setup_bans
from .cors import setup_cors
from .real_ip import setup_real_ip
from .static import (
CachingFileResponse, CachingStaticResource, staticresource_middleware)
from .static import CachingFileResponse, CachingStaticResource
# Import as alias
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
@@ -192,8 +191,7 @@ class HomeAssistantHTTP:
use_x_forwarded_for, trusted_proxies, trusted_networks,
login_threshold, is_ban_enabled, ssl_profile):
"""Initialize the HTTP Home Assistant server."""
app = self.app = web.Application(
middlewares=[staticresource_middleware])
app = self.app = web.Application(middlewares=[])
# This order matters
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
+1 -22
View File
@@ -1,15 +1,10 @@
"""Static file handling for HTTP component."""
import re
from aiohttp import hdrs
from aiohttp.web import FileResponse, middleware
from aiohttp.web import FileResponse
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource
from yarl import URL
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
@@ -56,19 +51,3 @@ class CachingFileResponse(FileResponse):
# Overwriting like this because __init__ can change implementation.
self._sendfile = sendfile
@middleware
async def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets."""
path = request.path
if not path.startswith('/static/') and not path.startswith('/frontend'):
return await handler(request)
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
if fingerprinted:
request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups())
return await handler(request)
@@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .config_flow import SmartThingsFlowHandler # noqa
from .const import (
CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
from .smartapp import (
setup_smartapp, setup_smartapp_endpoint, validate_installed_app)
@@ -154,6 +154,19 @@ class DeviceBroker:
continue
device.status.apply_attribute_update(
evt.component_id, evt.capability, evt.attribute, evt.value)
# Fire events for buttons
if evt.capability == 'button' and evt.attribute == 'button':
data = {
'component_id': evt.component_id,
'device_id': evt.device_id,
'location_id': evt.location_id,
'value': evt.value,
'name': device.label
}
self._hass.bus.async_fire(EVENT_BUTTON, data)
_LOGGER.debug("Fired button event: %s", data)
updated_devices.add(device.device_id)
_LOGGER.debug("Update received with %s events and updated %s devices",
len(req.events), len(updated_devices))
@@ -12,6 +12,7 @@ CONF_LOCATION_ID = 'location_id'
DATA_MANAGER = 'manager'
DATA_BROKERS = 'brokers'
DOMAIN = 'smartthings'
EVENT_BUTTON = "smartthings.button"
SIGNAL_SMARTTHINGS_UPDATE = 'smartthings_update'
SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_'
SETTINGS_INSTANCE_ID = "hassInstanceId"
@@ -25,6 +26,7 @@ SUPPORTED_PLATFORMS = [
]
SUPPORTED_CAPABILITIES = [
'accelerationSensor',
'button',
'colorControl',
'colorTemperature',
'contactSensor',
+1 -1
View File
@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 87
PATCH_VERSION = '0b2'
PATCH_VERSION = '0b5'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
+4 -4
View File
@@ -526,7 +526,7 @@ hole==0.3.0
holidays==0.9.9
# homeassistant.components.frontend
home-assistant-frontend==20190202.0
home-assistant-frontend==20190203.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.2
@@ -605,6 +605,9 @@ libnacl==1.6.1
# homeassistant.components.dyson
libpurecoollink==0.4.2
# homeassistant.components.camera.foscam
libpyfoscam==1.0
# homeassistant.components.device_tracker.mikrotik
librouteros==2.2.0
@@ -1014,9 +1017,6 @@ pyflunearyou==1.0.1
# homeassistant.components.light.futurenow
pyfnip==0.2
# homeassistant.components.camera.foscam
pyfoscam==1.2
# homeassistant.components.fritzbox
pyfritzhome==0.4.0
+1 -1
View File
@@ -113,7 +113,7 @@ hdate==0.8.7
holidays==0.9.9
# homeassistant.components.frontend
home-assistant-frontend==20190202.0
home-assistant-frontend==20190203.0
# homeassistant.components.homekit_controller
homekit==0.12.2
+18 -13
View File
@@ -1,8 +1,11 @@
"""The test for the History Statistics sensor platform."""
# pylint: disable=protected-access
from datetime import timedelta
from datetime import datetime, timedelta
import unittest
from unittest.mock import patch
import pytest
import pytz
from homeassistant.helpers import template
from homeassistant.const import STATE_UNKNOWN
from homeassistant.setup import setup_component
@@ -12,7 +15,6 @@ from homeassistant.helpers.template import Template
import homeassistant.util.dt as dt_util
from tests.common import init_recorder_component, get_test_home_assistant
import pytest
class TestHistoryStatsSensor(unittest.TestCase):
@@ -50,19 +52,22 @@ class TestHistoryStatsSensor(unittest.TestCase):
def test_period_parsing(self):
"""Test the conversion from templates to period."""
today = Template('{{ now().replace(hour=0).replace(minute=0)'
'.replace(second=0) }}', self.hass)
duration = timedelta(hours=2, minutes=1)
now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc)
with patch.dict(template.ENV.globals, {'now': lambda: now}):
print(dt_util.now())
today = Template('{{ now().replace(hour=0).replace(minute=0)'
'.replace(second=0) }}', self.hass)
duration = timedelta(hours=2, minutes=1)
sensor1 = HistoryStatsSensor(
self.hass, 'test', 'on', today, None, duration, 'time', 'test')
sensor2 = HistoryStatsSensor(
self.hass, 'test', 'on', None, today, duration, 'time', 'test')
sensor1 = HistoryStatsSensor(
self.hass, 'test', 'on', today, None, duration, 'time', 'test')
sensor2 = HistoryStatsSensor(
self.hass, 'test', 'on', None, today, duration, 'time', 'test')
sensor1.update_period()
sensor1_start, sensor1_end = sensor1._period
sensor2.update_period()
sensor2_start, sensor2_end = sensor2._period
sensor1.update_period()
sensor1_start, sensor1_end = sensor1._period
sensor2.update_period()
sensor2_start, sensor2_end = sensor2._period
# Start = 00:00:00
assert sensor1_start.hour == 0
+14 -8
View File
@@ -254,14 +254,16 @@ def device_factory_fixture():
@pytest.fixture(name="event_factory")
def event_factory_fixture():
"""Fixture for creating mock devices."""
def _factory(device_id, event_type="DEVICE_EVENT"):
def _factory(device_id, event_type="DEVICE_EVENT", capability='',
attribute='Updated', value='Value'):
event = Mock()
event.event_type = event_type
event.device_id = device_id
event.component_id = 'main'
event.capability = ''
event.attribute = 'Updated'
event.value = 'Value'
event.capability = capability
event.attribute = attribute
event.value = value
event.location_id = str(uuid4())
return event
return _factory
@@ -269,11 +271,15 @@ def event_factory_fixture():
@pytest.fixture(name="event_request_factory")
def event_request_factory_fixture(event_factory):
"""Fixture for creating mock smartapp event requests."""
def _factory(device_ids):
def _factory(device_ids=None, events=None):
request = Mock()
request.installed_app_id = uuid4()
request.events = [event_factory(id) for id in device_ids]
request.events.append(event_factory(uuid4()))
request.events.append(event_factory(device_ids[0], event_type="OTHER"))
if events is None:
events = []
if device_ids:
events.extend([event_factory(id) for id in device_ids])
events.append(event_factory(uuid4()))
events.append(event_factory(device_ids[0], event_type="OTHER"))
request.events = events
return request
return _factory
@@ -6,9 +6,10 @@ real HTTP calls are not initiated during testing.
"""
from pysmartthings import Attribute, Capability
from homeassistant.components.binary_sensor import DEVICE_CLASSES
from homeassistant.components.smartthings import DeviceBroker, binary_sensor
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE)
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_CAPABILITIES)
from homeassistant.config_entries import (
CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry)
from homeassistant.const import ATTR_FRIENDLY_NAME
@@ -32,6 +33,18 @@ async def _setup_platform(hass, *devices):
return config_entry
async def test_mapping_integrity():
"""Test ensures the map dicts have proper integrity."""
# Ensure every CAPABILITY_TO_ATTRIB key is in SUPPORTED_CAPABILITIES
# Ensure every CAPABILITY_TO_ATTRIB value is in ATTRIB_TO_CLASS keys
for capability, attrib in binary_sensor.CAPABILITY_TO_ATTRIB.items():
assert capability in SUPPORTED_CAPABILITIES, capability
assert attrib in binary_sensor.ATTRIB_TO_CLASS.keys(), attrib
# Ensure every ATTRIB_TO_CLASS value is in DEVICE_CLASSES
for device_class in binary_sensor.ATTRIB_TO_CLASS.values():
assert device_class in DEVICE_CLASSES
async def test_async_setup_platform():
"""Test setup platform does nothing (it uses config entries)."""
await binary_sensor.async_setup_platform(None, None, None)
@@ -58,15 +71,15 @@ async def test_entity_and_device_attributes(hass, device_factory):
# Act
await _setup_platform(hass, device)
# Assert
entity = entity_registry.async_get('binary_sensor.motion_sensor_1_motion')
assert entity
assert entity.unique_id == device.device_id + '.' + Attribute.motion
device_entry = device_registry.async_get_device(
entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion')
assert entry
assert entry.unique_id == device.device_id + '.' + Attribute.motion
entry = device_registry.async_get_device(
{(DOMAIN, device.device_id)}, [])
assert device_entry
assert device_entry.name == device.label
assert device_entry.model == device.device_type_name
assert device_entry.manufacturer == 'Unavailable'
assert entry
assert entry.name == device.label
assert entry.model == device.device_type_name
assert entry.manufacturer == 'Unavailable'
async def test_update_from_signal(hass, device_factory):
+30 -1
View File
@@ -8,7 +8,8 @@ import pytest
from homeassistant.components import smartthings
from homeassistant.components.smartthings.const import (
DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE,
SUPPORTED_PLATFORMS)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -181,3 +182,31 @@ async def test_event_handler_ignores_other_installed_app(
await hass.async_block_till_done()
assert not called
async def test_event_handler_fires_button_events(
hass, device_factory, event_factory, event_request_factory):
"""Test the event handler fires button events."""
device = device_factory('Button 1', ['button'])
event = event_factory(device.device_id, capability='button',
attribute='button', value='pushed')
request = event_request_factory(events=[event])
called = False
def handler(evt):
nonlocal called
called = True
assert evt.data == {
'component_id': 'main',
'device_id': device.device_id,
'location_id': event.location_id,
'value': 'pushed',
'name': device.label
}
hass.bus.async_listen(EVENT_BUTTON, handler)
broker = smartthings.DeviceBroker(
hass, [device], request.installed_app_id)
await broker.event_handler(request, None, None)
await hass.async_block_till_done()
assert called