diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py new file mode 100644 index 00000000000..af8bb60f8b1 --- /dev/null +++ b/homeassistant/components/google_pubsub/__init__.py @@ -0,0 +1,99 @@ +""" +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 +import os +from typing import Any, Dict + +import voluptuous as vol + +from homeassistant.const import ( + 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__) + +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]): + """Activate Google Pub/Sub component.""" + from google.cloud import pubsub_v1 + + 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, + config[CONF_SERVICE_PRINCIPAL]) + + if not os.path.isfile(service_principal_path): + _LOGGER.error("Path to credentials file cannot be found") + return False + + entities_filter = config[CONF_FILTER] + + publisher = (pubsub_v1 + .PublisherClient + .from_service_account_json(service_principal_path) + ) + + topic_path = publisher.topic_path(project_id, # pylint: disable=E1101 + topic_name) + + encoder = DateTimeJSONEncoder() + + def send_to_pubsub(event: Event): + """Send states to Pub/Sub.""" + 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): + """Encode python objects. + + 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().default(o) diff --git a/requirements_all.txt b/requirements_all.txt index 5911eb938b9..683f69ba29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -471,6 +471,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.googlehome googledevices==1.0.2 diff --git a/tests/components/google_pubsub/test_pubsub.py b/tests/components/google_pubsub/test_pubsub.py new file mode 100644 index 00000000000..b97dc33f8b1 --- /dev/null +++ b/tests/components/google_pubsub/test_pubsub.py @@ -0,0 +1,22 @@ +"""The tests for the Google Pub/Sub component.""" +from datetime import datetime + +from homeassistant.components.google_pubsub import ( + DateTimeJSONEncoder as victim) + + +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"}'