Skip to content

Commit

Permalink
Maintenance update (#29)
Browse files Browse the repository at this point in the history
* Share event listeners for multiple sensors

and dispatch all event filters for each sensor derived from a specific event.

* Parse boolean values in event data

so "true"/"false" are correctly processed.

* Add travis CI config

- To check lint stage with pre-commit
- To produce a zip file for HACS on deploy stage when releasing

* Update requirements

* Disable listening to EVENT_STATE_CHANGED even with manual YAML

* Add version tag to hacs.json

and also declare a zip release file

* Update CHANGELOG.md

* Add version & issue_tracker tags to manifest.json

* Bump minor version and update CHANGELOG.md
  • Loading branch information
azogue authored Mar 6, 2021
1 parent 7a050eb commit 6e600c1
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 40 deletions.
33 changes: 33 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
dist: bionic
language: python
cache:
pip: true
directories:
- $HOME/.cache/pre-commit
env:
- TEST_CMD="pre-commit run --all-files"
install:
- pip install pre-commit
- pre-commit install-hooks
script:
- $TEST_CMD

matrix:
include:
- name: "lint"
python: 3.8

before_deploy:
- cd custom_components/eventsensor
- zip -r eventsensor .
deploy:
provider: releases
api_key: $GITHUB_OAUTH_TOKEN
tag_name: $TRAVIS_TAG
name: $TRAVIS_TAG
file: eventsensor.zip
skip_cleanup: true
draft: true
prerelease: true
on:
tags: true
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Changelog

## [v2.3.0](https://github.com/azogue/eventsensor/tree/v2.3.0) (2020-08-06)
## [v2.4.0](https://github.com/azogue/eventsensor/tree/v2.4.0) (2021-03-07)

[Full Changelog](https://github.com/azogue/eventsensor/compare/v2.2.0...v2.3.0)
[Full Changelog](https://github.com/azogue/eventsensor/compare/v2.2.0...v2.4.0)

**Implemented enhancements:**
**Changes:**

- Disable listening to EVENT_STATE_CHANGED even with manual YAML.
- Add version tag to hacs.json & manifest.json, and declare a zip release file.
- Share event listeners for multiple sensors, and dispatch all event filters for each sensor derived from a specific event.
- Parse boolean values in event data, so "true"/"false" are processed as expected.
- Add default mapping templates for Philips Hue Smart Button.

## [v2.2.0](https://github.com/azogue/eventsensor/tree/v2.2.0) (2020-08-02)
Expand Down
4 changes: 4 additions & 0 deletions custom_components/eventsensor/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def parse_numbers(raw_item):
if isinstance(raw_item, dict):
return {parse_numbers(k): parse_numbers(v) for k, v in raw_item.items()}

if raw_item in ("true", "True", True):
return True
elif raw_item in ("false", "False", False):
return False
try:
return int(raw_item)
except ValueError:
Expand Down
4 changes: 3 additions & 1 deletion custom_components/eventsensor/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"name": "EventSensor",
"config_flow": true,
"documentation": "https://github.com/azogue/eventsensor",
"issue_tracker": "https://github.com/azogue/eventsensor/issues",
"requirements": [],
"dependencies": [],
"codeowners": [
"@azogue"
]
],
"version": "2.4.0"
}
149 changes: 118 additions & 31 deletions custom_components/eventsensor/sensor.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"""Event sensor."""
import logging
from typing import Any, Callable, List
from typing import Any, Callable, Dict, List

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
from homeassistant.const import CONF_EVENT, CONF_EVENT_DATA, CONF_NAME, CONF_STATE
from homeassistant.const import (
CONF_EVENT,
CONF_EVENT_DATA,
CONF_NAME,
CONF_STATE,
EVENT_STATE_CHANGED,
)
from homeassistant.core import callback
from homeassistant.helpers.config_validation import string
from homeassistant.helpers.event import Event
Expand Down Expand Up @@ -52,7 +58,7 @@ async def async_setup_platform(
Left just to read deprecated manual configuration.
"""
if config:
if config and config.get(CONF_EVENT) != EVENT_STATE_CHANGED:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, data=config, context={"source": SOURCE_IMPORT}
Expand All @@ -66,6 +72,14 @@ async def async_setup_platform(
config.get(CONF_EVENT),
)

elif config and config.get(CONF_EVENT) == EVENT_STATE_CHANGED:
_LOGGER.error(
"Listen to the `%s` event is forbidden, "
"so the EventSensor '%s' won't be created :(",
EVENT_STATE_CHANGED,
config.get(CONF_NAME),
)

return True


Expand All @@ -78,7 +92,20 @@ async def async_setup_entry(
if DOMAIN_DATA not in hass.data:
hass.data[DOMAIN_DATA] = {}

async_add_entities([EventSensor(config_entry.unique_id, config_entry.data)], False)
if "_dispatcher" not in hass.data[DOMAIN_DATA]:
hass.data[DOMAIN_DATA]["_dispatcher"] = EventSensorDispatcher()

async_add_entities(
[
EventSensor(
config_entry.entry_id,
config_entry.unique_id,
config_entry.data,
hass.data[DOMAIN_DATA]["_dispatcher"],
)
],
False,
)

# add an update listener to enable edition by OptionsFlow
hass.data[DOMAIN_DATA][config_entry.entry_id] = config_entry.add_update_listener(
Expand All @@ -104,22 +131,81 @@ async def update_listener(hass: HomeAssistantType, entry: ConfigEntry):
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))


class EventSensorDispatcher:
"""Dispatcher for EventSensors."""

def __init__(self):
"""Set up the event sensor dispatcher."""
self._listeners: Dict[str, Callable[[], None]] = {}
self._filters: Dict[str, Dict[str, Any]] = {}

async def async_add_entry(
self,
hass: HomeAssistantType,
entry_id: str,
event_type: str,
event_data_filter: Dict[str, Any],
callback_update_sensor: Callable[[Event], None],
) -> None:
"""Add event listener when adding entity to Home Assistant."""
if event_type not in self._filters:
# requires a new listener
self._filters[event_type] = {}

assert entry_id not in self._filters[event_type]
self._filters[event_type][entry_id] = (
event_data_filter,
callback_update_sensor,
)

@callback
def async_dispatch_by_event_type(event: Event):
"""Dispatch sensor updates when event is received."""
for ev_filter, cb_sensor in self._filters[event.event_type].values():
if check_dict_is_contained_in_another(ev_filter, event.data):
cb_sensor(event)

if event_type not in self._listeners:
# requires a new listener
self._listeners[event_type] = hass.bus.async_listen(
event_type, async_dispatch_by_event_type
)
_LOGGER.info("Added event listener for '%s'", event_type)

async def async_remove_entry(self, event_type: str, entry_id: str):
"""Remove listeners when removing entity from Home Assistant."""
assert event_type in self._filters
assert entry_id in self._filters[event_type]
self._filters[event_type].pop(entry_id)
if not self._filters[event_type]:
self._filters.pop(event_type)
assert event_type in self._listeners
self._listeners.pop(event_type)()
_LOGGER.debug("Removed event listener for '%s'", event_type)


class EventSensor(RestoreEntity):
"""Sensor to store information originated with events."""

should_poll = False
icon = "mdi:bullseye-arrow"

def __init__(self, unique_id: str, sensor_data: dict):
def __init__(
self,
entry_id: str,
unique_id: str,
sensor_data: Dict[str, Any],
dispatcher: EventSensorDispatcher,
):
"""Set up a new sensor mirroring some event."""
self._dispatcher = dispatcher
self._entry_id = entry_id
self._unique_id = unique_id
self._name = sensor_data.get(CONF_NAME)
self._event = sensor_data.get(CONF_EVENT)
self._state_key = sensor_data.get(CONF_STATE)
self._event_data = parse_numbers(sensor_data.get(CONF_EVENT_DATA, {}))
self._state_map = parse_numbers(sensor_data.get(CONF_STATE_MAP, {}))

self._event_listener = None
self._state = None
self._attributes = {}

Expand Down Expand Up @@ -153,29 +239,32 @@ async def async_added_to_hass(self) -> None:

@callback
def async_update_sensor(event: Event):
"""Update state when event is received."""
if check_dict_is_contained_in_another(self._event_data, event.data):
# Extract new state
new_state = extract_state_from_event(self._state_key, event.data)

# Apply custom state mapping
if new_state in self._state_map:
new_state = self._state_map[new_state]

self._state = new_state
self._attributes = {
**event.data,
"origin": event.origin.name,
"time_fired": event.time_fired,
}
_LOGGER.debug("%s: New state: %s", self.entity_id, self._state)
self.async_write_ha_state()
"""Update state when a valid event is received."""
# Extract new state
new_state = extract_state_from_event(self._state_key, event.data)

# Apply custom state mapping
if new_state in self._state_map:
new_state = self._state_map[new_state]

self._state = new_state
self._attributes = {
**event.data,
"origin": event.origin.name,
"time_fired": event.time_fired,
}
_LOGGER.debug("%s: New state: %s", self.entity_id, self._state)
self.async_write_ha_state()

# Listen for event
self._event_listener = self.hass.bus.async_listen(
self._event, async_update_sensor
await self._dispatcher.async_add_entry(
self.hass,
self._entry_id,
self._event,
self._event_data,
async_update_sensor,
)
_LOGGER.debug(
_LOGGER.info(
"%s: Added sensor listening to '%s' with event data: %s",
self.entity_id,
self._event,
Expand All @@ -184,7 +273,5 @@ def async_update_sensor(event: Event):

async def async_will_remove_from_hass(self):
"""Remove listeners when removing entity from Home Assistant."""
if self._event_listener is not None:
self._event_listener()
self._event_listener = None
_LOGGER.debug("%s: Removed event listener", self.entity_id)
await self._dispatcher.async_remove_entry(self._event, self._entry_id)
_LOGGER.info("%s: Removed event listener", self.entity_id)
5 changes: 4 additions & 1 deletion hacs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"domains": [
"sensor"
],
"zip_release": true,
"filename": "eventsensor.zip",
"homeassistant": "0.109.0",
"iot_class": "Local Push",
"render_readme": true
"render_readme": true,
"version": "2.4.0"
}
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
[tool.poetry]
name = "eventsensor"
version = "2.3.0"
version = "2.4.0"
description = "HomeAssistant custom sensor to track specific events"
authors = ["Eugenio Panadero <[email protected]>"]
license = "MIT"

[tool.poetry.dependencies]
python = "^3.7"
homeassistant = "^0.109.0"
pre-commit = "^2.2.0"
python = ">=3.7"
homeassistant = ">=0.109.0"

[tool.poetry.dev-dependencies]
pre-commit = ">=2.2.0"

[tool.black]
line_length = 88
Expand Down

0 comments on commit 6e600c1

Please sign in to comment.