-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First Commit of becker custom component
- Loading branch information
Nicolas
committed
Apr 6, 2020
1 parent
c3ea44a
commit 130dcb1
Showing
5 changed files
with
299 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,5 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
|
||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
[](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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |