Skip to content

Commit

Permalink
Merge pull request #28 from nobbi1991/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
nobbi1991 authored Nov 21, 2023
2 parents 7058a76 + 70e3212 commit 7a449d6
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 56 deletions.
11 changes: 11 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Version 5.3.0 - 21.11.2023

## Features

- added ``habapp_rules.common.logic.Sum`` for calculation the sum of number items

## Bugfix

- only use items (instead item names) for all habapp_rules implementations which are using ``habapp_rules.core.helper.send_if_different``
- cancel timer / timeouts of replaced rules

# Version 5.2.1 - 17.10.2023

## Bugfix
Expand Down
2 changes: 1 addition & 1 deletion habapp_rules/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Set version for the package."""
__version__ = "5.2.1"
__version__ = "5.3.0"
6 changes: 4 additions & 2 deletions habapp_rules/actors/state_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ def __init__(self, item_name: str, control_names: list[str] | None = None, group
self._group_last_event = 0

self._item.listen_event(self._cb_item, HABApp.openhab.events.ItemStateChangedEventFilter())
HABApp.util.EventListenerGroup().add_listener(self.__control_items, self._cb_control_item, HABApp.openhab.events.ItemCommandEventFilter()).listen()
HABApp.util.EventListenerGroup().add_listener(self.__group_items, self._cb_group_item, HABApp.openhab.events.ItemStateUpdatedEventFilter()).listen()
for control_item in self.__control_items:
control_item.listen_event(self._cb_control_item, HABApp.openhab.events.ItemCommandEventFilter())
for group_item in self.__group_items:
group_item.listen_event(self._cb_group_item, HABApp.openhab.events.ItemStateUpdatedEventFilter())

@property
def value(self) -> float | bool:
Expand Down
33 changes: 30 additions & 3 deletions habapp_rules/common/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _set_output_state(self, output_state: str) -> None:
if isinstance(self._output_item, HABApp.openhab.items.ContactItem):
self._output_item.oh_post_update(output_state)
else:
habapp_rules.core.helper.send_if_different(self._output_item.name, output_state)
habapp_rules.core.helper.send_if_different(self._output_item, output_state)


class And(_BinaryLogicBase):
Expand Down Expand Up @@ -153,7 +153,7 @@ def _set_output_state(self, output_state: str) -> None:
:param output_state: state which will be set
"""
habapp_rules.core.helper.send_if_different(self._output_item.name, output_state)
habapp_rules.core.helper.send_if_different(self._output_item, output_state)


class Min(_NumericLogicBase):
Expand All @@ -172,7 +172,6 @@ def _apply_numeric_logic(self, input_values: list[float]) -> float:
return HABApp.util.functions.min(input_values)



class Max(_NumericLogicBase):
"""Logical Max function with filter for old / not updated items.
Expand All @@ -187,3 +186,31 @@ def _apply_numeric_logic(self, input_values: list[float]) -> float:
:return: max value of the given values
"""
return HABApp.util.functions.max(input_values)


class Sum(_NumericLogicBase):
"""Logical Sum function with filter for old / not updated items.
Example:
habapp_rules.common.logic.Sum(["Item_1", "Item_2"], "Item_result", 600)
"""

def __init__(self, input_names: list[str], output_name: str, ignore_old_values_time: int | None = None) -> None:
"""Init a logical function.
:param input_names: list of input items (must be either Dimmer or Number and all have to match to output_item)
:param output_name: name of output item
:param ignore_old_values_time: ignores values which are older than the given time in seconds. If None, all values will be taken
:raises TypeError: if unsupported item-type is given for output_name
"""
_NumericLogicBase.__init__(self, input_names, output_name, ignore_old_values_time)
if isinstance(self._output_item, HABApp.openhab.items.DimmerItem):
raise TypeError(f"Dimmer items can not be used for Sum function! Given input_names: {input_names} | output_name: {output_name}")

def _apply_numeric_logic(self, input_values: list[float]) -> float:
"""Apply numeric logic
:param input_values: input values
:return: min value of the given values
"""
return sum(input_values)
10 changes: 10 additions & 0 deletions habapp_rules/core/state_machine_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import inspect
import os
import pathlib
import threading

