Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for functional parameter (#33) #34

Merged
merged 21 commits into from
May 3, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions pyvlx/command_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@
class CommandSend(ApiEvent):
"""Class for sending command to API."""

def __init__(self, pyvlx, node_id, parameter, wait_for_completion=True, timeout_in_seconds=60):
def __init__(
self,
pyvlx,
node_id,
parameter,
active_parameter=0,
wait_for_completion=True,
timeout_in_seconds=60,
**functional_parameter):
"""Initialize SceneList class."""
super().__init__(pyvlx=pyvlx, timeout_in_seconds=timeout_in_seconds)
self.success = False
self.node_id = node_id
self.parameter = parameter
self.active_parameter = active_parameter
self.functional_parameter = functional_parameter
self.wait_for_completion = wait_for_completion
self.session_id = None

Expand All @@ -39,4 +49,9 @@ async def handle_frame(self, frame):
def request_frame(self):
"""Construct initiating frame."""
self.session_id = get_new_session_id()
return FrameCommandSendRequest(node_ids=[self.node_id], parameter=self.parameter, session_id=self.session_id)
return FrameCommandSendRequest(
node_ids=[self.node_id],
parameter=self.parameter,
active_parameter=self.active_parameter,
session_id=self.session_id,
**self.functional_parameter)
50 changes: 39 additions & 11 deletions pyvlx/frames/frame_command_send.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pyvlx.const import Command, Originator, Priority
from pyvlx.exception import PyVLXException
from pyvlx.parameter import Parameter
from pyvlx.parameter import Parameter, Position

from .frame import FrameBase

Expand All @@ -13,29 +13,55 @@ class FrameCommandSendRequest(FrameBase):

PAYLOAD_LEN = 66

def __init__(self, node_ids=None, parameter=Parameter(), session_id=None, originator=Originator.USER):
def __init__(self,
node_ids=None,
parameter=Parameter(),
active_parameter=0,
session_id=None,
originator=Originator.USER,
**functional_parameter):
"""Init Frame."""
super().__init__(Command.GW_COMMAND_SEND_REQ)
self.node_ids = node_ids
self.parameter = parameter
self.active_parameter = active_parameter
self.fpi1 = 0
self.fpi2 = 0
self.functional_parameter = {}
self.session_id = session_id
self.originator = originator
self.priority = Priority.USER_LEVEL_2
"""Set the functional parameter indicator bytes in order to show which functional parameters are included in the frame.
Functional parameter dictionary will be checked for keys 'fp1' to 'fp16'
to set the appropriate indicator and the corresponding self.functional_parameter."""
for i in range(1, 17):
key = 'fp%s' % (i)
if key in functional_parameter:
self.functional_parameter[key] = functional_parameter[key]
if i < 9:
self.fpi1 += 2**(8-i)
if i >= 9:
self.fpi2 += 2**(16-i)
else:
self.functional_parameter[key] = bytes(2)

def get_payload(self):
"""Return Payload."""
# Session id
ret = bytes([self.session_id >> 8 & 255, self.session_id & 255])
ret += bytes([self.originator.value])
ret += bytes([self.priority.value])
ret += bytes([0]) # ParameterActive pointing to main parameter (MP)
ret += bytes([self.active_parameter]) # ParameterActive pointing to main parameter (MP)
# FPI 1+2
ret += bytes([0])
ret += bytes([0])

# Main parameter + functional parameter
ret += bytes([self.fpi1])
ret += bytes([self.fpi2])
# Main parameter + functional parameter fp1 to fp3
ret += bytes(self.parameter)
ret += bytes(32)
ret += bytes(self.functional_parameter['fp1'])
ret += bytes(self.functional_parameter['fp2'])
ret += bytes(self.functional_parameter['fp3'])
# Functional parameter fp4 to fp16
ret += bytes(26)

# Nodes array: Number of nodes + node array + padding
ret += bytes([len(self.node_ids)]) # index array count
Expand Down Expand Up @@ -66,9 +92,11 @@ def from_payload(self, payload):

def __str__(self):
"""Return human readable string."""
return '<FrameCommandSendRequest node_ids={} parameter="{}" session_id={} originator={}/>'.format(
self.node_ids, self.parameter, self.session_id,
self.originator)
functional_parameter = ""
for key, value in self.functional_parameter.items():
functional_parameter += "%s: %s, " % (str(key), Position(Parameter(bytes(value))))
return '<FrameCommandSendRequest node_ids={} parameter="{}" functional_parameter="{}" session_id={} originator={}/>'.format(
self.node_ids, self.parameter, functional_parameter, self.session_id, self.originator)


class CommandSendConfirmationStatus(Enum):
Expand Down
9 changes: 6 additions & 3 deletions pyvlx/node_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from .lightening_device import Light
from .log import PYVLXLOG
from .on_off_switch import OnOffSwitch
from .opening_device import (
Awning, Blade, Blind, GarageDoor, Gate, RollerShutter, Window)
from .opening_device import Awning, Blade, Blind, GarageDoor, Gate, RollerShutter, Window


def convert_frame_to_node(pyvlx, frame):
Expand All @@ -22,7 +21,11 @@ def convert_frame_to_node(pyvlx, frame):
position_parameter=frame.current_position)
if frame.node_type == NodeTypeWithSubtype.INTERIOR_VENETIAN_BLIND or \
frame.node_type == NodeTypeWithSubtype.VERTICAL_INTERIOR_BLINDS or \
frame.node_type == NodeTypeWithSubtype.EXTERIOR_VENETIAN_BLIND or \
frame.node_type == NodeTypeWithSubtype.INTERIOR_VENETIAN_BLIND:
return RollerShutter(pyvlx=pyvlx, node_id=frame.node_id, name=frame.name, serial_number=frame.serial_number)
# Blinds have position and orientation (inherit frame.current_position_fp3) attribute
if frame.node_type == NodeTypeWithSubtype.EXTERIOR_VENETIAN_BLIND or \
frame.node_type == NodeTypeWithSubtype.ADJUSTABLE_SLUTS_ROLLING_SHUTTER or \
frame.node_type == NodeTypeWithSubtype.LOUVER_BLIND:
return Blind(pyvlx=pyvlx, node_id=frame.node_id, name=frame.name, serial_number=frame.serial_number,
position_parameter=frame.current_position)
Expand Down
38 changes: 23 additions & 15 deletions pyvlx/node_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from .frames import (
FrameGetAllNodesInformationNotification,
FrameNodeStatePositionChangedNotification)
from .opening_device import OpeningDevice
from .opening_device import OpeningDevice, Blind
from .lightening_device import LighteningDevice
from .parameter import Intensity, Position
from .parameter import Intensity, Position, Parameter
from .pyvlx import PYVLXLOG


