From 4ce0813685b3a6be50eaba961effdd85345ee239 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 10:51:05 +0100 Subject: [PATCH 1/9] Add google pubsub component --- .../components/google_pubsub/__init__.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 homeassistant/components/google_pubsub/__init__.py diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py new file mode 100644 index 00000000000000..7353d26c0efefb --- /dev/null +++ b/homeassistant/components/google_pubsub/__init__.py @@ -0,0 +1,78 @@ +import datetime +import json +import logging +import os +from typing import Dict, Any + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, Event, State +from homeassistant.helpers.entityfilter import FILTER_SCHEMA + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['google-cloud-pubsub==0.39.1'] + +DOMAIN = 'google_pubsub' + +CONF_PROJECT_ID = 'project_id' +CONF_TOPIC_NAME = 'topic_name' +CONF_SERVICE_PRINCIPAL = 'credentials_json' +CONF_FILTER = 'filter' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Required(CONF_TOPIC_NAME): cv.string, + vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, + vol.Required(CONF_FILTER): FILTER_SCHEMA + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): + from google.cloud import pubsub_v1 + + config = yaml_config.get(DOMAIN, {}) + project_id = config[CONF_PROJECT_ID] + topic_name = config[CONF_TOPIC_NAME] + service_principal_path = os.path.join(hass.config.config_dir, + config[CONF_SERVICE_PRINCIPAL]) + + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = service_principal_path + + entities_filter = config[CONF_FILTER] + + publisher = pubsub_v1.PublisherClient() + topic_path = publisher.topic_path(project_id, topic_name) + + encoder = DateTimeJSONEncoder() + + def send_to_pubsub(event: Event): + state: State = event.data.get('new_state') + if (state is None + or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) + or not entities_filter(state.entity_id)): + return + + as_dict = state.as_dict() + data = json.dumps( + obj=as_dict, + default=encoder.encode + ).encode('utf-8') + + publisher.publish(topic_path, data=data) + + hass.bus.listen(EVENT_STATE_CHANGED, send_to_pubsub) + + return True + + +class DateTimeJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + else: + return super(DateTimeJSONEncoder, self).default(obj) From c6dd89a229fd326035b82818b4512596f671934d Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 10:51:06 +0100 Subject: [PATCH 2/9] Add tests and requirements --- .../components/google_pubsub/__init__.py | 3 ++- requirements_all.txt | 3 +++ tests/components/google_pubsub/test_pubsub.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/components/google_pubsub/test_pubsub.py diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 7353d26c0efefb..d8021f870cf69a 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -7,7 +7,8 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.const import ( + EVENT_STATE_CHANGED, STATE_UNKNOWN, STATE_UNAVAILABLE) from homeassistant.core import HomeAssistant, Event, State from homeassistant.helpers.entityfilter import FILTER_SCHEMA diff --git a/requirements_all.txt b/requirements_all.txt index 7172a7027efbd3..32c56ec068c74a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,6 +456,9 @@ gntp==1.0.3 # homeassistant.components.google google-api-python-client==1.6.4 +# homeassistant.components.google_pubsub +google-cloud-pubsub==0.39.1 + # homeassistant.components.sensor.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/google_pubsub/test_pubsub.py b/tests/components/google_pubsub/test_pubsub.py new file mode 100644 index 00000000000000..522d9d8d97dae9 --- /dev/null +++ b/tests/components/google_pubsub/test_pubsub.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from homeassistant.components.google_pubsub import ( + DateTimeJSONEncoder as victim) + + +class TestDateTimeJSONEncoder(object): + + def test_datetime(self): + time = datetime(2019, 1, 13, 12, 30, 5) + assert victim().encode(time) == '"2019-01-13T12:30:05.000000"' + + def test_no_datetime(self): + assert victim().encode(42) == '42' + + def test_nested(self): + assert victim().encode({'foo': 'bar'}) == '{"foo": "bar"}' From f8f3d6fc7d385b3290ed0ebbb2dc94423ca4236f Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 11:00:47 +0100 Subject: [PATCH 3/9] Make python3.5 compatible --- homeassistant/components/google_pubsub/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index d8021f870cf69a..d18d5acd8d70c1 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -52,7 +52,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): encoder = DateTimeJSONEncoder() def send_to_pubsub(event: Event): - state: State = event.data.get('new_state') + state = event.data.get('new_state') if (state is None or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) or not entities_filter(state.entity_id)): From cb66716b5726cf61afe5a87430d466134eb4f9c4 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 11:01:22 +0100 Subject: [PATCH 4/9] Fix linting --- homeassistant/components/google_pubsub/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index d18d5acd8d70c1..513949e8f14a5f 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -9,7 +9,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNKNOWN, STATE_UNAVAILABLE) -from homeassistant.core import HomeAssistant, Event, State +from homeassistant.core import HomeAssistant, Event from homeassistant.helpers.entityfilter import FILTER_SCHEMA _LOGGER = logging.getLogger(__name__) From a85faf6dac0ac5d98ad83c880deff069c79a1752 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 11:31:07 +0100 Subject: [PATCH 5/9] Fix pubsub test --- tests/components/google_pubsub/test_pubsub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/google_pubsub/test_pubsub.py b/tests/components/google_pubsub/test_pubsub.py index 522d9d8d97dae9..882ef7929a8bbb 100644 --- a/tests/components/google_pubsub/test_pubsub.py +++ b/tests/components/google_pubsub/test_pubsub.py @@ -8,7 +8,7 @@ class TestDateTimeJSONEncoder(object): def test_datetime(self): time = datetime(2019, 1, 13, 12, 30, 5) - assert victim().encode(time) == '"2019-01-13T12:30:05.000000"' + assert victim().encode(time) == '"2019-01-13T12:30:05"' def test_no_datetime(self): assert victim().encode(42) == '42' From 3a6fdf38711e060dc7e4909016fad271422d04c6 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 13:25:37 +0100 Subject: [PATCH 6/9] Code review comments --- .../components/google_pubsub/__init__.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 513949e8f14a5f..03fd90899ffdbb 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -2,14 +2,14 @@ import json import logging import os -from typing import Dict, Any +from typing import Any, Dict import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_STATE_CHANGED, STATE_UNKNOWN, STATE_UNAVAILABLE) -from homeassistant.core import HomeAssistant, Event + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.core import Event, HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -34,6 +34,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): + """Activate Google Pub/Sub component.""" from google.cloud import pubsub_v1 config = yaml_config.get(DOMAIN, {}) @@ -42,6 +43,10 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): service_principal_path = os.path.join(hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL]) + if not os.path.isfile(service_principal_path): + _LOGGER.error("Path to credentials file cannot be found") + return False + os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = service_principal_path entities_filter = config[CONF_FILTER] @@ -52,6 +57,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): encoder = DateTimeJSONEncoder() def send_to_pubsub(event: Event): + """Sends states to Pub/Sub.""" state = event.data.get('new_state') if (state is None or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) @@ -72,8 +78,10 @@ def send_to_pubsub(event: Event): class DateTimeJSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, datetime.datetime): - return obj.isoformat() - else: - return super(DateTimeJSONEncoder, self).default(obj) + """Encodes python objects, adding the encoding of + datetime objects as isoformat. + """ + def default(self, o): + if isinstance(o, datetime.datetime): + return o.isoformat() + return super(DateTimeJSONEncoder, self).default(o) From e96841026f42139fac77ccf758f3da1e3e1a1bbe Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 13 Jan 2019 21:50:36 +0100 Subject: [PATCH 7/9] Add missing docstrings --- .../components/google_pubsub/__init__.py | 22 ++++++++++++++----- tests/components/google_pubsub/test_pubsub.py | 5 +++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 03fd90899ffdbb..240c7f2d318d99 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -1,3 +1,9 @@ +""" +Support for Google Cloud Pub/Sub. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/google_pubsub/ +""" import datetime import json import logging @@ -35,7 +41,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Pub/Sub component.""" - from google.cloud import pubsub_v1 + from google.cloud import pubsub_v1 # pylint: disable=E0611 config = yaml_config.get(DOMAIN, {}) project_id = config[CONF_PROJECT_ID] @@ -52,12 +58,13 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): entities_filter = config[CONF_FILTER] publisher = pubsub_v1.PublisherClient() - topic_path = publisher.topic_path(project_id, topic_name) + topic_path = publisher.topic_path(project_id, # pylint: disable=E1101 + topic_name) encoder = DateTimeJSONEncoder() def send_to_pubsub(event: Event): - """Sends states to Pub/Sub.""" + """Send states to Pub/Sub.""" state = event.data.get('new_state') if (state is None or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE) @@ -78,10 +85,13 @@ def send_to_pubsub(event: Event): class DateTimeJSONEncoder(json.JSONEncoder): - """Encodes python objects, adding the encoding of - datetime objects as isoformat. + """Encode python objects. + + Additonaly add encoding for datetime objects as isoformat. """ - def default(self, o): + + def default(self, o): # pylint: disable=E0202 + """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() return super(DateTimeJSONEncoder, self).default(o) diff --git a/tests/components/google_pubsub/test_pubsub.py b/tests/components/google_pubsub/test_pubsub.py index 882ef7929a8bbb..b97dc33f8b14a8 100644 --- a/tests/components/google_pubsub/test_pubsub.py +++ b/tests/components/google_pubsub/test_pubsub.py @@ -1,3 +1,4 @@ +"""The tests for the Google Pub/Sub component.""" from datetime import datetime from homeassistant.components.google_pubsub import ( @@ -5,13 +6,17 @@ class TestDateTimeJSONEncoder(object): + """Bundle for DateTimeJSONEncoder tests.""" def test_datetime(self): + """Test datetime encoding.""" time = datetime(2019, 1, 13, 12, 30, 5) assert victim().encode(time) == '"2019-01-13T12:30:05"' def test_no_datetime(self): + """Test integer encoding.""" assert victim().encode(42) == '42' def test_nested(self): + """Test dictionary encoding.""" assert victim().encode({'foo': 'bar'}) == '{"foo": "bar"}' From 81d7b55b523faadc2eb45dbac7cfaa7029d1dd22 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Fri, 8 Feb 2019 10:53:59 +0100 Subject: [PATCH 8/9] Update requirements_all --- requirements_all.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 799021b04ad7a1..acec5f3a0d7556 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -471,14 +471,12 @@ gntp==1.0.3 # homeassistant.components.google google-api-python-client==1.6.4 - # homeassistant.components.google_pubsub google-cloud-pubsub==0.39.1 # homeassistant.components.googlehome googledevices==1.0.2 - # homeassistant.components.sensor.google_travel_time googlemaps==2.5.1 From c08a13aebad80187ce4253942bd89d560fc22946 Mon Sep 17 00:00:00 2001 From: Tim van Cann Date: Sun, 10 Feb 2019 17:18:16 +0100 Subject: [PATCH 9/9] Code review comment - Remove pylint ignores - Don't modify global environment --- .../components/google_pubsub/__init__.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 240c7f2d318d99..af8bb60f8b1dc6 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -41,9 +41,9 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Pub/Sub component.""" - from google.cloud import pubsub_v1 # pylint: disable=E0611 + from google.cloud import pubsub_v1 - config = yaml_config.get(DOMAIN, {}) + config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] service_principal_path = os.path.join(hass.config.config_dir, @@ -53,11 +53,13 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): _LOGGER.error("Path to credentials file cannot be found") return False - os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = service_principal_path - entities_filter = config[CONF_FILTER] - publisher = pubsub_v1.PublisherClient() + publisher = (pubsub_v1 + .PublisherClient + .from_service_account_json(service_principal_path) + ) + topic_path = publisher.topic_path(project_id, # pylint: disable=E1101 topic_name) @@ -87,11 +89,11 @@ def send_to_pubsub(event: Event): class DateTimeJSONEncoder(json.JSONEncoder): """Encode python objects. - Additonaly add encoding for datetime objects as isoformat. + Additionally add encoding for datetime objects as isoformat. """ def default(self, o): # pylint: disable=E0202 """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() - return super(DateTimeJSONEncoder, self).default(o) + return super().default(o)