import HABApp
import HABApp.openhab.connection.handler.func_sync
Expand Down Expand Up @@ -34,6 +35,7 @@ def __init__(self, state_item_name: str | None = None, state_item_label: str | N
:param state_item_name: name of the item to hold the state
:param state_item_label: OpenHAB label of the state_item; This will be used if the state_item will be created by HABApp
"""
self.state_machine: transitions.Machine | None = None
HABApp.Rule.__init__(self)

# get prefix for items
Expand Down Expand Up @@ -83,3 +85,11 @@ def _set_state(self, state_name: str) -> None:
def _update_openhab_state(self) -> None:
"""Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine."""
self._item_state.oh_send_command(self.state)

def on_rule_removed(self) -> None:
"""Override this to implement logic that will be called when the rule has been unloaded."""
# stop timeout timer of current state
if self.state_machine:
for itm in self.state_machine.states[self.state].runner.values():
if isinstance(itm, threading.Timer) and itm.is_alive():
itm.cancel()
50 changes: 30 additions & 20 deletions habapp_rules/system/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import habapp_rules.core.helper
import habapp_rules.core.logger
import habapp_rules.core.state_machine_rule
from habapp_rules.system import PresenceState

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,13 +89,15 @@ def __init__(self, name_presence: str, leaving_name: str, outside_door_names: li
# add callbacks
self.__leaving_item.listen_event(self._cb_leaving, HABApp.openhab.events.ItemStateChangedEventFilter())
self.__presence_item.listen_event(self._cb_presence, HABApp.openhab.events.ItemStateChangedEventFilter())
HABApp.util.EventListenerGroup().add_listener(self.__outside_door_items, self._cb_outside_door, HABApp.core.events.ValueChangeEventFilter()).listen()
HABApp.util.EventListenerGroup().add_listener(self.__phone_items, self._cb_phone, HABApp.core.events.ValueChangeEventFilter()).listen()
for door_item in self.__outside_door_items:
door_item.listen_event(self._cb_outside_door, HABApp.core.events.ValueChangeEventFilter())
for phone_item in self.__phone_items:
phone_item.listen_event(self._cb_phone, HABApp.core.events.ValueChangeEventFilter())

self.__phone_absence_timer: threading.Timer | None = None
self._instance_logger.debug(super().get_initial_log_message())

def _get_initial_state(self, default_value: str = "presence") -> str:
def _get_initial_state(self, default_value: str = PresenceState.PRESENCE.value) -> str:
"""Get initial state of state machine.
:param default_value: default / initial state
Expand All @@ -103,20 +106,20 @@ def _get_initial_state(self, default_value: str = "presence") -> str:
phone_items = [phone for phone in self.__phone_items if phone.value is not None]
if phone_items:
if any((item.value == "ON" for item in phone_items)):
return "presence"
return PresenceState.PRESENCE.value

if self.__presence_item.value == "ON":
return "leaving"
return "long_absence" if self._item_state.value == "long_absence" else "absence"
return PresenceState.LEAVING.value
return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value

if self.__leaving_item.value == "ON":
return "leaving"
return PresenceState.LEAVING.value

if self.__presence_item.value == "ON":
return "presence"
return PresenceState.PRESENCE.value

if self.__presence_item.value == "OFF":
return "long_absence" if self._item_state.value == "long_absence" else "absence"
return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value

return default_value

Expand All @@ -126,17 +129,16 @@ def _update_openhab_state(self) -> None:
self._instance_logger.info(f"Presence state changed to {self.state}")

# update presence item
target_value = "ON" if self.state in {"presence", "leaving"} else "OFF"
habapp_rules.core.helper.send_if_different(self.__presence_item.name, target_value)

habapp_rules.core.helper.send_if_different(self.__leaving_item.name, "ON" if self.state == "leaving" else "OFF")
target_value = "ON" if self.state in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value} else "OFF"
habapp_rules.core.helper.send_if_different(self.__presence_item, target_value)
habapp_rules.core.helper.send_if_different(self.__leaving_item, "ON" if self.state == PresenceState.LEAVING.value else "OFF")

