diff --git a/README.md b/README.md index 46eb3ad..3c7d655 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,231 @@ # Becker cover support for Home Assistant -A native home assistant component to control becker RF shutters with a Becker Centronic USB Stick. - +A native Home Assistant component to control Becker RF shutters with a Becker Centronic USB stick. +It supports the Becker ***Centronic USB Stick*** with the Becker order number ***4035 200 041 0***. +It works for the Becker ***Centronic*** roller shutters, blinds and sun protection as well as for Roto roof windows with RF remotes. It is based on the work of [ole](https://github.com/ole1986) and [Nicolas Berthel](https://github.com/nicolasberthel). -The becker integration currently support to -- Open a Cover -- Close a Cover -- Stop a Cover -- Set Cover position -- Track Cover position by travel time or template -- Track commands from Becker Remote - -It tracks the position by using the travel time of your cover. - -It support value template if you want to use sensors to set the current state of your cover. - +The Becker integration currently supports the following cover operations: +- Open +- Close +- Stop +- Open tilt +- Close tilt +- Set cover position -## Installation +There are three ways to track position of the cover: +- Add the travel time to configuration +- Provide a value template e.g. to use sensors to set the current position +- Track the cover commands from Becker remotes -Add this repository to HACS custom repositories (preferred). +# Installation -Alternatively copy this repository into the custom_components folder of your HA configuration directory - -## Configuration - -Once installed you can add a new cover configuration in your HA configuration +1. Add [this](https://github.com/RainerStaude/hass-becker-component-plus-pybecker) repository to HACS custom + repositories (preferred). + Alternatively copy the files of this [this](https://github.com/RainerStaude/hass-becker-component-plus-pybecker) + repository into the custom_components folder of your HA configuration directory. +2. Plug the Becker USB stick into any free USB port. It is a good practice to add a short USB extension cable + and place the Becker USB stick away from other RF sources. +3. Add the Becker integration to your configuration. +4. Reboot Home Assistant +# Configuration +## Basic configuration ```yaml cover: - platform: becker - # Optional device path (useful when running from docker container) - # Default device: - # "/dev/serial/by-id/usb-BECKER-ANTRIEBE_GmbH_CDC_RS232_v125_Centronic-if00" - device: "/dev/beckercentronicusb" - # Optional database filename (database is stored in HA config folder) - filename: "centronic-stick.db" covers: + # Use unique names for each cover like kitchen, bedroom or living_room kitchen: friendly_name: "Kitchen Cover" + # Becker Centronic USB stick provides up to five units (1-5) with up to seven (1-7) channels + # Unit 1 - Channel 2 channel: "1" bedroom: friendly_name: "Bedroom Cover" - # Using Unit 1 - Channel 2 + # Unit 1 - Channel 2 channel: "2" - value_template: "{{ states('sensor.bedroom_sensor_value') | float > 22 }}" - livingroom: + living_room: + friendly_name: "Living room Cover" + # Use Unit 2 - Channel 1 + channel: "2:1" +``` +Note: The channel needs to be a string! + +## Platform configuration +If you use the Becker integration with the ***Home Assistant Operating System***, +the default device path for the USB Stick should work. The default path is: +`/dev/serial/by-id/usb-BECKER-ANTRIEBE_GmbH_CDC_RS232_v125_Centronic-if00` +If you run Home Assistant within a Virtual Machine or a Docker container, it +might be useful use a different device path. +Note: The serial port is opened using +the [pySerial serial_for_url handler](https://pyserial.readthedocs.io/en/latest/url_handlers.html). +Therefore connections e.g. over a TCP/IP socket are supported as well. + +The Becker integration uses a database file `centronic-stick.db` located in the +Home Assistant configuration folder to store an incremental number for each unit. +You can change the filename if needed. If the database file gets lost it will be +restored on startup automatically. In case any cover does not respond press the STOP +button several times. +```yaml +cover: + - platform: becker + device: "/dev/my-becker-centronic-usb" + filename: "my-centronic-stick.db" +``` + +## Position by Travel Time +There is no feedback from the covers available! In order to track the position of +the cover, it is recommended to add the travel time for each cover. Determine the +movement time in ***seconds*** for each cover from closed to open position. To +improve the precision, add the movement time from open to closed position as well. +This will also enable the ability to set the cover position from Home Assistant user +interface or through the service `cover.set_cover_position`. +```yaml +cover: + - platform: becker + covers: + living_room: friendly_name: "Living room Cover" - # Using Unit 2 - Channel 1 channel: "2:1" - # Optional Travel Time to track cover position by time - # one time is sufficient if up and down travel time is equal + # The travel time for direction up is sufficient if travel time for up and down are equal travelling_time_up: 30 - # Optional Travel Time for direction down - travelling_time_down: 27 - # Optional Remote ID from your Becker Remote, e.g. your master sender (multiple ID's separated by comma are possible) - # to find out the Remote ID of your Becker Remote enable debug log for becker + # Optional travel time for direction down + travelling_time_down: 26.5 +``` + +### Position by value template +In some cases it might be useful to add a value template to determine the position +of your cover. For example for a roof window with rain sensor. In case of rain, +the roof window will close, but you cannot determine this without an additional +sensor. +Every time the template generates a new result, the position of the cover is overwritten by the result of the template. +The following results are valid: +- any number between `0` and `100` where `0` is `closed` and `100` is `open` +- logic values where + - `'closed'`, `'false'`, `False` are `closed` + - `'open'`, `'true'`, `True` are `open` +- unknown values `'unknown'`, `'unavailable'`, `'none'`, `None` +The unknown values are useful to set the position only to confirm one specific +position, like closed in the example above. For any other values the position +will not changed. This allows to use the value template in conjunction with the position by travel time. +```yaml +cover: + - platform: becker + covers: + roof_window: + friendly_name: "Roof window" + channel: "3:1" + travelling_time_up: 15 + # Set position to closed (False) if sensor.roof_window is closed, otherwise keep value + value_template: "{{ 0 if is_state('sensor.roof_window', 'closed') else None }}" +``` + +### Position tracking for cover commands from Becker remotes +Usually there is at least one remote, the master remote, used to control the cover. +The remote communicates directly with the cover. It is possible to receive and track +all remote commands within home assistant. Therefore the position of the cover +is updated whenever a remote command is received. +In order to determine the remote ID, it es necessary to enable debug log messages +(see troubleshooting). The debug message will look as follows: +`... DEBUG ... \[custom_components.becker.pybecker.becker_helper]` Received packet: +unit_id: `12345`, channel: `2`, command: HALT, argument: 0, packet: ... +```yaml +cover: + - platform: becker + covers: + living_room: + friendly_name: "Living room Cover" + channel: "2:1" + travelling_time_up: 30 + travelling_time_down: 26.5 + # The remote ID consists of the unit_id and the channel separated by a colon + # Multiple ID's separated by comma are possible remote_id: "12345:2" ``` -Note: The channel and remote_id needs to be a string! +### Intermediate cover position +Becker covers supports two intermediate positions. One when opening the cover +and one when closing the cover. Please see the manual of your cover to see how +to program these intermediate positions in your cover. +Your cover will travel to the corresponding intermediate position if your double +tab the UP or DOWN button on your remote. +The default intermediate positions in the Becker integraten are `25` for UP +direction and `75` for DOWN direction. If the cover already passed the intermediate +position it will close instead. +This behaviour is imitated by the Becker integration in Home Assistant. To imitate +the cover movement properly in Home Assistant it is required to set the positions properly. +You can calculate the `intermediate_position_up` by dividing the measured runtime from +closed position to the intermediate position in direction UP by the `travelling_time_up`. +```yaml + - platform: becker + covers: + kitchen: + friendly_name: "Kitchen Cover" + channel: "1" + intermediate_position_up: 70 + intermediate_position_down: 40 +``` +If you have not programmed any intermediate positions in your cover you should +disable the intermediate cover position. +```yaml + - platform: becker + covers: + kitchen: + friendly_name: "Kitchen Cover" + channel: "1" + intermediate_position: off +``` -## Note +### Tilt intermediate +The Becker integration provide the ability to control the intermediate position from +Home Assistant user interface. Therefore the tilt functionality of Home Assistant is used +to issue the commands to drive to intermediate positions. +If you don't want to control the intermediate positions from Home Assistant, you can +disable the tilt functionality for each cover. +This will also disable the service `cover.close_cover_tilt` and `cover.open_cover_tilt`. +```yaml + - platform: becker + covers: + kitchen: + friendly_name: "Kitchen Cover" + channel: "1" + tilt_intermediate: off +``` +Note: You still need to set the intermediate cover position appropriately! -To use your cover in HA you need to pair it first with the USB stick. +### Tilt blind +The Becker integration provides support for blinds. The Becker blinds allow to control +their tilt position by short press of the UP or DOWN button on their master remote. +A long press of the UP or DOWN button fully open or closes the blinds. +To control the tilt position of your blind and for proper tracking of your blind position, +you need to enable `tilt_blind`. This changes the tilt functionality of Home Assistant +from intermediate position to tilt blind. The default tilt time is 0.3 seconds. +This time can be adapted to your needs. +```yaml + - platform: becker + covers: + Living_room_blind: + friendly_name: "Living room blind" + channel: "2:2" + tilt_blind: on + tilt_time_blind: 0.5 +``` -The pairing is always between your remote and the shutter. The shutter will react on the commands of all paired remotes. -Usually you already have programmed your original remote as the master remote. It is not recommended to program the USB stick as the master remote! -The USB stick is like an additional remote. Therefore the pairing procedure for the USB stick is the same as with additional remotes. Please refer to you manual for more details. +## Pairing the Becker USB Stick with your covers +To use your cover in HA you need to pair it first with the Becker USB stick. The +pairing is always between your remote and the shutter. The shutter will react on +the commands of all paired remotes. +Usually you already have programmed your original remote as the master remote. It +is not recommended to program the USB stick as the master remote! The USB stick is +like an additional remote. Therefore the pairing procedure for the USB stick is +the same as with additional remotes. Please refer to you manual for more details. -You have to put your shutter in pairing mode before. This is done by pressing the program button of your master remote until you hear a "clac" noise +You have to put your shutter in pairing mode before. This is done by pressing the +program button of your master remote until you hear a "clac" noise -To pair your shutter run the service becker.pair once (see HA Developer Tools -> Services). The shutter will confirm the successful pair with a single "clac" noise followed by a double "clac" noise. +To pair your shutter run the service becker.pair once (see HA Developer Tools -> Services). +The shutter will confirm the successful pair with a single "clac" noise followed by a double "clac" noise. Example data for service becker.pair: @@ -84,19 +238,28 @@ data: ``` ## Troubleshooting - -If you have any trouble enable debug log for becker. Add the following lines to your configuration.yaml: +If you have any trouble follow these steps: +- Restart Home Assistant after you have plugged in the USB stick +- Enable debug log for becker. +Add the following lines to your configuration.yaml to enable debug log: ```yaml logger: default: info logs: - # This must be the folder name of your /config/custom_components/hass-becker-component folder - custom_components.hass-becker-component: debug + # This must correspond to the folder name of your /config/custom_components/becker folder + custom_components.becker: debug ``` +This enable DEBUG messages to the home-assistant.log file in your config folder. +It is also helpful to find out the Remote ID of your Becker Remote. The message +will be something like below every time you press a key on your Remote: +`... DEBUG ... \[custom_components.becker.pybecker.becker_helper]` Received packet: +unit_id: `12345`, channel: `2`, command: HALT, argument: 0, packet: ... -This enable DEBUG messages to the home-assistant.log file in your config folder. - -Also helpful to find out the Remote ID of your Becker Remote. The message will be something like below every time you press a key on your Remote: - -... DEBUG ... \[custom_components.hass-becker-component...\] Received packet: **unit_id: 12345, channel: 2**, command: HALT, argument: 0, packet: ... +In case of any errors related to the Becker integration try to fix them. +If you require additional help have a look at the +[Home Assistant Community](https://community.home-assistant.io). There is already one thread about the +Becker integration: +[Integrating Becker Motors](https://community.home-assistant.io/t/integrating-becker-motors-in-to-hassio/151705) +Another way is to open a new issue on +[GitHub](https://github.com/RainerStaude/hass-becker-component-plus-pybecker/issues). diff --git a/const.py b/const.py index 63407f3..99de925 100644 --- a/const.py +++ b/const.py @@ -2,12 +2,18 @@ import re +from homeassistant.const import ( + STATE_CLOSED, + STATE_OPEN, +) + from .pybecker.becker import ( COMMAND_HALT, COMMAND_UP, COMMAND_UP5, COMMAND_DOWN, COMMAND_DOWN5, + COMMAND_RELEASE, ) DOMAIN = "becker" @@ -24,14 +30,22 @@ CONF_REMOTE_ID = "remote_id" CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down' CONF_TRAVELLING_TIME_UP = 'travelling_time_up' -CONF_INTERMEDIATE_DISABLE = 'intermediate_position_disable' +CONF_INTERMEDIATE_DISABLE = 'intermediate_position_disable' # deprecated +CONF_INTERMEDIATE_POSITION = 'intermediate_position' CONF_INTERMEDIATE_POSITION_UP = 'intermediate_position_up' CONF_INTERMEDIATE_POSITION_DOWN = 'intermediate_position_down' +CONF_TILT_INTERMEDIATE = 'tilt_intermediate' +CONF_TILT_BLIND = 'tilt_blind' +CONF_TILT_TIME_BLIND = 'tilt_time_blind' + +TILT_FUNCTIONALITY = 'tilt_functionality' CLOSED_POSITION = 0 VENTILATION_POSITION = 25 INTERMEDIATE_POSITION = 75 OPEN_POSITION = 100 +TILT_TIME = 0.3 +TILT_RECEIVE_TIMEOUT = 1.0 COMMANDS = { 'halt': f'{COMMAND_HALT:02x}'.encode(), @@ -39,6 +53,11 @@ 'up_intermediate': f'{COMMAND_UP5:02x}'.encode(), 'down': f'{COMMAND_DOWN:02x}'.encode(), 'down_intermediate': f'{COMMAND_DOWN5:02x}'.encode(), + 'release': f'{COMMAND_RELEASE:02x}'.encode(), } REMOTE_ID = re.compile(r'(?P[0-9A-F]{5,5}):(?P[0-9A-F]{1,1})') + +TEMPLATE_VALID_OPEN = [STATE_OPEN, 'true', True] +TEMPLATE_VALID_CLOSE = [STATE_CLOSED, 'false', False] +TEMPLATE_UNKNOWN_STATES = ['unknown', 'unavailable', 'none', None] diff --git a/cover.py b/cover.py index 6b9d64d..6718bfb 100644 --- a/cover.py +++ b/cover.py @@ -2,6 +2,7 @@ import logging +import time import voluptuous as vol from xknx.devices import TravelCalculator @@ -35,8 +36,6 @@ CONF_FILENAME, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - STATE_CLOSED, - STATE_OPEN, ) from .const import ( DOMAIN, @@ -53,8 +52,18 @@ CONF_INTERMEDIATE_POSITION_UP, CONF_INTERMEDIATE_POSITION_DOWN, CONF_INTERMEDIATE_DISABLE, + CONF_INTERMEDIATE_POSITION, COMMANDS, REMOTE_ID, + CONF_TILT_INTERMEDIATE, + CONF_TILT_BLIND, + CONF_TILT_TIME_BLIND, + TILT_TIME, + TILT_RECEIVE_TIMEOUT, + TILT_FUNCTIONALITY, + TEMPLATE_VALID_CLOSE, + TEMPLATE_VALID_OPEN, + TEMPLATE_UNKNOWN_STATES ) from .rf_device import PyBecker @@ -63,16 +72,6 @@ COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP -_VALID_STATES = [ - STATE_OPEN, - STATE_CLOSED, - "true", - "false", - True, - False -] - - COVER_SCHEMA = vol.Schema( { vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -83,7 +82,11 @@ vol.Optional(CONF_TRAVELLING_TIME_UP): cv.positive_float, vol.Optional(CONF_INTERMEDIATE_POSITION_UP, default=VENTILATION_POSITION): cv.positive_int, vol.Optional(CONF_INTERMEDIATE_POSITION_DOWN, default=INTERMEDIATE_POSITION): cv.positive_int, - vol.Optional(CONF_INTERMEDIATE_DISABLE, default=False): cv.boolean, + vol.Optional(CONF_INTERMEDIATE_DISABLE): cv.boolean, + vol.Optional(CONF_INTERMEDIATE_POSITION, default=True): cv.boolean, + vol.Optional(CONF_TILT_INTERMEDIATE): cv.boolean, + vol.Optional(CONF_TILT_BLIND, default=False): cv.boolean, + vol.Optional(CONF_TILT_TIME_BLIND, default=TILT_TIME): cv.positive_float, } ) @@ -111,9 +114,56 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= remote_id = device_config.get(CONF_REMOTE_ID) travel_time_down = device_config.get(CONF_TRAVELLING_TIME_DOWN) travel_time_up = device_config.get(CONF_TRAVELLING_TIME_UP) + # Warning if both template and travelling time are set + if (travel_time_down or travel_time_up) is not None and state_template is not None: + _LOGGER.warning('Both "%s" and "%s" are configured for cover %s. "%s" might influence with "%s"!', + CONF_VALUE_TEMPLATE, + CONF_TRAVELLING_TIME_UP.rpartition("_")[0], + friendly_name, + CONF_VALUE_TEMPLATE, + CONF_TRAVELLING_TIME_UP.rpartition("_")[0], + ) + # intermediate settings + intermediate_disable = device_config.get(CONF_INTERMEDIATE_DISABLE) + if intermediate_disable is not None: + _LOGGER.error( + "%s is no longer supported for cover %s. Please remove from your configuration.yaml and replace by %s: %s", + CONF_INTERMEDIATE_DISABLE, + friendly_name, + CONF_TILT_INTERMEDIATE, + not intermediate_disable, + ) + else: + intermediate_disable = False + intermediate_position = device_config.get(CONF_INTERMEDIATE_POSITION) and not intermediate_disable intermediate_pos_up = device_config.get(CONF_INTERMEDIATE_POSITION_UP) intermediate_pos_down = device_config.get(CONF_INTERMEDIATE_POSITION_DOWN) - intermediate_disable = device_config.get(CONF_INTERMEDIATE_DISABLE) + # tilt settings + tilt_intermediate = device_config.get(CONF_TILT_INTERMEDIATE) + tilt_blind = device_config.get(CONF_TILT_BLIND) + if tilt_intermediate is None: + tilt_intermediate = intermediate_position and not tilt_blind + if tilt_intermediate and not intermediate_position: + _LOGGER.error( + '%s is enabled for cover %s, but %s is deactivated. Will deactivate %s.', + CONF_TILT_INTERMEDIATE, + friendly_name, + CONF_INTERMEDIATE_POSITION, + CONF_TILT_INTERMEDIATE, + ) + tilt_intermediate = False + if tilt_intermediate and tilt_blind: + _LOGGER.error( + 'Both, %s and %s are enabled for cover %s. Will use %s and deactivate %s.', + CONF_TILT_INTERMEDIATE, + CONF_TILT_BLIND, + friendly_name, + CONF_TILT_BLIND, + CONF_TILT_INTERMEDIATE, + ) + tilt_intermediate = False + tilt_time_blind = device_config.get(CONF_TILT_TIME_BLIND) + if channel is None: _LOGGER.error("Must specify %s", CONF_CHANNEL) continue @@ -124,7 +174,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= BeckerEntity( PyBecker.becker, friendly_name, channel, state_template, remote_id, travel_time_down, travel_time_up, - intermediate_pos_up, intermediate_pos_down, intermediate_disable, + intermediate_pos_up, intermediate_pos_down, intermediate_position, + tilt_intermediate, tilt_blind, tilt_time_blind, ) ) @@ -137,9 +188,10 @@ class BeckerEntity(CoverEntity, RestoreEntity): def __init__( self, becker, name, channel, state_template, remote_id, travel_time_down, travel_time_up, - intermediate_pos_up, intermediate_pos_down, intermediate_disable, + intermediate_pos_up, intermediate_pos_down, intermediate_position, + tilt_intermediate, tilt_blind, tilt_time_blind, ): - """Init the Becker device.""" + """Init the Becker entity.""" self._becker = becker self._name = name self._attr = dict() @@ -149,21 +201,32 @@ def __init__( # Template self._template = state_template # Intermediate position settings + self._intermediate_position = intermediate_position self._intermediate_pos_up = intermediate_pos_up self._intermediate_pos_down = intermediate_pos_down - if not intermediate_disable: + if intermediate_position: + self._attr[CONF_INTERMEDIATE_POSITION] = str(intermediate_position) + self._attr[CONF_INTERMEDIATE_POSITION_UP] = str(intermediate_pos_up) + self._attr[CONF_INTERMEDIATE_POSITION_DOWN] = str(intermediate_pos_down) + # tilt settings + self._tilt_intermediate = tilt_intermediate + self._tilt_blind = tilt_blind + self._tilt_time_blind = tilt_time_blind + self._tilt_timeout = time.time() + if tilt_intermediate or tilt_blind: self._cover_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT + if tilt_blind: + self._attr[TILT_FUNCTIONALITY] = str(CONF_TILT_BLIND) + self._attr[CONF_TILT_TIME_BLIND] = str(tilt_time_blind) + if tilt_intermediate: + self._attr[TILT_FUNCTIONALITY] = str(CONF_TILT_INTERMEDIATE) # Callbacks self._callbacks = dict() - # Setup TravelCalculator + # Setup TravelCalculator + # todo enable set position and self_template if not ((travel_time_down or travel_time_up) is None or self._template is not None): self._cover_features |= SUPPORT_SET_POSITION - # Warning if both template and travelling time are set - if (travel_time_down or travel_time_up) is not None and self._template is not None: - _LOGGER.warning('Both "%s" and "%s" are configured for cover %s. "%s" will disable "%s"!', - CONF_VALUE_TEMPLATE, CONF_TRAVELLING_TIME_UP.rpartition("_")[0], self._name, - CONF_VALUE_TEMPLATE, CONF_TRAVELLING_TIME_UP.rpartition("_")[0], - ) + travel_time_down = travel_time_down or travel_time_up or 0 travel_time_up = travel_time_up or travel_time_down or 0 if self._cover_features & SUPPORT_SET_POSITION: @@ -283,8 +346,12 @@ async def async_open_cover(self, **kwargs): async def async_open_cover_tilt(self, **kwargs): """Open the cover tilt.""" # Feature only available if SUPPORT_OPEN_TILT is set - self._travel_up_intermediate() - await self._becker.move_up_intermediate(self._channel) + if self._tilt_blind: + await self.async_open_cover() + self._update_scheduled_stop_travel_callback(self._tilt_time_blind) + if self._tilt_intermediate: + self._travel_up_intermediate() + await self._becker.move_up_intermediate(self._channel) async def async_close_cover(self, **kwargs): """Set the cover to the closed position.""" @@ -294,8 +361,12 @@ async def async_close_cover(self, **kwargs): async def async_close_cover_tilt(self, **kwargs): """Close the cover tilt.""" # Feature only available if SUPPORT_CLOSE_TILT is set - self._travel_down_intermediate() - await self._becker.move_down_intermediate(self._channel) + if self._tilt_blind: + await self.async_close_cover() + self._update_scheduled_stop_travel_callback(self._tilt_time_blind) + if self._tilt_intermediate: + self._travel_down_intermediate() + await self._becker.move_down_intermediate(self._channel) async def async_stop_cover(self, **kwargs): """Set the cover to the stopped position.""" @@ -340,16 +411,14 @@ def _travel_stop(self): def _travel_up_intermediate(self): pos = self.current_cover_position - if ((pos > self._intermediate_pos_up) or - not (self._cover_features & SUPPORT_OPEN_TILT)): + if pos >= self._intermediate_pos_up or not self._intermediate_position: self._travel_to_position(OPEN_POSITION) else: self._travel_to_position(self._intermediate_pos_up) def _travel_down_intermediate(self): pos = self.current_cover_position - if ((pos < self._intermediate_pos_down) or - not (self._cover_features & SUPPORT_CLOSE_TILT)): + if pos <= self._intermediate_pos_down or not self._intermediate_position: self._travel_to_position(CLOSED_POSITION) else: self._travel_to_position(self._intermediate_pos_down) @@ -405,24 +474,27 @@ async def _async_message_received(self, packet): if ids in self._remode_ids: _LOGGER.debug("%s received a packet from dispatcher", self._name) command = packet.group('command') + b'0' - cmd_arg = command + packet.group('argument') + cmd_arg = packet.group('command') + packet.group('argument') if command == COMMANDS['halt']: self._travel_stop() - elif ((cmd_arg == COMMANDS['up_intermediate']) and - (self._cover_features & SUPPORT_OPEN_TILT)): - self._travel_up_intermediate(self) + elif command == COMMANDS['release'] and self._tilt_timeout > time.time(): + if self._tilt_blind and (self.is_opening or self.is_closing): + self._travel_stop() + elif cmd_arg == COMMANDS['up_intermediate'] and self._intermediate_position: + self._travel_up_intermediate() elif command == COMMANDS['up']: self._travel_to_position(OPEN_POSITION) - elif ((cmd_arg == COMMANDS['down_intermediate']) and - (self._cover_features & SUPPORT_CLOSE_TILT)): - self._travel_down_intermediate(self) + self._tilt_timeout = time.time() + TILT_RECEIVE_TIMEOUT + elif cmd_arg == COMMANDS['down_intermediate'] and self._intermediate_position: + self._travel_down_intermediate() elif command == COMMANDS['down']: self._travel_to_position(CLOSED_POSITION) + self._tilt_timeout = time.time() + TILT_RECEIVE_TIMEOUT @callback async def _async_stop_travel(self, _): """Stop the cover callack.""" - self._update_scheduled_stop_travel_callback() + self._travel_stop() await self._becker.stop(self._channel) @callback @@ -442,14 +514,17 @@ async def _async_on_template_update(self, _, updates): '%s: Update template with result: %s with type %s', self._name, result, type(result) ) - if result in _VALID_STATES: - if result in (True, "true", STATE_OPEN): - pos = OPEN_POSITION - else: - pos = CLOSED_POSITION + if isinstance(result, str): + result = result.lower() + if result in TEMPLATE_VALID_OPEN: + pos = OPEN_POSITION + elif TEMPLATE_VALID_CLOSE: + pos = CLOSED_POSITION elif isinstance(result, int) or isinstance(result, float): # Clip position to a range of 0 - 100 pos = round(max(min(result, OPEN_POSITION), CLOSED_POSITION)) + elif result in TEMPLATE_UNKNOWN_STATES: + pos = self.current_cover_position else: _LOGGER.error('%s: invalid template result: %s', self._name, result diff --git a/pybecker/becker.py b/pybecker/becker.py index 9149688..7aa4a1d 100644 --- a/pybecker/becker.py +++ b/pybecker/becker.py @@ -10,6 +10,7 @@ from .becker_helper import BeckerCommunicator from .database import Database +COMMAND_RELEASE = 0x00 # button release COMMAND_UP = 0x20 COMMAND_UP2 = 0x21 # move up COMMAND_UP3 = 0x22 # move up @@ -90,6 +91,8 @@ async def run_codes(self, channel, unit, cmd, test): codes.append(generate_code(channel, unit, COMMAND_UP5)) elif cmd == "HALT": codes.append(generate_code(channel, unit, COMMAND_HALT)) + elif cmd == "RELEASE": + codes.append(generate_code(channel, unit, COMMAND_RELEASE)) elif cmd == "DOWN": codes.append(generate_code(channel, unit, COMMAND_DOWN)) elif cmd == "DOWN2": diff --git a/pybecker/becker_helper.py b/pybecker/becker_helper.py index 90e4be4..6ba32dc 100644 --- a/pybecker/becker_helper.py +++ b/pybecker/becker_helper.py @@ -36,6 +36,7 @@ ) COMMANDS = {b'0': 'RELEASE', b'1': 'HALT', b'2': 'UP', b'4': 'DOWN', b'8': 'TRAIN'} +COMMUNICATION_TIMEOUT = 0.1 _LOGGER = logging.getLogger(__name__) @@ -215,6 +216,8 @@ def __init__( # Setup interface self._connection = BeckerConnection(device=device) self._read_buffer = bytes() + # timeout will be used within thread only + self._timeout = time.time() def run(self) -> None: '''Run BeckerCommunicator thread.''' @@ -222,20 +225,26 @@ def run(self) -> None: callback_valid = False if self._callback is None else True # pylint: disable=simplifiable-if-expression packet = None while True: - # Get packet from write queue - try: - packet = self._write_queue.get(block=False) - except queue.Empty: - pass - else: - self._connection.write(packet) - self._log(packet, "Sent packet: ") # Read bytes from serial port if callback_valid: - self._read_buffer += self._connection.read() + data = self._connection.read() + if len(data) > 0: + self._timeout = time.time() + COMMUNICATION_TIMEOUT + self._read_buffer += data self._parse() + # Get packet from write queue if timeout expired + if self._timeout < time.time(): + try: + packet = self._write_queue.get(block=False) + except queue.Empty: + pass + else: + self._connection.write(packet) + self._timeout = time.time() + COMMUNICATION_TIMEOUT + self._log(packet, "Sent packet: ") + # Sleep for thread switch and wait time between packets - time.sleep(0.1) + time.sleep(0.01) # Ensure all packets in queue are send before thread is stopped if self._stop_flag.is_set() and self._write_queue.empty(): break