class NodeUpdater():
Expand All @@ -16,23 +17,30 @@ def __init__(self, pyvlx):

async def process_frame(self, frame):
"""Update nodes via frame, usually received by house monitor."""
if isinstance(frame, FrameNodeStatePositionChangedNotification):
if isinstance(frame, (FrameGetAllNodesInformationNotification, FrameNodeStatePositionChangedNotification)):
PYVLXLOG.debug("NodeUpdater process frame: %d", frame)
if frame.node_id not in self.pyvlx.nodes:
return
node = self.pyvlx.nodes[frame.node_id]
if isinstance(node, OpeningDevice):
node.position = Position(frame.current_position)
position = Position(frame.current_position)
orientation = Position(frame.current_position_fp3)
# KLF transmits for functional parameters basically always 'No feed-back value known’ (0xF7FF).
# In home assistant this cause unreasonable values like -23%. Therefore a check is implemented
# whether the frame parameter is inside the maximum range.
if isinstance(node, Blind):
if position.position <= Parameter.MAX:
node.position = position
PYVLXLOG.debug("%d position changed to: %d", node.name, position)
if orientation.position <= Parameter.MAX:
node.orientation = orientation
PYVLXLOG.debug("%d orientation changed to: %d", node.name, orientation)
await node.after_update()
elif isinstance(node, LighteningDevice):
node.intensity = Intensity(frame.current_position)
await node.after_update()
elif isinstance(frame, FrameGetAllNodesInformationNotification):
if frame.node_id not in self.pyvlx.nodes:
return
node = self.pyvlx.nodes[frame.node_id]
if isinstance(node, OpeningDevice):
node.position = Position(frame.current_position)
elif isinstance(node, OpeningDevice):
if position.position <= Parameter.MAX:
node.position = position
await node.after_update()
elif isinstance(node, LighteningDevice):
node.intensity = Intensity(frame.current_position)
intensity = Intensity(frame.current_position)
if intensity.intensity <= Parameter.MAX:
node.intensity = intensity
await node.after_update()
115 changes: 114 additions & 1 deletion pyvlx/opening_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .command_send import CommandSend
from .exception import PyVLXException
from .node import Node
from .parameter import CurrentPosition, Position, Parameter
from .parameter import CurrentPosition, Parameter, Position, TargetPosition