def _cb_outside_door(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
"""Callback, which is called if any outside door changed state.
:param event: state change event of door item
"""
if event.value == "OPEN" and self.state != "presence":
if event.value == "OPEN" and self.state not in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value}:
self._instance_logger.debug(f"Presence detected by door ({event.name})")
self.presence_detected()

Expand All @@ -145,10 +147,10 @@ def _cb_leaving(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> Non
:param event: Item state change event of leaving item
"""
if event.value == "ON" and self.state in {"presence", "absence", "long_absence"}:
if event.value == "ON" and self.state in {PresenceState.PRESENCE.value, PresenceState.ABSENCE.value, PresenceState.LONG_ABSENCE.value}:
self._instance_logger.debug("Start leaving through leaving switch")
self.leaving_detected()
if event.value == "OFF" and self.state == "leaving":
if event.value == "OFF" and self.state == PresenceState.LEAVING.value:
self._instance_logger.debug("Abort leaving through leaving switch")
self.abort_leaving()

Expand All @@ -157,10 +159,10 @@ def _cb_presence(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> No
:param event: Item state change event of presence item
"""
if event.value == "ON" and self.state in {"absence", "long_absence"}:
if event.value == "ON" and self.state in {PresenceState.ABSENCE.value, PresenceState.LONG_ABSENCE.value}:
self._instance_logger.debug("Presence was set manually by presence switch")
self.presence_detected()
elif event.value == "OFF" and self.state in {"presence", "leaving"}:
elif event.value == "OFF" and self.state in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value}:
self._instance_logger.debug("Absence was set manually by presence switch")
self.absence_detected()

