Skip to content

Commit

Permalink
First Commit of becker custom component
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas committed Apr 6, 2020
1 parent c3ea44a commit 130dcb1
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

.idea/
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,54 @@
# -hass-integration-becker
Becker Cover custom component for Home Assistant
# Becker cover support for Home Assistant

A native home assistant component to control becker RF shutters with a Becker Centronic USB Stick.
It uses [pybecker](https://pypi.org/project/pybecker/).
The becker integration currently support to
- Open a Cover
- Close a Cover
- Stop a Cover

It as well support value template if you wan to use sensors to set the current state of your cover


## Installation

Copy the different sources in custom_components folder of your hass configuration directory

## Configuration

Once installed you can add a new cover configuration in your HA configuration

```yaml
cover:
- platform: becker
covers:
kitchen:
friendly_name: "Kitchen Cover"
channel: "1"
bedroom:
friendly_name: "Bedroom Cover"
channel: "2"
value_template: "{{ states('sensor.bedroom_sensor_value') | float > 22 }}"
```
Note: The channel needs to be a string!
## Note
The component does not support pairing for the moment. It means that your different shuters must be already paired with your USB stick
You can achieve this prior to the installation in home assistant by using [pybecker](https://pypi.org/project/pybecker/) with the following command
Of course you have to put your shutter in pairing mode before. This is generally done by holding the programming button of your sender until you hear a "clac" noise
```
pybecker -a PAIR -c <CHANNEL_ID>
```

## TODO

* Find a solution to pair new shutters from HA (if you have ideas you are welcome)

## Support

[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Q7A292QK8Z7BW&source=url)

This integration was developed for my home integration. Even if I tried to make it as generic as possible, it could be that you have to adapt it a bit for your own integration.
58 changes: 58 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""The becker component."""
from itertools import chain
import logging

from homeassistant.const import MATCH_ALL

_LOGGER = logging.getLogger(__name__)


def initialise_templates(hass, templates, attribute_templates=None):
"""Initialise templates and attribute templates."""
if attribute_templates is None:
attribute_templates = dict()
for template in chain(templates.values(), attribute_templates.values()):
if template is None:
continue
template.hass = hass


def extract_entities(
device_name, device_type, manual_entity_ids, templates, attribute_templates=None
):
"""Extract entity ids from templates and attribute templates."""
if attribute_templates is None:
attribute_templates = dict()
entity_ids = set()
if manual_entity_ids is None:
invalid_templates = []
for template_name, template in chain(
templates.items(), attribute_templates.items()
):
if template is None:
continue

template_entity_ids = template.extract_entities()

if template_entity_ids != MATCH_ALL:
entity_ids |= set(template_entity_ids)
else:
invalid_templates.append(template_name.replace("_template", ""))

if invalid_templates:
entity_ids = MATCH_ALL
_LOGGER.warning(
"Template %s '%s' has no entity ids configured to track nor"
" were we able to extract the entities to track from the %s "
"template(s). This entity will only be able to be updated "
"manually.",
device_type,
device_name,
", ".join(invalid_templates),
)
else:
entity_ids = list(entity_ids)
else:
entity_ids = manual_entity_ids

return entity_ids
177 changes: 177 additions & 0 deletions cover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Support for Becker RF covers."""

import logging

import voluptuous as vol

from homeassistant.components.cover import (
ATTR_POSITION,
CoverDevice,
PLATFORM_SCHEMA,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_STOP,
SUPPORT_OPEN_TILT,
SUPPORT_CLOSE_TILT
)
from homeassistant.const import (
CONF_FRIENDLY_NAME,
CONF_VALUE_TEMPLATE,
EVENT_HOMEASSISTANT_START,
STATE_CLOSED,
STATE_OPEN,
)

import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.exceptions import TemplateError
from . import extract_entities, initialise_templates

from pybecker.becker import Becker

_LOGGER = logging.getLogger(__name__)

COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT
DEVICE_CLASS = "shutter"

CONF_COVERS = "covers"
CONF_CHANNEL = "channel"
CONF_DEVICE = "device"

_VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"]

CLOSED_POSITION = 0
OPEN_POSITION = 100

COVER_SCHEMA = vol.Schema(
{
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Required(CONF_CHANNEL): cv.string,
vol.Optional(CONF_DEVICE): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)}
)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the becker platform."""
covers = []

becker = Becker()

for device, device_config in config[CONF_COVERS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
channel = device_config.get(CONF_CHANNEL)
state_template = device_config.get(CONF_VALUE_TEMPLATE)
if channel is None:
_LOGGER.error(
"Must specify %s", CONF_CHANNEL
)
continue
templates = {
CONF_VALUE_TEMPLATE: state_template,
}
initialise_templates(hass, templates)
entity_ids = extract_entities(device, "cover", None, templates)
covers.append(BeckerDevice(becker, friendly_name, int(channel), state_template, entity_ids))

async_add_entities(covers)


class BeckerDevice(CoverDevice, RestoreEntity):
"""
Representation of a Becker cover device.
"""

def __init__(self, becker, name, channel, state_template, entity_ids, position=0):
"""Init the Becker device."""
self._becker = becker
self._name = name
self._channel = str(channel)
self._template = state_template
self._entities = entity_ids
self._position = position

async def async_added_to_hass(self):
"""Register callbacks."""
await super().async_added_to_hass()

old_state = await self.async_get_last_state()
if old_state is not None:
self._state = old_state.state == STATE_OPEN

@property
def name(self):
"""Return the name of the device as reported by tellcore."""
return self._name

@property
def current_cover_position(self):
"""
Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position

@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS

@property
def supported_features(self):
"""Flag supported features."""
return COVER_FEATURES

@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._position == CLOSED_POSITION

async def async_open_cover(self, **kwargs):
"""Set the cover to the open position."""
if self._template is None:
self._position = OPEN_POSITION
await self._becker.move_up(self._channel)

async def async_open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
await self._becker.move_up_intermediate(self._channel)

async def async_close_cover(self, **kwargs):
"""Set the cover to the closed position."""
if self._template is None:
self._position = CLOSED_POSITION
await self._becker.move_down(self._channel)

async def async_close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
await self._becker.move_down_intermediate(self._channel)

async def async_stop_cover(self, **kwargs):
"""Set the cover to the closed position."""
if self._template is None:
self._position = 50
await self._becker.stop(self._channel)

async def async_update(self):
await super().async_update()
if self._template is not None:
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
if state in ("true", STATE_OPEN):
self._position = 100
else:
self._position = 0
else:
_LOGGER.error("Received invalid cover is_on state: %s. Expected: %s", state, ", ".join(_VALID_STATES))
self._position = None
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
8 changes: 8 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"domain": "becker",
"name": "Becker",
"documentation": "",
"requirements": ["pybecker==1.0.0"],
"dependencies": [],
"codeowners": ["@nicolasberthel"]
}

0 comments on commit 130dcb1

Please sign in to comment.