class OpeningDevice(Node):
Expand Down Expand Up @@ -127,6 +127,119 @@ def __str__(self):
class Blind(OpeningDevice):
"""Blind objects."""

def __init__(self, pyvlx, node_id, name, serial_number, position_parameter=Parameter()):
"""Initialize Blind class.

Parameters:
* pyvlx: PyVLX object
* node_id: internal id for addressing nodes.
Provided by KLF 200 device
* name: node name

"""
super().__init__(pyvlx=pyvlx,
node_id=node_id,
name=name,
serial_number=serial_number,
position_parameter=position_parameter)
self.orientation = Position(position_percent=0)
self.target_orientation = TargetPosition()
self.target_position = TargetPosition()

async def set_position(self, position, wait_for_completion=True):
"""Set window to desired position.

Parameters:
* position: Position object containing the current position.
* target_position: Position object holding the target position
which allows to ajust the position while the blind is in movement
without stopping the blind (if orientation position has been changed.)
* wait_for_completion: If set, function will return
after device has reached target position.

"""
self.target_position = position
self.position = position

command_send = CommandSend(
pyvlx=self.pyvlx,
wait_for_completion=wait_for_completion,
node_id=self.node_id,
parameter=position,
fp3=self.target_orientation
)
await command_send.do_api_call()
if not command_send.success:
raise PyVLXException("Unable to send command")
await self.after_update()

async def open(self, wait_for_completion=True):
"""Open Blind position."""
await self.set_position(
position=Position(position_percent=0),
wait_for_completion=wait_for_completion)

async def close(self, wait_for_completion=True):
"""Close Blind position."""
await self.set_position(
position=Position(position_percent=100),
wait_for_completion=wait_for_completion)

async def stop(self, wait_for_completion=True):
"""Stop Blind position."""
await self.set_position(
position=CurrentPosition(),
wait_for_completion=wait_for_completion)

async def set_orientation(self, orientation, wait_for_completion=True):
"""Set Blind shades to desired orientation.

Parameters:
* orientation: Position object containing the target orientation.
+ target_orientation: Position object holding the target orientation
which allows to ajust the orientation while the blind is in movement
without stopping the blind (if the position has been changed.)
* wait_for_completion: If set, function will return
after device has reached target position.

"""
self.target_orientation = orientation
self.orientation = orientation
print("Orientation in device: %s " % (orientation))
command_send = CommandSend(
pyvlx=self.pyvlx,
wait_for_completion=wait_for_completion,
node_id=self.node_id,
parameter=self.target_position,
fp3=orientation
)
await command_send.do_api_call()
if not command_send.success:
raise PyVLXException("Unable to send command")
await self.after_update()
# KLF200 always send UNKNOWN position for functional parameter, so orientation is set directly and not via GW_NODE_STATE_POSITION_CHANGED_NTF

async def open_orientation(self, wait_for_completion=True):
"""Open Blind slats orientation.

Blind slats with ±90° orientation are open at 50%
"""
await self.set_orientation(
orientation=Position(position_percent=50),
wait_for_completion=wait_for_completion)

async def close_orientation(self, wait_for_completion=True):
"""Close Blind slats."""
await self.set_orientation(
orientation=Position(position_percent=100),
wait_for_completion=wait_for_completion)

async def stop_orientation(self, wait_for_completion=True):
"""Stop Blind slats."""
await self.set_orientation(
orientation=CurrentPosition(),
wait_for_completion=wait_for_completion)


class Awning(OpeningDevice):
"""Awning objects."""
Expand Down
Loading