Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions homeassistant/components/google_pubsub/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
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
Comment thread
timvancann marked this conversation as resolved.

_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]):
Comment thread
timvancann marked this conversation as resolved.
"""Activate Google Pub/Sub component."""
from google.cloud import pubsub_v1 # pylint: disable=E0611

config = yaml_config.get(DOMAIN, {})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would the DOMAIN key be missing in the config dict? Why do we need to use dict.get?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be missing. Changed it to dict[]

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

os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = service_principal_path
Comment thread
timvancann marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some way to pass this without modifying the global environment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some digging, and yes, there is 😄. Fixed it


entities_filter = config[CONF_FILTER]

publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, # pylint: disable=E1101
Comment thread
MartinHjelmare marked this conversation as resolved.
topic_name)

encoder = DateTimeJSONEncoder()

def send_to_pubsub(event: Event):
Comment thread
timvancann marked this conversation as resolved.
"""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)
Comment thread
timvancann marked this conversation as resolved.

hass.bus.listen(EVENT_STATE_CHANGED, send_to_pubsub)

return True


class DateTimeJSONEncoder(json.JSONEncoder):
Comment thread
timvancann marked this conversation as resolved.
"""Encode python objects.

Additonaly add encoding for datetime objects as isoformat.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"""

def default(self, o): # pylint: disable=E0202
"""Implement encoding logic."""
if isinstance(o, datetime.datetime):
return o.isoformat()
return super(DateTimeJSONEncoder, self).default(o)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think super() is enough with Python 3.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely right.

3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 22 additions & 0 deletions tests/components/google_pubsub/test_pubsub.py
Original file line number Diff line number Diff line change
@@ -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"}'