Expand All @@ -176,7 +178,7 @@ def _cb_phone(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
self.__phone_absence_timer.cancel()
self.__phone_absence_timer = None

if self.state in {"absence", "long_absence"}:
if self.state in {PresenceState.ABSENCE.value, PresenceState.LONG_ABSENCE.value}:
self._instance_logger.debug("Presence was set through first phone joined network")
self.presence_detected()

Expand All @@ -187,7 +189,15 @@ def _cb_phone(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:

def __set_leaving_through_phone(self) -> None:
"""Set leaving detected if timeout expired."""
if self.state == "presence":
if self.state == PresenceState.PRESENCE.value:
self._instance_logger.debug("Leaving was set, because last phone left some time ago.")
self.leaving_detected()
self.__phone_absence_timer = None

def on_rule_removed(self) -> None:
habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed(self)

# stop phone absence timer
if self.__phone_absence_timer:
self.__phone_absence_timer.cancel()
self.__phone_absence_timer = None
8 changes: 4 additions & 4 deletions habapp_rules/system/sleep.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ def _update_openhab_state(self):

# update sleep state
if self.state in {"pre_sleeping", "sleeping"}:
habapp_rules.core.helper.send_if_different(self.__item_sleep.name, "ON")
habapp_rules.core.helper.send_if_different(self.__item_sleep, "ON")
else:
habapp_rules.core.helper.send_if_different(self.__item_sleep.name, "OFF")
habapp_rules.core.helper.send_if_different(self.__item_sleep, "OFF")

# update lock state
self.__update_lock_state()
Expand Down Expand Up @@ -179,9 +179,9 @@ def __update_lock_state(self):
"""Update the return lock state value of OpenHAB item."""
if self.__item_lock is not None:
if self.state in {"pre_sleeping", "post_sleeping", "locked"}:
habapp_rules.core.helper.send_if_different(self.__item_lock.name, "ON")
habapp_rules.core.helper.send_if_different(self.__item_lock, "ON")
else:
habapp_rules.core.helper.send_if_different(self.__item_lock.name, "OFF")
habapp_rules.core.helper.send_if_different(self.__item_lock, "OFF")

def _cb_sleep_request(self, event: HABApp.openhab.events.ItemStateChangedEvent):
"""Callback, which is called if sleep request item changed state.
Expand Down
48 changes: 30 additions & 18 deletions tests/common/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,24 @@ def test_or_callback_contact(self):
self.assertEqual(step.expected_output, output_item.value)


class TestMinMax(tests.helper.test_case_base.TestCaseBase):
"""Tests for AND / OR."""
class TestNumericLogic(tests.helper.test_case_base.TestCaseBase):
"""Tests for And / Or / Sum."""

def setUp(self) -> None:
"""Setup unit-tests."""
tests.helper.test_case_base.TestCaseBase.setUp(self)

tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_out_min", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_out_max", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_out_min", None)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_out_max", None)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_out_sum", None)

tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in1", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in2", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in3", 0)

tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_out_min", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_out_max", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_out_min", None)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_out_max", None)

tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in1", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in2", 0)
tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in3", 0)
Expand All @@ -215,37 +218,41 @@ def test_base_init_exceptions(self):
and_rule = habapp_rules.common.logic.Max(["Unittest_Number_in1", item_name], "Unittest_Number_out_max")
self.assertEqual(["Unittest_Number_in1"], [itm.name for itm in and_rule._input_items])

def test_number_min_max_without_filter(self):
"""Test min / max for number items."""
TestStep = collections.namedtuple("TestStep", "event_item_index, event_item_value, expected_min, expected_max")
def test_number_min_max_sum_without_filter(self):
"""Test min / max / sum for number items."""
TestStep = collections.namedtuple("TestStep", "event_item_index, event_item_value, expected_min, expected_max, expected_sum")

test_steps = [
# test change single value
TestStep(1, 100, 0, 100),
TestStep(1, 0, 0, 0),
TestStep(1, -100, -100, 0),
TestStep(1, 100, 0, 100, 100),
TestStep(1, 0, 0, 0, 0),
TestStep(1, -100, -100, 0, -100),

# change all values to 5000
TestStep(1, 5000, 0, 5000),
TestStep(2, 5000, 0, 5000),
TestStep(3, 5000, 5000, 5000),
TestStep(1, 5000, 0, 5000, 5000),
TestStep(2, 5000, 0, 5000, 10_000),
TestStep(3, 5000, 5000, 5000, 15_000),

# some random values
TestStep(3, -1000, -1000, 5000),
TestStep(3, -500, -500, 5000),
TestStep(1, 200, -500, 5000)
TestStep(3, -1000, -1000, 5000, 9000),
TestStep(3, -500, -500, 5000, 9500),
TestStep(1, 200, -500, 5000, 4700)
]

habapp_rules.common.logic.Min(["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], "Unittest_Number_out_min")
habapp_rules.common.logic.Max(["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], "Unittest_Number_out_max")
habapp_rules.common.logic.Sum(["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], "Unittest_Number_out_sum")

output_item_number_min = HABApp.openhab.items.NumberItem.get_item("Unittest_Number_out_min")
output_item_number_max = HABApp.openhab.items.NumberItem.get_item("Unittest_Number_out_max")
output_item_number_sum = HABApp.openhab.items.NumberItem.get_item("Unittest_Number_out_sum")

for step in test_steps:
tests.helper.oh_item.item_state_change_event(f"Unittest_Number_in{step.event_item_index}", step.event_item_value)

self.assertEqual(step.expected_min, output_item_number_min.value)
self.assertEqual(step.expected_max, output_item_number_max.value)
self.assertEqual(step.expected_sum, output_item_number_sum.value)

def test_dimmer_min_max_without_filter(self):
"""Test min / max for dimmer items."""
Expand Down Expand Up @@ -291,3 +298,8 @@ def test_cb_input_event(self):
with unittest.mock.patch("habapp_rules.core.helper.filter_updated_items", return_value=[None]), unittest.mock.patch.object(rule_max, "_set_output_state") as set_output_mock:
rule_max._cb_input_event(None)
set_output_mock.assert_not_called()

def test_exception_dimmer_sum(self):
"""Test exception if Sum is instantiated with dimmer items."""
with self.assertRaises(TypeError):
habapp_rules.common.logic.Sum(["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], "Unittest_Dimmer_out_max")
Loading

0 comments on commit 7a449d6

Please sign in to comment.