diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 8c28d3e..e04bf02 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -2,38 +2,39 @@ name: Create Docker Image
on:
release:
- types: [published]
+ types: [published]
workflow_dispatch:
jobs:
buildx:
runs-on: ubuntu-latest
+ environment: dockerhub
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- ref: main
-
- - name: Get latest release
- id: latest_version
- uses: abatilo/release-info-action@v1.3.1
- with:
- owner: nobbi1991
- repo: HABAppRules
-
- - name: Login to Docker Hub
- uses: docker/login-action@v1
- with:
- username: ${{ secrets.DOCKER_HUB_USERNAME }}
- password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v1
- - name: Build and push
- uses: docker/build-push-action@v2
- with:
- context: .
- file: ./Dockerfile
- push: true
- tags: |
- ${{ secrets.DOCKER_HUB_USERNAME }}/habapp_rules:latest
- ${{ secrets.DOCKER_HUB_USERNAME }}/habapp_rules:${{ steps.latest_version.outputs.latest_tag }}
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: main
+
+ - name: Get latest release
+ id: latest_version
+ uses: abatilo/release-info-action@v1.3.3
+ with:
+ owner: nobbi1991
+ repo: HABAppRules
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: |
+ ${{ secrets.DOCKER_HUB_USERNAME }}/habapp_rules:latest
+ ${{ secrets.DOCKER_HUB_USERNAME }}/habapp_rules:${{ steps.latest_version.outputs.latest_tag }}
diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml
index 6f15c9b..5f4fbcf 100644
--- a/.github/workflows/publish_pypi.yml
+++ b/.github/workflows/publish_pypi.yml
@@ -1,32 +1,31 @@
-name: Publish Python distributions to PyPI
+name: Publish Python Package
+
on:
release:
types: [published]
workflow_dispatch:
jobs:
- build-n-publish:
- name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
+ publish:
runs-on: ubuntu-latest
-
+ environment: pypi
steps:
- - uses: actions/checkout@v4
- with:
- ref: main
- - name: Set up Python 3.10
- uses: actions/setup-python@v2
- with:
- python-version: '3.10'
+ - name: Check out code
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.x"
+
+ - name: Install Hatch
+ run: pip install hatch
+
+ - name: Build the package
+ run: hatch build
- - name: Install setuptools
- run: |
- python -m pip install --upgrade pip
- python -m pip install --upgrade setuptools wheel twine
- - name: Build a binary wheel and a source tarball
- run: |
- python setup.py sdist bdist_wheel
- - name: Publish distribution to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
- with:
- user: __token__
- password: ${{ secrets.pypi_api_key }}
\ No newline at end of file
+ - name: Publish to PyPI
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.pypi_api_key }}
+ run: pip install twine && twine upload dist/*
diff --git a/.github/workflows/run_nox.yml b/.github/workflows/run_nox.yml
deleted file mode 100644
index 7cb125b..0000000
--- a/.github/workflows/run_nox.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# This is a basic workflow to help you get started with Actions
-
-name: Run Nox
-
-on:
- pull_request:
- workflow_dispatch:
-
-jobs:
- run_all_tests:
- name: Run nox
- runs-on: ubuntu-latest
- steps:
- - name: checkout repo
- uses: actions/checkout@v4
- - name: setup nox
- uses: excitedleigh/setup-nox@main
- - name: run nox
- run: nox
- - name: upload coverage report
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: coverage-report
- path: ///home/runner/work/HABAppRules/HABAppRules/tests/htmlcov
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
new file mode 100644
index 0000000..484130f
--- /dev/null
+++ b/.github/workflows/run_tests.yml
@@ -0,0 +1,64 @@
+name: Run Tests
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pre-commit
+
+ - name: Run Pre-Commit Hooks
+ run: |
+ pre-commit install
+ pre-commit run --all-files
+
+ unit-tests:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Hatch
+ run: |
+ python -m pip install --upgrade pip
+ pip install hatch
+
+ - name: Run Hatch Tests
+ run: |
+ hatch run tests:local
+
+ - name: Upload Coverage Report on Failure
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report-${{ matrix.python-version }}
+ path: htmlcov
diff --git a/.idea/HABAppRules.iml b/.idea/HABAppRules.iml
index 377f53d..e935fb8 100644
--- a/.idea/HABAppRules.iml
+++ b/.idea/HABAppRules.iml
@@ -15,11 +15,16 @@
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index a55e7a1..1a0176a 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
index dd4c951..c57e002 100644
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -4,4 +4,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d473ed7..03366d8 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,10 @@
+
+
+
-
\ No newline at end of file
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 2e60cb0..248f495 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -5,4 +5,4 @@
-
\ No newline at end of file
+
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..fde3780
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,55 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+default_install_hook_types: [pre-push]
+repos:
+ # - repo: https://github.com/pre-commit/mirrors-mypy
+ # rev: v1.13.0
+ # hooks:
+ # - id: mypy
+ # args: [--install-types, --non-interactive]
+ # exclude: ^tests/
+ # additional_dependencies:
+ # - pydantic
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-ast
+ - id: check-case-conflict
+ - id: check-executables-have-shebangs
+ - id: check-json
+ - id: check-toml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: fix-byte-order-marker
+ - id: trailing-whitespace
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.7.0
+ hooks:
+ - id: ruff
+ args: [--fix]
+ - id: ruff-format
+ - repo: https://github.com/google/yamlfmt
+ rev: v0.13.0
+ hooks:
+ - id: yamlfmt
+ args: [-conf, .yamlfmt.yaml]
+ - repo: https://github.com/scop/pre-commit-shfmt
+ rev: v3.10.0-1
+ hooks:
+ - id: shfmt
+ - repo: https://github.com/executablebooks/mdformat
+ rev: 0.7.18
+ hooks:
+ - id: mdformat
+
+ - repo: local
+ hooks:
+ - id: version-check
+ name: Check versions
+ entry: python version_check.py
+ language: python
+ pass_filenames: false
+ additional_dependencies:
+ - requests
+ - pytz
diff --git a/.yamlfmt.yaml b/.yamlfmt.yaml
new file mode 100644
index 0000000..2f0850f
--- /dev/null
+++ b/.yamlfmt.yaml
@@ -0,0 +1,5 @@
+formatter:
+ type: basic
+ eof_newline: true
+ max_line_length: 120
+ retain_line_breaks_single: true
diff --git a/Dockerfile b/Dockerfile
index 0b67232..85220c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.11 as buildimage
+FROM python:3.13 as buildimage
COPY . /tmp/app_install
@@ -7,7 +7,7 @@ RUN set -eux; \
cd /tmp/app_install; \
pip wheel --wheel-dir=/root/wheels .
-FROM python:3.11
+FROM python:3.13
COPY --from=buildimage /root/wheels /root/wheels
COPY container/entrypoint.sh /entrypoint.sh
diff --git a/changelog.md b/changelog.md
index f5fd3a9..8472733 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,46 +1,54 @@
-# Version 6.3.0 - dd.10.2024
+# Version 7.0.0 - 22.11.2024
+
+## Breaking changes
+
+- bumped HABApp to 24.11.0. Check [release info](https://community.openhab.org/t/habapp-24/152810/27)
+- updated docker container to use python 3.13
+- renamed all `habapp_rules` exceptions to error. E.g. `habapp_rules.HabAppRulesException` to `habapp_rules.HabAppRulesError`
+- renamed `habapp_rules.system.watchdog` to `habapp_rules.system.item_watchdog`
+- moved rules of `habapp_rules.actors.power` to `habapp_rules.sensors.current_switch`
## Features
-- Added rules in ``habapp_rules.actors.energy_save_switch`` to switch off sockets during sleeping time or at absence to save energy.
+- Added rules in `habapp_rules.actors.energy_save_switch` to switch off sockets during sleeping time or at absence to save energy.
## Bugfix
-- moved rules of ``habapp_rules.actors.power`` to ``habapp_rules.sensors.current_switch``. Old location is still supported, but deprecated
-- fixed wrong item name in ``habapp_rules.energy.monthly_report.MonthlyReport``
+- moved rules of `habapp_rules.actors.power` to `habapp_rules.sensors.current_switch`. Old location is still supported, but deprecated
+- fixed wrong item name in `habapp_rules.energy.monthly_report.MonthlyReport`
# Version 6.2.0 - 06.10.2024
## Features
-- added rule ``habapp_rules.system.notification.SendStateChanged`` which can be used to send a mail or telegram message if the state of an item changes
-- added rule ``habapp_rules.actors.heating.KnxHeating`` which can be used to set the target temperature of a KNX heating actor which only supports temperature offsets
-- added temperature difference item of ``habapp_rules.sensors.sun.SensorTemperatureDifference`` to ``filtered_signal_groups``
-- added rule ``habapp_rules.actors.power.CurrentSwitch`` which can be used to enable a switch item if current is above a threshold
-- added rule ``habapp_rules.system.watchdog.Watchdog`` which can be used to check if an item was updated in time
+- added rule `habapp_rules.system.notification.SendStateChanged` which can be used to send a mail or telegram message if the state of an item changes
+- added rule `habapp_rules.actors.heating.KnxHeating` which can be used to set the target temperature of a KNX heating actor which only supports temperature offsets
+- added temperature difference item of `habapp_rules.sensors.sun.SensorTemperatureDifference` to `filtered_signal_groups`
+- added rule `habapp_rules.actors.power.CurrentSwitch` which can be used to enable a switch item if current is above a threshold
+- added rule `habapp_rules.system.watchdog.Watchdog` which can be used to check if an item was updated in time
## Bugfix
-- fixed bug in ``habapp_rules.actors.light.LightSwitchExtended`` and ``habapp_rules.actors.light.LightDimmerExtended`` which did not re-trigger the timer if a door was opened.
-- fixed bug in all rules of ``habapp_rules.actors.light`` where a timer with time=None was used if a light function is not active. Now, the time is changed to 0 sec if a function is not configured.
+- fixed bug in `habapp_rules.actors.light.LightSwitchExtended` and `habapp_rules.actors.light.LightDimmerExtended` which did not re-trigger the timer if a door was opened.
+- fixed bug in all rules of `habapp_rules.actors.light` where a timer with time=None was used if a light function is not active. Now, the time is changed to 0 sec if a function is not configured.
# Version 6.1.0 - 19.08.2024
## Features
-- added support for dimmer items which can be configured for ``switch_on`` for all rules in ``habapp_rules.actors.light_hcl``
+- added support for dimmer items which can be configured for `switch_on` for all rules in `habapp_rules.actors.light_hcl`
- bumped versions:
- - HABApp to 24.08.1
- - multi-notifier to 0.5.0
- - holidays to 0.53
+ - HABApp to 24.08.1
+ - multi-notifier to 0.5.0
+ - holidays to 0.53
# Version 6.0.1 - 22.07.2024
## Bugfix
-- round light color of all rules in ``habapp_rules.actors.light_hcl`` to integer values to avoid strange formating in OpenHAB
-- added config parameter ``leaving_only_if_on`` to ``habapp_rules.actors.config.light.LightParameter`` to disable unexpected leaving light, if light was not on and leaving started
-- fixed bug in all shading rules of ``habapp_rules.actors.shading`` which did not switch to sleeping state if previous state was Auto_DoorOpen
+- round light color of all rules in `habapp_rules.actors.light_hcl` to integer values to avoid strange formating in OpenHAB
+- added config parameter `leaving_only_if_on` to `habapp_rules.actors.config.light.LightParameter` to disable unexpected leaving light, if light was not on and leaving started
+- fixed bug in all shading rules of `habapp_rules.actors.shading` which did not switch to sleeping state if previous state was Auto_DoorOpen
# Version 6.0.0 - 27.06.2024
@@ -50,46 +58,46 @@
## Features
-- added additional config to ``habapp_rules.actors.shading.Shutter`` and ``habapp_rules.actors.shading.Raffstore`` which allows to set different positions for day and night if sleeping is active
-- added possibility to pass shading objects to ``habapp_rules.actors.shading.ResetAllManualHand`` which should be reset by this rule
-- added ``habapp_rules.sensors.humidity.HumiditySwitch`` to set a switch item if high humidity is detected. Currently only a absolut threshold is accepted
-- send update of summer / winter of ``habapp_rules.system.summer_winter.SummerWinter`` after every check. If this rule is used to send the summer / winter state to the KNX bus, this ensures, that the state is sent at least once a day
-- added hysteresis switch to ``habapp_rules.sensors.sun.SensorBrightness`` and ``habapp_rules.sensors.sun.SunPositionFilter``. Breaking change: Parameter order changed!
+- added additional config to `habapp_rules.actors.shading.Shutter` and `habapp_rules.actors.shading.Raffstore` which allows to set different positions for day and night if sleeping is active
+- added possibility to pass shading objects to `habapp_rules.actors.shading.ResetAllManualHand` which should be reset by this rule
+- added `habapp_rules.sensors.humidity.HumiditySwitch` to set a switch item if high humidity is detected. Currently only a absolut threshold is accepted
+- send update of summer / winter of `habapp_rules.system.summer_winter.SummerWinter` after every check. If this rule is used to send the summer / winter state to the KNX bus, this ensures, that the state is sent at least once a day
+- added hysteresis switch to `habapp_rules.sensors.sun.SensorBrightness` and `habapp_rules.sensors.sun.SunPositionFilter`. Breaking change: Parameter order changed!
- bumped holidays to 0.51
- bumped matplotlib to 3.9.0
## Bugfix
-- fixed bug in ``habapp_rules.actors.shading.Shutter`` and ``habapp_rules.actors.shading.Raffstore`` which caused the ``Hand`` state if MDT actors are used
+- fixed bug in `habapp_rules.actors.shading.Shutter` and `habapp_rules.actors.shading.Raffstore` which caused the `Hand` state if MDT actors are used
# Version 5.7.0 - 09.04.2024
## Features
-- added possibility to add groups to ``habapp_rules.core.helper.create_additional_item``
-- added possibility to add groups to ``habapp_rules.sensors.sun.SensorBrightness`` and ``habapp_rules.sensors.sun.SensorTemperatureDifference``
+- added possibility to add groups to `habapp_rules.core.helper.create_additional_item`
+- added possibility to add groups to `habapp_rules.sensors.sun.SensorBrightness` and `habapp_rules.sensors.sun.SensorTemperatureDifference`
# Bugfix
-- fixed bug in ``habapp_rules.core.helper.create_additional_item`` which added a ``[%s]`` to string items
+- fixed bug in `habapp_rules.core.helper.create_additional_item` which added a `[%s]` to string items
# Version 5.6.2 - 02.04.2024
# Bugfix
-- fixed bug of all rules in ``habapp_rules.actors.ventilation`` which raised an exception if presence changed to long absence.
+- fixed bug of all rules in `habapp_rules.actors.ventilation` which raised an exception if presence changed to long absence.
# Version 5.6.1 - 01.04.2024
# Bugfix
-- fixed bug of missing resources of ``habapp_rules.energy.montly_report.MonthlyReport`` if used in docker container
+- fixed bug of missing resources of `habapp_rules.energy.montly_report.MonthlyReport` if used in docker container
# Version 5.6.0 - 24.03.2024
# Features
-- added ``habapp_rules.energy.montly_report.MonthlyReport`` for generating a monthly energy report mail
+- added `habapp_rules.energy.montly_report.MonthlyReport` for generating a monthly energy report mail
- bumped holidays to 0.45
- bumped multi-notifier to 0.4.0
@@ -97,113 +105,113 @@
# Bugfix
-- fixed bug in ``habapp_rules.actors.shading._ShadingBase``, which caused an exception if night state was checked but no day/night item was given
+- fixed bug in `habapp_rules.actors.shading._ShadingBase`, which caused an exception if night state was checked but no day/night item was given
# Version 5.5.0 - 05.03.2024
## Features
-- added rules in ``habapp_rules.actors.ventilation`` to control ventilation objects
-- added ``name_switch_on`` to ``habapp_rules.actors.light_hcl.HclTime`` and ``habapp_rules.actors.light_hcl.HclElevation`` to add the possibility to also update the color if a item switches on
-- added new transition to ``habapp_rules.actors.light._LightExtendedMixin`` to also switch on the light if current state is ``auto_preoff`` and the door opened
-- added ``habapp_rules.sensors.dwd.DwdWindAlarm`` to set wind alarm depending on DWD warnings
-- added ``habapp_rules.core.version.SetVersions`` to set versions of HABApp and habapp_rules to OpenHAB items
-- added ``habapp_rules.common.logic.InvertValue`` which can be used to set the inverted value of one item to another
+- added rules in `habapp_rules.actors.ventilation` to control ventilation objects
+- added `name_switch_on` to `habapp_rules.actors.light_hcl.HclTime` and `habapp_rules.actors.light_hcl.HclElevation` to add the possibility to also update the color if a item switches on
+- added new transition to `habapp_rules.actors.light._LightExtendedMixin` to also switch on the light if current state is `auto_preoff` and the door opened
+- added `habapp_rules.sensors.dwd.DwdWindAlarm` to set wind alarm depending on DWD warnings
+- added `habapp_rules.core.version.SetVersions` to set versions of HABApp and habapp_rules to OpenHAB items
+- added `habapp_rules.common.logic.InvertValue` which can be used to set the inverted value of one item to another
- bumped holidays to 0.44
- bumped HABApp to 24.02.0
# Bugfix
-- fixed bug in ``habapp_rules.actors.state_observer.StateObserverNumber`` which triggered the manual-detected-callback if the received number deviates only a little bit because of data types. (e.g.: 1.000001 != 1.0)
-- fixed bug for dimmer lights in ``habapp_rules.actors.light`` which did not set the correct brightness if light was switched on.
-- fixed bug in ``habapp_rules.common.hysteresis.HysteresisSwitch.get_output`` resulted in a wrong switch state if the value was 0.
-- added missing state transition to ``habapp_rules.sensors.motion.Motion``. When state was ``PostSleepLocked`` and sleep started there was no change to ``SleepLocked``
-- fixed strange behavior of ``habapp_rules.system.presence.Presence`` which did not abort leaving when the first phone appeared. This let to absence state if someone returned when leaving was active.
+- fixed bug in `habapp_rules.actors.state_observer.StateObserverNumber` which triggered the manual-detected-callback if the received number deviates only a little bit because of data types. (e.g.: 1.000001 != 1.0)
+- fixed bug for dimmer lights in `habapp_rules.actors.light` which did not set the correct brightness if light was switched on.
+- fixed bug in `habapp_rules.common.hysteresis.HysteresisSwitch.get_output` resulted in a wrong switch state if the value was 0.
+- added missing state transition to `habapp_rules.sensors.motion.Motion`. When state was `PostSleepLocked` and sleep started there was no change to `SleepLocked`
+- fixed strange behavior of `habapp_rules.system.presence.Presence` which did not abort leaving when the first phone appeared. This let to absence state if someone returned when leaving was active.
# Version 5.4.3 - 14.01.2024
## Bugfix
-- fixed bug in ``habapp_rules.actors.shading.Raffstore`` which triggered a hand detection also if only small slat differences occurred
+- fixed bug in `habapp_rules.actors.shading.Raffstore` which triggered a hand detection also if only small slat differences occurred
# Version 5.4.2 - 14.01.2024
## Bugfix
-- fixed bug in all observers of ``habapp_rules.actors.state_observer`` which triggered the manual callback also if the value change of numeric values is tiny
-- fixed bug in ``habapp_rules.actors.shading._ShadingBase`` which triggered a hand detection also if only small position differences occurred
+- fixed bug in all observers of `habapp_rules.actors.state_observer` which triggered the manual callback also if the value change of numeric values is tiny
+- fixed bug in `habapp_rules.actors.shading._ShadingBase` which triggered a hand detection also if only small position differences occurred
# Version 5.4.1 - 26.12.2023
## Bugfix
-- fixed bug in ``habapp_rules.core.state_machine.StateMachineRule`` which prevents inheritance of ``habapp_rules``-rules in local rules
+- fixed bug in `habapp_rules.core.state_machine.StateMachineRule` which prevents inheritance of `habapp_rules`-rules in local rules
# Version 5.4.0 - 25.12.2023
## Features
- added dependabot to keep all dependencies up to date
-- added ``habapp_rules.actors.light_hcl`` for setting light temperature depending on time or sun elevation
-- added ``habapp_rules.actors.state_observer.StateObserverNumber`` for observe state changes of a number item
+- added `habapp_rules.actors.light_hcl` for setting light temperature depending on time or sun elevation
+- added `habapp_rules.actors.state_observer.StateObserverNumber` for observe state changes of a number item
## Bugfix
-- fixed too short restore time for all light rules when sleep was aborted in ``habapp_rules.actors.light._LightBase``
+- fixed too short restore time for all light rules when sleep was aborted in `habapp_rules.actors.light._LightBase`
# Version 5.3.1 - 30.11.2023
## Bugfix
-- fixed bug in ``habapp_rules.core.state_machine_rule.on_rule_removed`` which did not remove rules which have a hierarchical state machine
+- fixed bug in `habapp_rules.core.state_machine_rule.on_rule_removed` which did not remove rules which have a hierarchical state machine
# Version 5.3.0 - 21.11.2023
## Features
-- added ``habapp_rules.common.logic.Sum`` for calculation the sum of number items
+- 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``
+- 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
-- fixed bug in ``habapp_rules.actors.shading.ResetAllManualHand`` which did not reset all shading objects if triggered via KNX
+- fixed bug in `habapp_rules.actors.shading.ResetAllManualHand` which did not reset all shading objects if triggered via KNX
# Version 5.2.0 - 10.10.2023
## Features
-- added rule ``habapp_rules.system.sleep.LinkSleep`` to link multiple sleep rules
+- added rule `habapp_rules.system.sleep.LinkSleep` to link multiple sleep rules
## Bugfix
-- fixed bug in ``habapp_rules.actors.shading.ResetAllManualHand`` which did not always reset all shading instances.
-- fixed bug in ``habapp_rules.actors.shading._ShadingBase`` which caused wrong shading states after sleeping or night
+- fixed bug in `habapp_rules.actors.shading.ResetAllManualHand` which did not always reset all shading instances.
+- fixed bug in `habapp_rules.actors.shading._ShadingBase` which caused wrong shading states after sleeping or night
# Version 5.1.0 - 06.10.2023
## Features
-- added rule ``habapp_rules.sensors.astro.SetNight`` and ``habapp_rules.sensors.astro.SetDay`` to set / unset night and day state depending on sun elevation
+- added rule `habapp_rules.sensors.astro.SetNight` and `habapp_rules.sensors.astro.SetDay` to set / unset night and day state depending on sun elevation
## Bugfix
-- fixed bug in ``habapp_rules.actors.shading._ShadingBase`` which caused a switch to night close if it was not configured.
+- fixed bug in `habapp_rules.actors.shading._ShadingBase` which caused a switch to night close if it was not configured.
# Version 5.0.0 - 01.10.2023
## Breaking changes
-- added support for more than two sensor values to ``habapp_rules.sensors.sun.SensorTemperatureDifference``. Breaking change: Item names must be given as list of names.
+- added support for more than two sensor values to `habapp_rules.sensors.sun.SensorTemperatureDifference`. Breaking change: Item names must be given as list of names.
## Features
-- added logic functions ``habapp_rules.common.logic.Min`` and ``habapp_rules.common.logic.Max``
+- added logic functions `habapp_rules.common.logic.Min` and `habapp_rules.common.logic.Max`
- updated HABApp to 23.09.02
# Version 4.1.0 - 27.09.2023
@@ -216,31 +224,31 @@
## Breaking changes
-- renamed ``habapp_rules.actors.light.Light`` to ``habapp_rules.actors.light.LightDimmer`` and ``habapp_rules.actors.light.LightExtended`` to ``habapp_rules.actors.light.LightDimmerExtended``
-- moved / renamed ``habapp_rules.actors.light_config`` to ``habapp_rules.actors.config.light``
-- changed parameter names and order of ``habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge`` and added support for KNX switch items
-- all items which are created from habapp_rules start with prefix ``H_``
-- removed ``_create_additional_item`` from ``habapp_rules.core.state_machine_rule.StateMachineRule`` and added it as standalone function: ``habapp_rules.core.helper.create_additional_item``
+- renamed `habapp_rules.actors.light.Light` to `habapp_rules.actors.light.LightDimmer` and `habapp_rules.actors.light.LightExtended` to `habapp_rules.actors.light.LightDimmerExtended`
+- moved / renamed `habapp_rules.actors.light_config` to `habapp_rules.actors.config.light`
+- changed parameter names and order of `habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge` and added support for KNX switch items
+- all items which are created from habapp_rules start with prefix `H_`
+- removed `_create_additional_item` from `habapp_rules.core.state_machine_rule.StateMachineRule` and added it as standalone function: `habapp_rules.core.helper.create_additional_item`
## Features
-- added ``habapp_rules.actors.light.LightSwitch`` and ``habapp_rules.actors.light.LightSwitchExtended`` which add the support for ``switch`` lights
-- added ``habapp_rules.sensors.sun`` to handle and filter different kind of sun sensors
-- added ``habapp_rules.common.filter.ExponentialFilter`` to apply a exponential filter to a number item. This can be used to smoothen signals.
-- added ``habapp_rules.actors.shading`` to handle shading objects
+- added `habapp_rules.actors.light.LightSwitch` and `habapp_rules.actors.light.LightSwitchExtended` which add the support for `switch` lights
+- added `habapp_rules.sensors.sun` to handle and filter different kind of sun sensors
+- added `habapp_rules.common.filter.ExponentialFilter` to apply a exponential filter to a number item. This can be used to smoothen signals.
+- added `habapp_rules.actors.shading` to handle shading objects
- increased startup speed by upgrading to HABApp==23.09.0
# Version 3.1.1 - 08.05.2023
## Bugfix
-- fixed bug of ``habapp_rules.actors.irrigation.Irrigation`` where type of Number item could be float type
+- fixed bug of `habapp_rules.actors.irrigation.Irrigation` where type of Number item could be float type
# Version 3.1.0 - 08.05.2023
## Features
-- added ``habapp_rules.actors.irrigation.Irrigation`` to control basic irrigation systems
+- added `habapp_rules.actors.irrigation.Irrigation` to control basic irrigation systems
# Version 3.0.1 - 28.04.2023
@@ -252,30 +260,30 @@
## Breaking changes
-- Moved some modules from ``common`` to ``core``
-- Changed parameter order of ``habapp_rules.system.presence.Presence``
+- Moved some modules from `common` to `core`
+- Changed parameter order of `habapp_rules.system.presence.Presence`
## Features
-- Added ``habapp_rules.actors.light`` to control dimmer lights (switch lights will be supported later):
- - ``habapp_rules.actors.light.Light`` for basic light functionality like switch-on brightness or leaving / sleeping light
- - ``habapp_rules.actors.light.LightExtended`` includes everything from ``habapp_rules.actors.light.Light`` plus switch on depending on motion or opening of a door
-- Added ``habapp_rules.sensors.motion`` to filter motion sensors
-- Added ``habapp_rules.common.hysteresis`` as a helper for value depended switch with hysteresis
-- Added ``habapp_rules.core.timeout_list``
-- Added logging of ``habapp_rules`` version
-- Added ``habapp_rules.common.hysteresis`` which implements a hysteresis switch
-- Changed ``habapp_rules.system.summer_winter`` that one full day of data is enough for summer / winter detected, also if more days are set for mean calculation
+- Added `habapp_rules.actors.light` to control dimmer lights (switch lights will be supported later):
+ - `habapp_rules.actors.light.Light` for basic light functionality like switch-on brightness or leaving / sleeping light
+ - `habapp_rules.actors.light.LightExtended` includes everything from `habapp_rules.actors.light.Light` plus switch on depending on motion or opening of a door
+- Added `habapp_rules.sensors.motion` to filter motion sensors
+- Added `habapp_rules.common.hysteresis` as a helper for value depended switch with hysteresis
+- Added `habapp_rules.core.timeout_list`
+- Added logging of `habapp_rules` version
+- Added `habapp_rules.common.hysteresis` which implements a hysteresis switch
+- Changed `habapp_rules.system.summer_winter` that one full day of data is enough for summer / winter detected, also if more days are set for mean calculation
## Bugfix
-- fixed bug of ``habapp_rules.system.presence.Presence`` which avoided instantiation if no phone outside_doors where given
-- fixed bug of ``habapp_rules.core.state_machine.StateMachineRule._create_additional_item`` which returned a bool value instead of the created item if an item was created
+- fixed bug of `habapp_rules.system.presence.Presence` which avoided instantiation if no phone outside_doors where given
+- fixed bug of `habapp_rules.core.state_machine.StateMachineRule._create_additional_item` which returned a bool value instead of the created item if an item was created
## GitHub Actions
- Changed updated checkout@v2 to checkout@v3 which uses node16
-- Removed ``helper`` submodule and switched to ``nose_helper`` package
+- Removed `helper` submodule and switched to `nose_helper` package
# Version 2.1.1 - 04.02.2023
diff --git a/container/entrypoint.sh b/container/entrypoint.sh
index a3997fa..79fcc37 100644
--- a/container/entrypoint.sh
+++ b/container/entrypoint.sh
@@ -7,19 +7,19 @@ NEW_GROUP_ID=${GROUP_ID:-$NEW_USER_ID}
echo "Starting with habapp user id: $NEW_USER_ID and group id: $NEW_GROUP_ID"
if ! id -u habapp >/dev/null 2>&1; then
- if [ -z "$(getent group $NEW_GROUP_ID)" ]; then
- echo "Create group habapp with id ${NEW_GROUP_ID}"
- groupadd -g $NEW_GROUP_ID habapp
- else
- group_name=$(getent group $NEW_GROUP_ID | cut -d: -f1)
- echo "Rename group $group_name to habapp"
- groupmod --new-name habapp $group_name
- fi
- echo "Create user habapp with id ${NEW_USER_ID}"
- adduser -u $NEW_USER_ID --disabled-password --gecos '' --home "${HABAPP_HOME}" --gid ${NEW_GROUP_ID} habapp
+ if [ -z "$(getent group $NEW_GROUP_ID)" ]; then
+ echo "Create group habapp with id ${NEW_GROUP_ID}"
+ groupadd -g $NEW_GROUP_ID habapp
+ else
+ group_name=$(getent group $NEW_GROUP_ID | cut -d: -f1)
+ echo "Rename group $group_name to habapp"
+ groupmod --new-name habapp $group_name
+ fi
+ echo "Create user habapp with id ${NEW_USER_ID}"
+ adduser -u $NEW_USER_ID --disabled-password --gecos '' --home "${HABAPP_HOME}" --gid ${NEW_GROUP_ID} habapp
fi
chown -R habapp:habapp "${HABAPP_HOME}/config"
sync
-exec "$@"
\ No newline at end of file
+exec "$@"
diff --git a/custom_pylint_dictionary.txt b/custom_pylint_dictionary.txt
deleted file mode 100644
index 5d342c5..0000000
--- a/custom_pylint_dictionary.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-astro
-autoupdate
-bool
-config
-dataclass
-donut
-dwd
-DWD
-EnergySaveSwitchItems
-enums
-ga
-HCL
-HABApp
-html
-init
-KNX
-kwargs
-mixin
-mqtt
-msg
-NumberItem
-openhab
-png
-pre
-pragma
-pydantic
-raffstore
-Raffstore
-rollershutter
-Rollershutter
-repr
-Setpoint
-smtp
-unittest
\ No newline at end of file
diff --git a/habapp_rules/__init__.py b/habapp_rules/__init__.py
index acd2253..5425659 100644
--- a/habapp_rules/__init__.py
+++ b/habapp_rules/__init__.py
@@ -1,4 +1,9 @@
-"""Define all rules for HABApp"""
+"""Define all rules for HABApp."""
+
import pathlib
+import pytz
+
+__version__ = "7.0.0"
BASE_PATH = pathlib.Path(__file__).parent.parent.resolve()
+TIMEZONE = pytz.timezone("Europe/Berlin")
diff --git a/habapp_rules/__version__.py b/habapp_rules/__version__.py
deleted file mode 100644
index 4c14556..0000000
--- a/habapp_rules/__version__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""Set version for the package."""
-__version__ = "6.3.0"
diff --git a/habapp_rules/actors/config/energy_save_switch.py b/habapp_rules/actors/config/energy_save_switch.py
index 2d5290c..dc082b1 100644
--- a/habapp_rules/actors/config/energy_save_switch.py
+++ b/habapp_rules/actors/config/energy_save_switch.py
@@ -7,24 +7,27 @@
class EnergySaveSwitchItems(habapp_rules.core.pydantic_base.ItemBase):
- """Item config for EnergySaveSwitch rule."""
- switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item, which will be handled")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
- manual: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to switch to manual mode and disable the automatic functions")
- presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="presence state set via habapp_rules.presence.Presence")
- sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleeping state set via habapp_rules.system.sleep.Sleep")
- external_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to request ON state from external. This request will overwrite the target state of presence / sleeping.")
- current: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="item which measures the current")
+ """Item config for EnergySaveSwitch rule."""
+
+ switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item, which will be handled")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
+ manual: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to switch to manual mode and disable the automatic functions")
+ presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="presence state set via habapp_rules.presence.Presence")
+ sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleeping state set via habapp_rules.system.sleep.Sleep")
+ external_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to request ON state from external. This request will overwrite the target state of presence / sleeping.")
+ current: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="item which measures the current")
class EnergySaveSwitchParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter config for EnergySaveSwitch rule."""
- max_on_time: int | None = pydantic.Field(None, description="maximum on time in seconds. None means no timeout.")
- hand_timeout: int | None = pydantic.Field(None, description="Fallback time from hand to automatic mode in seconds. None means no timeout.")
- current_threshold: float = pydantic.Field(0.030, description="threshold in Ampere.")
+ """Parameter config for EnergySaveSwitch rule."""
+
+ max_on_time: int | None = pydantic.Field(None, description="maximum on time in seconds. None means no timeout.")
+ hand_timeout: int | None = pydantic.Field(None, description="Fallback time from hand to automatic mode in seconds. None means no timeout.")
+ current_threshold: float = pydantic.Field(0.030, description="threshold in Ampere.")
class EnergySaveSwitchConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for EnergySaveSwitch rule."""
- items: EnergySaveSwitchItems = pydantic.Field(..., description="Config items for power switch rule")
- parameter: EnergySaveSwitchParameter = pydantic.Field(EnergySaveSwitchParameter(), description="Config parameter for power switch rule")
+ """Config for EnergySaveSwitch rule."""
+
+ items: EnergySaveSwitchItems = pydantic.Field(..., description="Config items for power switch rule")
+ parameter: EnergySaveSwitchParameter = pydantic.Field(EnergySaveSwitchParameter(), description="Config parameter for power switch rule")
diff --git a/habapp_rules/actors/config/heating.py b/habapp_rules/actors/config/heating.py
index 412b788..561db2b 100644
--- a/habapp_rules/actors/config/heating.py
+++ b/habapp_rules/actors/config/heating.py
@@ -1,4 +1,5 @@
"""Config models for heating rules."""
+
import HABApp.openhab.items
import pydantic
@@ -6,13 +7,15 @@
class KnxHeatingItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for KNX heating abstraction rule."""
- virtual_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="temperature item, which is used in OpenHAB to set the target temperature")
- actor_feedback_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="temperature item, which holds the current target temperature set by the heating actor")
- temperature_offset: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item for setting the offset temperature")
+ """Items for KNX heating abstraction rule."""
+
+ virtual_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="temperature item, which is used in OpenHAB to set the target temperature")
+ actor_feedback_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="temperature item, which holds the current target temperature set by the heating actor")
+ temperature_offset: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item for setting the offset temperature")
class KnxHeatingConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for KNX heating abstraction rule."""
- items: KnxHeatingItems = pydantic.Field(..., description="items for heating rule")
- parameter: None = None
+ """Config for KNX heating abstraction rule."""
+
+ items: KnxHeatingItems = pydantic.Field(..., description="items for heating rule")
+ parameter: None = None
diff --git a/habapp_rules/actors/config/irrigation.py b/habapp_rules/actors/config/irrigation.py
index 606d1ba..f45358e 100644
--- a/habapp_rules/actors/config/irrigation.py
+++ b/habapp_rules/actors/config/irrigation.py
@@ -1,37 +1,43 @@
"""Config models for irrigation rules."""
-import typing
import HABApp.openhab.items
import pydantic
+import typing_extensions
import habapp_rules.core.exceptions
import habapp_rules.core.pydantic_base
class IrrigationItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for irrigation rules."""
- valve: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="valve item which will be switched")
- active: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to activate the rule")
- hour: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="start hour")
- minute: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="start minute")
- duration: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="duration in minutes")
- repetitions: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="number of repetitions")
- brake: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="time in minutes between repetitions")
+ """Items for irrigation rules."""
- @pydantic.model_validator(mode="after")
- def validate_model(self) -> typing.Self:
- """Validate model
+ valve: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="valve item which will be switched")
+ active: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to activate the rule")
+ hour: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="start hour")
+ minute: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="start minute")
+ duration: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="duration in minutes")
+ repetitions: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="number of repetitions")
+ brake: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="time in minutes between repetitions")
- :return: validated model
- :raises AssertionError: if 'repetitions' and 'brake' are not set together
- """
- if (self.repetitions is None) != (self.brake is None):
- raise AssertionError("If repeats item is given, also the brake item must be given!")
+ @pydantic.model_validator(mode="after")
+ def validate_model(self) -> typing_extensions.Self:
+ """Validate model.
- return self
+ Returns:
+ validated model
+
+ Raises:
+ AssertionError: if 'repetitions' and 'brake' are not set together
+ """
+ if (self.repetitions is None) != (self.brake is None):
+ msg = "If repeats item is given, also the brake item must be given!"
+ raise AssertionError(msg)
+
+ return self
class IrrigationConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for irrigation actors."""
- items: IrrigationItems = pydantic.Field(..., description="items for irrigation rule")
- parameter: None = None
+ """Config for irrigation actors."""
+
+ items: IrrigationItems = pydantic.Field(..., description="items for irrigation rule")
+ parameter: None = None
diff --git a/habapp_rules/actors/config/light.py b/habapp_rules/actors/config/light.py
index 6e3c89e..1e951c9 100644
--- a/habapp_rules/actors/config/light.py
+++ b/habapp_rules/actors/config/light.py
@@ -1,10 +1,11 @@
"""Config models for light rules."""
+
import collections.abc
import logging
-import typing
import HABApp.openhab.items
import pydantic
+import typing_extensions
import habapp_rules.core.exceptions
import habapp_rules.core.pydantic_base
@@ -13,131 +14,165 @@
class LightItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for all light rules."""
- light: HABApp.openhab.items.DimmerItem | HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which controls the light")
- light_control: list[HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="control items to improve manual detection")
- light_groups: list[HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="group items which can additionally set the light state. This can be used to improve the manual detection")
- manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to switch to manual mode and disable the automatic functions")
- presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="presence state set via habapp_rules.presence.Presence")
- day: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which is ON at day and OFF at night")
- sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleeping state set via habapp_rules.system.sleep.Sleep")
- pre_sleep_prevent: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to prevent pre-sleep (Can be used for example to prevent the pre sleep light when guests are sleeping)")
- doors: list[HABApp.openhab.items.ContactItem] = pydantic.Field([], description="door items for switching on the light if the door is opening")
- motion: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="motion sensor to enable light if motion is detected")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
+ """Items for all light rules."""
+
+ light: HABApp.openhab.items.DimmerItem | HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which controls the light")
+ light_control: list[HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="control items to improve manual detection")
+ light_groups: list[HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="group items which can additionally set the light state. This can be used to improve the manual detection")
+ manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to switch to manual mode and disable the automatic functions")
+ presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="presence state set via habapp_rules.presence.Presence")
+ day: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which is ON at day and OFF at night")
+ sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleeping state set via habapp_rules.system.sleep.Sleep")
+ pre_sleep_prevent: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item to prevent pre-sleep (Can be used for example to prevent the pre sleep light when guests are sleeping)")
+ doors: list[HABApp.openhab.items.ContactItem] = pydantic.Field([], description="door items for switching on the light if the door is opening")
+ motion: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="motion sensor to enable light if motion is detected")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
class BrightnessTimeout(pydantic.BaseModel):
- """Define brightness and timeout for light states."""
- brightness: int | bool = pydantic.Field(..., description="brightness which should be set. If bool ON will be sent for True and OFF for False")
- timeout: float = pydantic.Field(..., description="Timeout / max time in seconds until switch off")
+ """Define brightness and timeout for light states."""
+
+ brightness: int | bool = pydantic.Field(..., description="brightness which should be set. If bool ON will be sent for True and OFF for False")
+ timeout: float = pydantic.Field(..., description="Timeout / max time in seconds until switch off")
+
+ def __init__(self, brightness: int | bool, timeout: float) -> None:
+ """Initialize BrightnessTimeout without kwargs.
- def __init__(self, brightness: int | bool, timeout: float):
- """Initialize BrightnessTimeout without kwargs
+ Args:
+ brightness: brightness value
+ timeout: timeout value
+ """
+ super().__init__(brightness=brightness, timeout=timeout)
- :param brightness: brightness value
- :param timeout: timeout value
- """
- super().__init__(brightness=brightness, timeout=timeout)
+ @pydantic.model_validator(mode="after")
+ def validata_model(self) -> typing_extensions.Self:
+ """Validate brightness and timeout.
- @pydantic.model_validator(mode="after")
- def validata_model(self) -> typing.Self:
- """Validate brightness and timeout
+ Returns:
+ self
- :raises AssertionError: if brightness and timeout are not valid
- :return: self
- """
- if self.brightness is False or self.brightness == 0:
- # Default if the light should be switched off e.g. for leaving / sleeping
- if not self.timeout:
- self.timeout = 0.5
+ Raises:
+ AssertionError: if brightness and timeout are not valid
+ """
+ if self.brightness is False or self.brightness == 0: # noqa: SIM102
+ # Default if the light should be switched off e.g. for leaving / sleeping
+ if not self.timeout:
+ self.timeout = 0.5
- if not self.timeout:
- raise AssertionError(f"Brightness and timeout are not valid: brightness = {self.brightness} | timeout = {self.timeout}")
- return self
+ if not self.timeout:
+ msg = f"Brightness and timeout are not valid: brightness = {self.brightness} | timeout = {self.timeout}"
+ raise AssertionError(msg)
+ return self
class FunctionConfig(pydantic.BaseModel):
- """Define brightness and timeout values for one function."""
- day: BrightnessTimeout | None = pydantic.Field(..., description="config for day. If None the light will not be switched on during the day")
- night: BrightnessTimeout | None = pydantic.Field(..., description="config for night. If None the light will not be switched on during the night")
- sleeping: BrightnessTimeout | None = pydantic.Field(..., description="config for sleeping. If None the light will not be switched on during sleeping")
+ """Define brightness and timeout values for one function."""
+
+ day: BrightnessTimeout | None = pydantic.Field(..., description="config for day. If None the light will not be switched on during the day")
+ night: BrightnessTimeout | None = pydantic.Field(..., description="config for night. If None the light will not be switched on during the night")
+ sleeping: BrightnessTimeout | None = pydantic.Field(..., description="config for sleeping. If None the light will not be switched on during sleeping")
class LightParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for all light rules.
-
- For all parameter which have the following type: FunctionConfig | None -> If None this state will be disabled for the rule
- """
- on: FunctionConfig = pydantic.Field(FunctionConfig(day=BrightnessTimeout(True, 14 * 3600), night=BrightnessTimeout(80, 10 * 3600), sleeping=BrightnessTimeout(20, 3 * 3600)),
- description="values which are used if the light is switched on manually")
- pre_off: FunctionConfig | None = pydantic.Field(FunctionConfig(day=BrightnessTimeout(50, 10), night=BrightnessTimeout(40, 7), sleeping=BrightnessTimeout(10, 7)), description="values which are used if the light changes pre_off state")
- leaving: FunctionConfig | None = pydantic.Field(FunctionConfig(day=BrightnessTimeout(False, 0), night=BrightnessTimeout(False, 0), sleeping=BrightnessTimeout(False, 0)), description="values which are used if the light changes to leaving state")
- pre_sleep: FunctionConfig | None = pydantic.Field(FunctionConfig(day=BrightnessTimeout(False, 10), night=BrightnessTimeout(False, 10), sleeping=None), description="values which are used if the light changes to pre_sleep state")
- pre_sleep_prevent: collections.abc.Callable[[], bool] | HABApp.openhab.items.OpenhabItem | None = pydantic.Field(None, description="Enable pre sleep prevent -> disable pre sleep if True")
- motion: FunctionConfig | None = pydantic.Field(None, description="values which are used if the light changes to motion state")
- door: FunctionConfig | None = pydantic.Field(None, description="values which are used if the light is enabled via a door opening")
- off_at_door_closed_during_leaving: bool = pydantic.Field(False, description="this can be used to switch lights off, when door is closed in leaving state")
- hand_off_lock_time: int = pydantic.Field(20, description="time in seconds where door / motion switch on is disabled after a manual OFF")
- leaving_only_if_on: bool = pydantic.Field(False, description="switch to leaving only if light is on. If False leaving light is always activated")
-
- @pydantic.field_validator("on", mode="after")
- @classmethod
- def validate_on(cls, value: FunctionConfig) -> FunctionConfig:
- """Validate config for on-state
-
- :param value: given value
- :return: validated value
- :raises AssertionError: if on is not valid
- """
- if any(conf is None for conf in [value.day, value.night, value.sleeping]):
- raise AssertionError("For function 'on' all brightness / timeout values must be set.")
- return value
-
- @pydantic.field_validator("pre_sleep", mode="after")
- @classmethod
- def validate_pre_sleep(cls, value: FunctionConfig | None) -> FunctionConfig | None:
- """Validate pre_sleep config
-
- :param value: value of pre sleep
- :raises AssertionError: if pre_sleep is not valid
- :return: validated value
- """
- if value is None:
- return value
-
- if value.sleeping is not None:
- LOGGER.warning("It's not allowed to set brightness / timeout for pre_sleep.sleeping. Set it to None")
- value.sleeping = None
-
- return value
+ """Parameter for all light rules.
+
+ For all parameter which have the following type: FunctionConfig | None -> If None this state will be disabled for the rule
+ """
+
+ on: FunctionConfig = pydantic.Field(
+ FunctionConfig(day=BrightnessTimeout(brightness=True, timeout=14 * 3600), night=BrightnessTimeout(80, 10 * 3600), sleeping=BrightnessTimeout(20, 3 * 3600)), description="values which are used if the light is switched on manually"
+ )
+ pre_off: FunctionConfig | None = pydantic.Field(FunctionConfig(day=BrightnessTimeout(50, 10), night=BrightnessTimeout(40, 7), sleeping=BrightnessTimeout(10, 7)), description="values which are used if the light changes pre_off state")
+ leaving: FunctionConfig | None = pydantic.Field(
+ FunctionConfig(day=BrightnessTimeout(brightness=False, timeout=0), night=BrightnessTimeout(brightness=False, timeout=0), sleeping=BrightnessTimeout(brightness=False, timeout=0)),
+ description="values which are used if the light changes to leaving state",
+ )
+ pre_sleep: FunctionConfig | None = pydantic.Field(
+ FunctionConfig(day=BrightnessTimeout(brightness=False, timeout=10), night=BrightnessTimeout(brightness=False, timeout=10), sleeping=None), description="values which are used if the light changes to pre_sleep state"
+ )
+ pre_sleep_prevent: collections.abc.Callable[[], bool] | HABApp.openhab.items.OpenhabItem | None = pydantic.Field(None, description="Enable pre sleep prevent -> disable pre sleep if True")
+ motion: FunctionConfig | None = pydantic.Field(None, description="values which are used if the light changes to motion state")
+ door: FunctionConfig | None = pydantic.Field(None, description="values which are used if the light is enabled via a door opening")
+ off_at_door_closed_during_leaving: bool = pydantic.Field(default=False, description="this can be used to switch lights off, when door is closed in leaving state")
+ hand_off_lock_time: int = pydantic.Field(20, description="time in seconds where door / motion switch on is disabled after a manual OFF")
+ leaving_only_if_on: bool = pydantic.Field(default=False, description="switch to leaving only if light is on. If False leaving light is always activated")
+
+ @pydantic.field_validator("on", mode="after")
+ @classmethod
+ def validate_on(cls, value: FunctionConfig) -> FunctionConfig:
+ """Validate config for on-state.
+
+ Args:
+ value: given value
+
+ Returns:
+ validated value
+
+ Raises:
+ AssertionError: if on is not valid
+ """
+ if any(conf is None for conf in [value.day, value.night, value.sleeping]):
+ msg = "For function 'on' all brightness / timeout values must be set."
+ raise AssertionError(msg)
+ return value
+
+ @pydantic.field_validator("pre_sleep", mode="after")
+ @classmethod
+ def validate_pre_sleep(cls, value: FunctionConfig | None) -> FunctionConfig | None:
+ """Validate pre_sleep config.
+
+ Args:
+ value: value of pre sleep
+
+ Returns:
+ validated value
+
+ Raises:
+ AssertionError: if pre_sleep is not valid
+ """
+ if value is None:
+ return value
+
+ if value.sleeping is not None:
+ LOGGER.warning("It's not allowed to set brightness / timeout for pre_sleep.sleeping. Set it to None")
+ value.sleeping = None
+
+ return value
class LightConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for all light rules."""
- items: LightItems = pydantic.Field(..., description="items for all light rules")
- parameter: LightParameter = pydantic.Field(LightParameter(), description="parameter for all light rules")
+ """Config for all light rules."""
+
+ items: LightItems = pydantic.Field(..., description="items for all light rules")
+ parameter: LightParameter = pydantic.Field(LightParameter(), description="parameter for all light rules")
+
+ @pydantic.model_validator(mode="after")
+ def validate_config(self) -> typing_extensions.Self:
+ """Validate config.
- @pydantic.model_validator(mode="after")
- def validate_config(self) -> typing.Self:
- """Validate config
+ Returns:
+ validated config
- :raises AssertionError: if config is not valid
- :return: config if valid
- """
- if self.items.motion is not None and self.parameter.motion is None:
- raise AssertionError("item motion is given, but not configured via parameter")
+ Raises:
+ AssertionError: if config is not valid
+ """
+ if self.items.motion is not None and self.parameter.motion is None:
+ msg = "item motion is given, but not configured via parameter"
+ raise AssertionError(msg)
- if len(self.items.doors) and self.parameter.door is None:
- raise AssertionError("item door is given, but not configured via parameter")
+ if len(self.items.doors) and self.parameter.door is None:
+ msg = "item door is given, but not configured via parameter"
+ raise AssertionError(msg)
- if self.items.sleeping_state is not None and self.parameter.pre_sleep is None:
- raise AssertionError("item sleeping_state is given, but not configured via parameter")
+ if self.items.sleeping_state is not None and self.parameter.pre_sleep is None:
+ msg = "item sleeping_state is given, but not configured via parameter"
+ raise AssertionError(msg)
- if self.items.presence_state is not None and self.parameter.leaving is None:
- raise AssertionError("item presence_state is given, but not configured via parameter")
+ if self.items.presence_state is not None and self.parameter.leaving is None:
+ msg = "item presence_state is given, but not configured via parameter"
+ raise AssertionError(msg)
- if self.items.pre_sleep_prevent is not None and self.parameter.pre_sleep_prevent is not None:
- LOGGER.warning("item pre_sleep_prevent and parameter pre_sleep_prevent are given. The item will be prioritized and the parameter will be ignored!")
+ if self.items.pre_sleep_prevent is not None and self.parameter.pre_sleep_prevent is not None:
+ LOGGER.warning("item pre_sleep_prevent and parameter pre_sleep_prevent are given. The item will be prioritized and the parameter will be ignored!")
- return self
+ return self
diff --git a/habapp_rules/actors/config/light_hcl.py b/habapp_rules/actors/config/light_hcl.py
index 129933f..9036315 100644
--- a/habapp_rules/actors/config/light_hcl.py
+++ b/habapp_rules/actors/config/light_hcl.py
@@ -1,72 +1,70 @@
"""Config models for HCL color rules."""
-import typing
+
+import operator
import HABApp
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class HclTimeItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for HCL color which depends on time"""
- color: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="HCL color which will be set by the HCL rule")
- manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item to disable all automatic functions")
- sleep_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleep state item")
- focus: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="focus state item")
- switch_on: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="switch item which triggers a color update if switched on")
- state: HABApp.openhab.items.StringItem | None = pydantic.Field(..., description="state item for storing the current state")
+ """Items for HCL color which depends on time."""
+
+ color: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="HCL color which will be set by the HCL rule")
+ manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item to disable all automatic functions")
+ sleep_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleep state item")
+ focus: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="focus state item")
+ switch_on: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="switch item which triggers a color update if switched on")
+ state: HABApp.openhab.items.StringItem | None = pydantic.Field(..., description="state item for storing the current state")
class HclElevationItems(HclTimeItems):
- """Items for HCL color which depends on sun elevation"""
- elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun elevation")
+ """Items for HCL color which depends on sun elevation."""
+
+ elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun elevation")
class HclElevationParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for HCL color"""
- color_map: list[tuple[int, int]] = pydantic.Field([(-15, 3900), (0, 4500), (5, 5500), (15, 6500)], description="Color mapping. The first value is the sun elevation, the second is the HCL color")
- hand_timeout: int = pydantic.Field(18_000, description="hand timeout. After this time the HCL light rule will fall back to auto mode", gt=0) # 5 hours
- sleep_color: float = pydantic.Field(2500, description="color if sleeping is active", gt=0)
- post_sleep_timeout: int = pydantic.Field(1, description="time after sleeping was active where the sleeping color will be set", gt=0)
- focus_color: float = pydantic.Field(6000, description="color if focus is active", gt=0)
-
- @pydantic.model_validator(mode="after")
- def validate_model(self) -> typing.Self:
- """Sort color map
-
- :return: model with sorted color map
- """
- self.color_map = sorted(self.color_map, key=lambda x: x[0])
- return self
-
-
-_DEFAULT_TIME_MAP = [
- (0, 2200),
- (4, 2200),
- (5, 3200),
- (6, 3940),
- (8, 5000),
- (12, 7000),
- (19, 7000),
- (21, 5450),
- (22, 4000),
- (23, 2600)
-]
+ """Parameter for HCL color."""
+
+ color_map: list[tuple[int, int]] = pydantic.Field([(-15, 3900), (0, 4500), (5, 5500), (15, 6500)], description="Color mapping. The first value is the sun elevation, the second is the HCL color")
+ hand_timeout: int = pydantic.Field(18_000, description="hand timeout. After this time the HCL light rule will fall back to auto mode", gt=0) # 5 hours
+ sleep_color: float = pydantic.Field(2500, description="color if sleeping is active", gt=0)
+ post_sleep_timeout: int = pydantic.Field(1, description="time after sleeping was active where the sleeping color will be set", gt=0)
+ focus_color: float = pydantic.Field(6000, description="color if focus is active", gt=0)
+
+ @pydantic.model_validator(mode="after")
+ def validate_model(self) -> typing_extensions.Self:
+ """Sort color map.
+
+ Returns:
+ model with sorted color map
+ """
+ self.color_map = sorted(self.color_map, key=operator.itemgetter(0))
+ return self
+
+
+_DEFAULT_TIME_MAP = [(0, 2200), (4, 2200), (5, 3200), (6, 3940), (8, 5000), (12, 7000), (19, 7000), (21, 5450), (22, 4000), (23, 2600)]
class HclTimeParameter(HclElevationParameter):
- """Parameter for HCL color which depends on time"""
- color_map: list[tuple[int, int]] = pydantic.Field(_DEFAULT_TIME_MAP, description="Color mapping. The first value is the hour, the second is the HCL color")
- shift_weekend_holiday: bool = pydantic.Field(False, description="If this is active the color will shift on weekends and holidays for one hour")
+ """Parameter for HCL color which depends on time."""
+
+ color_map: list[tuple[int, int]] = pydantic.Field(_DEFAULT_TIME_MAP, description="Color mapping. The first value is the hour, the second is the HCL color")
+ shift_weekend_holiday: bool = pydantic.Field(default=False, description="If this is active the color will shift on weekends and holidays for one hour")
class HclElevationConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for HCL color which depends on sun elevation"""
- items: HclElevationItems = pydantic.Field(..., description="items for HCL color which depends on sun elevation")
- parameter: HclElevationParameter = pydantic.Field(HclElevationParameter(), description="parameter for HCL color which depends on sun elevation")
+ """Config for HCL color which depends on sun elevation."""
+
+ items: HclElevationItems = pydantic.Field(..., description="items for HCL color which depends on sun elevation")
+ parameter: HclElevationParameter = pydantic.Field(HclElevationParameter(), description="parameter for HCL color which depends on sun elevation")
class HclTimeConfig(HclElevationConfig):
- """Config for HCL color which depends on time"""
- items: HclTimeItems = pydantic.Field(..., description="items for HCL color which depends on time")
- parameter: HclTimeParameter = pydantic.Field(HclTimeParameter(), description="parameter for HCL color which depends on time")
+ """Config for HCL color which depends on time."""
+
+ items: HclTimeItems = pydantic.Field(..., description="items for HCL color which depends on time")
+ parameter: HclTimeParameter = pydantic.Field(HclTimeParameter(), description="parameter for HCL color which depends on time")
diff --git a/habapp_rules/actors/config/power.py b/habapp_rules/actors/config/power.py
deleted file mode 100644
index 7786794..0000000
--- a/habapp_rules/actors/config/power.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""Deprecated"""
-import warnings # pragma: no cover
-
-from habapp_rules.sensors.config.current_switch import CurrentSwitchConfig, CurrentSwitchItems, CurrentSwitchParameter # noqa: F401 pylint: disable=unused-import # pragma: no cover
-
-warnings.warn(
- "CurrentSwitch config has been moved to 'habapp_rules.sensors.config.current_switch.py'. Importing it from 'habapp_rules.actors.config.power.py' is deprecated and will be removed in a future release.",
- DeprecationWarning,
- stacklevel=2
-) # pragma: no cover
diff --git a/habapp_rules/actors/config/shading.py b/habapp_rules/actors/config/shading.py
index d71ea29..6c0c10f 100644
--- a/habapp_rules/actors/config/shading.py
+++ b/habapp_rules/actors/config/shading.py
@@ -1,160 +1,179 @@
"""Config models for shading rules."""
+
from __future__ import annotations
import copy
-import typing
-import HABApp.openhab.items
+import HABApp.openhab.items # noqa: TCH002
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class ShadingPosition(pydantic.BaseModel):
- """Position of shading object"""
- position: float | bool | None = pydantic.Field(..., description="target position")
- slat: float | None = pydantic.Field(None, description="target slat position")
+ """Position of shading object."""
+
+ position: float | bool | None = pydantic.Field(..., description="target position")
+ slat: float | None = pydantic.Field(None, description="target slat position")
- def __init__(self, position: float | bool | None, slat: float | None = None) -> None:
- """Initialize shading position with position and slat
+ def __init__(self, position: float | bool | None, slat: float | None = None) -> None:
+ """Initialize shading position with position and slat.
- :param position: target position value
- :param slat: slat value
- """
- super().__init__(position=position, slat=slat)
+ Args:
+ position: target position value
+ slat: slat value
+ """
+ super().__init__(position=position, slat=slat)
class ShadingItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for shading rules."""
- shading_position: HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="item for setting the shading position")
- slat: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="item for setting the slat value")
- manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to switch to manual mode and disable the automatic functions")
- shading_position_control: list[HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="control items to improve manual detection")
- shading_position_group: list[HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="")
- wind_alarm: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON when wind alarm is active")
- sun_protection: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON when sun protection is needed")
- sun_protection_slat: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="value for the slat when sun protection is active")
- sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="item of the sleeping state set via habapp_rules.system.sleep.Sleep")
- night: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON at night or darkness")
- door: HABApp.openhab.items.ContactItem | None = pydantic.Field(None, description="item for setting position when door is opened")
- summer: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON during summer")
- hand_manual_is_active_feedback: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="feedback item which is ON when hand or manual is active")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
+ """Items for shading rules."""
+
+ shading_position: HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="item for setting the shading position")
+ slat: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="item for setting the slat value")
+ manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to switch to manual mode and disable the automatic functions")
+ shading_position_control: list[HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="control items to improve manual detection")
+ shading_position_group: list[HABApp.openhab.items.RollershutterItem | HABApp.openhab.items.DimmerItem] = pydantic.Field([], description="")
+ wind_alarm: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON when wind alarm is active")
+ sun_protection: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON when sun protection is needed")
+ sun_protection_slat: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="value for the slat when sun protection is active")
+ sleeping_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="item of the sleeping state set via habapp_rules.system.sleep.Sleep")
+ night: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON at night or darkness")
+ door: HABApp.openhab.items.ContactItem | None = pydantic.Field(None, description="item for setting position when door is opened")
+ summer: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON during summer")
+ hand_manual_is_active_feedback: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="feedback item which is ON when hand or manual is active")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")
class ShadingParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for shading rules."""
- pos_auto_open: ShadingPosition = pydantic.Field(ShadingPosition(0, 0), description="position for auto open")
- pos_wind_alarm: ShadingPosition | None = pydantic.Field(ShadingPosition(0, 0), description="position for wind alarm")
- pos_sleeping_night: ShadingPosition | None = pydantic.Field(ShadingPosition(100, 100), description="position for sleeping at night")
- pos_sleeping_day: ShadingPosition | None = pydantic.Field(None, description="position for sleeping at day")
- pos_sun_protection: ShadingPosition | None = pydantic.Field(ShadingPosition(100, None), description="position for sun protection")
- pos_night_close_summer: ShadingPosition | None = pydantic.Field(None, description="position for night close during summer")
- pos_night_close_winter: ShadingPosition | None = pydantic.Field(ShadingPosition(100, 100), description="position for night close during winter")
- pos_door_open: ShadingPosition | None = pydantic.Field(ShadingPosition(0, 0), description="position if door is opened")
- manual_timeout: int = pydantic.Field(24 * 3600, description="fallback timeout for manual state", gt=0)
- door_post_time: int = pydantic.Field(5 * 60, description="extended time after door is closed", gt=0)
- value_tolerance: int = pydantic.Field(0, description="value tolerance for shading position which is allowed without manual detection", ge=0)
-
- @pydantic.model_validator(mode="after")
- def validate_model(self) -> typing.Self:
- """Validate model
-
- :return: validated model
- """
- if self.pos_sleeping_night and not self.pos_sleeping_day:
- self.pos_sleeping_day = copy.deepcopy(self.pos_sleeping_night)
- return self
+ """Parameter for shading rules."""
+
+ pos_auto_open: ShadingPosition = pydantic.Field(ShadingPosition(0, 0), description="position for auto open")
+ pos_wind_alarm: ShadingPosition | None = pydantic.Field(ShadingPosition(0, 0), description="position for wind alarm")
+ pos_sleeping_night: ShadingPosition | None = pydantic.Field(ShadingPosition(100, 100), description="position for sleeping at night")
+ pos_sleeping_day: ShadingPosition | None = pydantic.Field(None, description="position for sleeping at day")
+ pos_sun_protection: ShadingPosition | None = pydantic.Field(ShadingPosition(100, None), description="position for sun protection")
+ pos_night_close_summer: ShadingPosition | None = pydantic.Field(None, description="position for night close during summer")
+ pos_night_close_winter: ShadingPosition | None = pydantic.Field(ShadingPosition(100, 100), description="position for night close during winter")
+ pos_door_open: ShadingPosition | None = pydantic.Field(ShadingPosition(0, 0), description="position if door is opened")
+ manual_timeout: int = pydantic.Field(24 * 3600, description="fallback timeout for manual state", gt=0)
+ door_post_time: int = pydantic.Field(5 * 60, description="extended time after door is closed", gt=0)
+ value_tolerance: int = pydantic.Field(0, description="value tolerance for shading position which is allowed without manual detection", ge=0)
+
+ @pydantic.model_validator(mode="after")
+ def validate_model(self) -> typing_extensions.Self:
+ """Validate model.
+
+ Returns:
+ validated model
+ """
+ if self.pos_sleeping_night and not self.pos_sleeping_day:
+ self.pos_sleeping_day = copy.deepcopy(self.pos_sleeping_night)
+ return self
class ShadingConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for shading objects."""
- items: ShadingItems = pydantic.Field(..., description="items for shading")
- parameter: ShadingParameter = pydantic.Field(ShadingParameter(), description="parameter for shading")
+ """Config for shading objects."""
+
+ items: ShadingItems = pydantic.Field(..., description="items for shading")
+ parameter: ShadingParameter = pydantic.Field(ShadingParameter(), description="parameter for shading")
- @pydantic.model_validator(mode="after")
- def validate_model(self) -> typing.Self:
- """Validate model
+ @pydantic.model_validator(mode="after")
+ def validate_model(self) -> typing_extensions.Self:
+ """Validate model.
- :return: validated model
- :raises AssertionError: if 'parameter.pos_night_close_summer' is set but 'items.summer' is missing
- """
- if self.parameter.pos_night_close_summer is not None and self.items.summer is None:
- raise AssertionError("Night close position is set for summer, but item for summer / winter is missing!")
- return self
+ Returns:
+ validated model
+
+ Raises:
+ AssertionError: if 'parameter.pos_night_close_summer' is set but 'items.summer' is missing
+ """
+ if self.parameter.pos_night_close_summer is not None and self.items.summer is None:
+ msg = "Night close position is set for summer, but item for summer / winter is missing!"
+ raise AssertionError(msg)
+ return self
class ResetAllManualHandItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for reset all manual hand items."""
- reset_manual_hand: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item for resetting manual and hand state to automatic state")
+ """Items for reset all manual hand items."""
+
+ reset_manual_hand: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item for resetting manual and hand state to automatic state")
class ResetAllManualHandParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for reset all manual hand parameter."""
- shading_objects: list[object] | None = pydantic.Field(None, description="list of shading objects to reset")
+ """Parameter for reset all manual hand parameter."""
+
+ shading_objects: list[object] | None = pydantic.Field(None, description="list of shading objects to reset")
class ResetAllManualHandConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for reset all manual hand config."""
- items: ResetAllManualHandItems = pydantic.Field(..., description="items for reset all manual hand config")
- parameter: ResetAllManualHandParameter = pydantic.Field(ResetAllManualHandParameter(), description="parameter for reset all manual hand config")
+ """Config for reset all manual hand config."""
+
+ items: ResetAllManualHandItems = pydantic.Field(..., description="items for reset all manual hand config")
+ parameter: ResetAllManualHandParameter = pydantic.Field(ResetAllManualHandParameter(), description="parameter for reset all manual hand config")
class SlatValueItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for slat values for sun protection."""
- sun_elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item for sun elevation")
- slat_value: HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="item for slat value, which should be set")
- summer: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item for summer")
+ """Items for slat values for sun protection."""
+
+ sun_elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item for sun elevation")
+ slat_value: HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="item for slat value, which should be set")
+ summer: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item for summer")
class ElevationSlatMapping(pydantic.BaseModel):
- """Mapping from elevation to slat value."""
- elevation: int
- slat_value: int
+ """Mapping from elevation to slat value."""
+
+ elevation: int
+ slat_value: int
- def __init__(self, elevation: int, slat_value: int):
- """Initialize the elevation slat mapping.
+ def __init__(self, elevation: int, slat_value: int) -> None:
+ """Initialize the elevation slat mapping.
- :param elevation: elevation value
- :param slat_value: mapped slat value
- """
- super().__init__(elevation=elevation, slat_value=slat_value)
+ Args:
+ elevation: elevation value
+ slat_value: mapped slat value
+ """
+ super().__init__(elevation=elevation, slat_value=slat_value)
class SlatValueParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for slat values for sun protection."""
- elevation_slat_characteristic: list[ElevationSlatMapping] = pydantic.Field([
- ElevationSlatMapping(0, 100),
- ElevationSlatMapping(4, 100),
- ElevationSlatMapping(8, 90),
- ElevationSlatMapping(18, 80),
- ElevationSlatMapping(26, 70),
- ElevationSlatMapping(34, 60),
- ElevationSlatMapping(41, 50)], description="list of tuple-mappings from elevation to slat value")
- elevation_slat_characteristic_summer: list[ElevationSlatMapping] = pydantic.Field([
- ElevationSlatMapping(0, 100),
- ElevationSlatMapping(4, 100),
- ElevationSlatMapping(8, 90),
- ElevationSlatMapping(18, 80)], description="list of tuple-mappings from elevation to slat value, which is used if summer is active")
-
- @pydantic.field_validator("elevation_slat_characteristic", "elevation_slat_characteristic_summer")
- @classmethod
- def sort_mapping(cls, values: list[ElevationSlatMapping]) -> list[ElevationSlatMapping]:
- """Sort the elevation slat mappings.
-
- :param values: input values
- :return: sorted values
- :raises AssertionError: if elevation values are not unique
- """
- values.sort(key=lambda x: x.elevation)
-
- if len(values) != len(set(value.elevation for value in values)):
- raise AssertionError("Elevation values must be unique!")
-
- return values
+ """Parameter for slat values for sun protection."""
+
+ elevation_slat_characteristic: list[ElevationSlatMapping] = pydantic.Field(
+ [ElevationSlatMapping(0, 100), ElevationSlatMapping(4, 100), ElevationSlatMapping(8, 90), ElevationSlatMapping(18, 80), ElevationSlatMapping(26, 70), ElevationSlatMapping(34, 60), ElevationSlatMapping(41, 50)],
+ description="list of tuple-mappings from elevation to slat value",
+ )
+ elevation_slat_characteristic_summer: list[ElevationSlatMapping] = pydantic.Field(
+ [ElevationSlatMapping(0, 100), ElevationSlatMapping(4, 100), ElevationSlatMapping(8, 90), ElevationSlatMapping(18, 80)], description="list of tuple-mappings from elevation to slat value, which is used if summer is active"
+ )
+
+ @pydantic.field_validator("elevation_slat_characteristic", "elevation_slat_characteristic_summer")
+ @classmethod
+ def sort_mapping(cls, values: list[ElevationSlatMapping]) -> list[ElevationSlatMapping]:
+ """Sort the elevation slat mappings.
+
+ Args:
+ values: input values
+
+ Returns:
+ sorted values
+
+ Raises:
+ AssertionError: if elevation values are not unique
+ """
+ values.sort(key=lambda x: x.elevation)
+
+ if len(values) != len({value.elevation for value in values}):
+ msg = "Elevation values must be unique!"
+ raise AssertionError(msg)
+
+ return values
class SlatValueConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for slat values for sun protection."""
- items: SlatValueItems = pydantic.Field(..., description="items for slat values for sun protection")
- parameter: SlatValueParameter = pydantic.Field(SlatValueParameter(), description="parameter for slat values for sun protection")
+ """Config for slat values for sun protection."""
+
+ items: SlatValueItems = pydantic.Field(..., description="items for slat values for sun protection")
+ parameter: SlatValueParameter = pydantic.Field(SlatValueParameter(), description="parameter for slat values for sun protection")
diff --git a/habapp_rules/actors/config/ventilation.py b/habapp_rules/actors/config/ventilation.py
index 66184ad..14654c3 100644
--- a/habapp_rules/actors/config/ventilation.py
+++ b/habapp_rules/actors/config/ventilation.py
@@ -1,4 +1,5 @@
"""Config models for ventilation rules."""
+
import datetime
import HABApp
@@ -8,69 +9,79 @@
class StateConfig(pydantic.BaseModel):
- """Basic state config."""
- level: int
- display_text: str
+ """Basic state config."""
+
+ level: int
+ display_text: str
class StateConfigWithTimeout(StateConfig):
- """State config with timeout."""
- timeout: int
+ """State config with timeout."""
+
+ timeout: int
class StateConfigLongAbsence(StateConfig):
- """State config for long absence state."""
- duration: int = 3600
- start_time: datetime.time = datetime.time(6)
+ """State config for long absence state."""
+
+ duration: int = 3600
+ start_time: datetime.time = datetime.time(6)
class _VentilationItemsBase(habapp_rules.core.pydantic_base.ItemBase):
- """Base class for ventilation items."""
- manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to disable all automatic functions")
- hand_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item to enter the hand state")
- external_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item to enter the external state")
- presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item of presence state to detect long absence")
- feedback_on: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item which shows that ventilation is on")
- feedback_power: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item which shows that ventilation is in power mode")
- display_text: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item which can be used to set the display text")
- state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item for storing the current state")
+ """Base class for ventilation items."""
+
+ manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to disable all automatic functions")
+ hand_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item to enter the hand state")
+ external_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item to enter the external state")
+ presence_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item of presence state to detect long absence")
+ feedback_on: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item which shows that ventilation is on")
+ feedback_power: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="Item which shows that ventilation is in power mode")
+ display_text: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item which can be used to set the display text")
+ state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="Item for storing the current state")
class VentilationItems(_VentilationItemsBase):
- """Items for ventilation."""
- ventilation_level: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item to set the ventilation level")
+ """Items for ventilation."""
+
+ ventilation_level: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item to set the ventilation level")
class VentilationTwoStageItems(_VentilationItemsBase):
- """Items for ventilation."""
- ventilation_output_on: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to switch on the ventilation")
- ventilation_output_power: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to switch on the power mode")
- current: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="Item to measure the current of the ventilation")
+ """Items for ventilation."""
+
+ ventilation_output_on: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to switch on the ventilation")
+ ventilation_output_power: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item to switch on the power mode")
+ current: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="Item to measure the current of the ventilation")
class VentilationParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for ventilation."""
- state_normal: StateConfig = pydantic.Field(StateConfig(level=1, display_text="Normal"))
- state_hand: StateConfigWithTimeout = pydantic.Field(StateConfigWithTimeout(level=2, display_text="Hand", timeout=3600))
- state_external: StateConfig = pydantic.Field(StateConfig(level=2, display_text="External"))
- state_humidity: StateConfig = pydantic.Field(StateConfig(level=2, display_text="Humidity"))
- state_long_absence: StateConfigLongAbsence = pydantic.Field(StateConfigLongAbsence(level=2, display_text="LongAbsence"))
+ """Parameter for ventilation."""
+
+ state_normal: StateConfig = pydantic.Field(StateConfig(level=1, display_text="Normal"))
+ state_hand: StateConfigWithTimeout = pydantic.Field(StateConfigWithTimeout(level=2, display_text="Hand", timeout=3600))
+ state_external: StateConfig = pydantic.Field(StateConfig(level=2, display_text="External"))
+ state_humidity: StateConfig = pydantic.Field(StateConfig(level=2, display_text="Humidity"))
+ state_long_absence: StateConfigLongAbsence = pydantic.Field(StateConfigLongAbsence(level=2, display_text="LongAbsence"))
class VentilationTwoStageParameter(VentilationParameter):
- """Parameter for ventilation."""
- state_after_run: StateConfig = pydantic.Field(StateConfig(level=2, display_text="After run"))
- after_run_timeout: int = pydantic.Field(390, description="")
- current_threshold_power: float = pydantic.Field(0.105, description="")
+ """Parameter for ventilation."""
+
+ state_after_run: StateConfig = pydantic.Field(StateConfig(level=2, display_text="After run"))
+ after_run_timeout: int = pydantic.Field(390, description="")
+ current_threshold_power: float = pydantic.Field(0.105, description="")
class VentilationConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for ventilation."""
- items: VentilationItems = pydantic.Field(..., description="Items for ventilation")
- parameter: VentilationParameter = pydantic.Field(VentilationParameter(), description="Parameter for ventilation")
+ """Config for ventilation."""
+
+ items: VentilationItems = pydantic.Field(..., description="Items for ventilation")
+ parameter: VentilationParameter = pydantic.Field(VentilationParameter(), description="Parameter for ventilation")
class VentilationTwoStageConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for ventilation."""
- items: VentilationTwoStageItems = pydantic.Field(..., description="Items for ventilation")
- parameter: VentilationTwoStageParameter = pydantic.Field(VentilationTwoStageParameter(), description="Parameter for ventilation")
+ """Config for ventilation."""
+
+ items: VentilationTwoStageItems = pydantic.Field(..., description="Items for ventilation")
+ parameter: VentilationTwoStageParameter = pydantic.Field(VentilationTwoStageParameter(), description="Parameter for ventilation")
diff --git a/habapp_rules/actors/energy_save_switch.py b/habapp_rules/actors/energy_save_switch.py
index 68d68b3..ef2ade9 100644
--- a/habapp_rules/actors/energy_save_switch.py
+++ b/habapp_rules/actors/energy_save_switch.py
@@ -1,5 +1,7 @@
"""Energy save switch rules."""
+
import logging
+import typing
import HABApp
@@ -12,213 +14,217 @@
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member, invalid-name
class EnergySaveSwitch(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Rules class to manage energy save switches.
-
- # Items:
- Switch Switch "Switch" {channel="..."}
- Switch Switch_State "State"
-
- # Config:
- config = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
- items=EnergySaveSwitchItems(
- switch="Switch",
- state="Switch_State"
- )
- ))
-
- # Rule init:
- habapp_rules.actors.energy_save_switch.EnergySaveSwitch(config)
- """
-
- states = [
- {"name": "Manual"},
- {"name": "Hand", "timeout": 0, "on_timeout": ["_auto_hand_timeout"]},
-
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "On"},
- {"name": "Off"},
- {"name": "WaitCurrent"},
- ]}
- ]
-
- trans = [
- {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
- {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
-
- {"trigger": "hand_detected", "source": "Auto", "dest": "Hand"},
- {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
-
- {"trigger": "on_conditions_met", "source": ["Auto_Off", "Auto_WaitCurrent"], "dest": "Auto_On"},
- {"trigger": "off_conditions_met", "source": "Auto_On", "dest": "Auto_Off", "unless": "_current_above_threshold"},
- {"trigger": "off_conditions_met", "source": "Auto_On", "dest": "Auto_WaitCurrent", "conditions": "_current_above_threshold"},
- {"trigger": "current_below_threshold", "source": "Auto_WaitCurrent", "dest": "Auto_Off"},
-
- {"trigger": "max_on_countdown", "source": ["Auto_On", "Auto_WaitCurrent", "Hand"], "dest": "Auto_Off"},
- ]
-
- def __init__(self, config: habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig) -> None:
- """Init of energy save switch.
-
- :param config: energy save switch config
- """
- self._config = config
- self._switch_observer = habapp_rules.actors.state_observer.StateObserverSwitch(config.items.switch.name, self._cb_hand, self._cb_hand)
-
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.switch.name)
-
- # init state machine
- self._previous_state = None
- self._restore_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
-
- self._max_on_countdown = self.run.countdown(self._config.parameter.max_on_time, self._cb_max_on_countdown) if self._config.parameter.max_on_time is not None else None
- self._set_timeouts()
- self._set_state(self._get_initial_state())
-
- # callbacks
- self._config.items.switch.listen_event(self._cb_switch, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- if self._config.items.manual is not None:
- self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.presence_state is not None:
- self._config.items.presence_state.listen_event(self._cb_presence_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.sleeping_state is not None:
- self._config.items.sleeping_state.listen_event(self._cb_sleeping_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.external_request is not None:
- self._config.items.external_request.listen_event(self._cb_external_request, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.current is not None:
- self._config.items.current.listen_event(self._cb_current_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- LOGGER.info(self.get_initial_log_message())
-
- def _set_timeouts(self) -> None:
- """Set timeouts."""
- self.state_machine.states["Hand"].timeout = self._config.parameter.hand_timeout if self._config.parameter.hand_timeout else 0
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.manual is not None and self._config.items.manual.is_on():
- return "Manual"
-
- if self._get_on_off_conditions_met():
- return "Auto_On"
- if self._current_above_threshold():
- return "Auto_WaitCurrent"
- return "Auto_Off"
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This should method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_switch_state()
- self._previous_state = self.state
-
- def on_enter_Auto_Init(self):
- """Callback, which is called on enter of init state"""
- if self._get_on_off_conditions_met():
- self.to_Auto_On()
- else:
- self.to_Auto_Off()
-
- def _set_switch_state(self) -> None:
- """Set switch state."""
- if self.state == "Auto_On":
- self._switch_observer.send_command("ON")
- elif self.state == "Auto_Off":
- self._switch_observer.send_command("OFF")
-
- def _current_above_threshold(self) -> bool:
- """Current is above threshold.
-
- :return: True if current is above threshold
- """
- if self._config.items.current is None or self._config.items.current.value is None:
- return False
-
- return self._config.items.current.value > self._config.parameter.current_threshold
-
- def _get_on_off_conditions_met(self) -> bool:
- """Check if on/off conditions are met.
-
- :return: True if switch should be ON, else False
- """
- external_req = self._config.items.external_request is not None and self._config.items.external_request.is_on()
- present = self._config.items.presence_state is not None and self._config.items.presence_state == PresenceState.PRESENCE.value
- awake = self._config.items.sleeping_state is not None and self._config.items.sleeping_state == SleepState.AWAKE.value
-
- return external_req or (present and awake)
-
- def _conditions_changed(self) -> None:
- """Sleep, presence or external state changed."""
- if self._get_on_off_conditions_met():
- self.on_conditions_met()
- else:
- self.off_conditions_met()
-
- def _cb_max_on_countdown(self) -> None:
- """Callback which is triggered if max on time is reached."""
- if self._max_on_countdown is not None:
- self.max_on_countdown()
-
- def _cb_switch(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if switch changed.
-
- :param event: event which triggered this callback
- """
- if self._max_on_countdown is not None:
- if event.value == "ON":
- self._max_on_countdown.reset()
- else:
- self._max_on_countdown.stop()
-
- def _cb_hand(self, event: HABApp.openhab.events.ItemStateChangedEvent):
- """Callback, which is triggered by the state observer if a manual change was detected
-
- :param event: event which triggered this callback.
- """
- self.hand_detected()
-
- def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the manual switch has a state change event.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.manual_on()
- else:
- self.manual_off()
-
- def _cb_presence_state(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if presence_state changed."""
- self._conditions_changed()
-
- def _cb_sleeping_state(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if sleeping state changed."""
- self._conditions_changed()
-
- def _cb_external_request(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if external request changed."""
- self._conditions_changed()
-
- def _cb_current_changed(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if the current value changed."""
- if self.state == "Auto_WaitCurrent" and not self._current_above_threshold():
- self.current_below_threshold()
+ """Rules class to manage energy save switches.
+
+ # Items:
+ Switch Switch "Switch" {channel="..."}
+ Switch Switch_State "State"
+
+ # Config:
+ config = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
+ items=EnergySaveSwitchItems(
+ switch="Switch",
+ state="Switch_State"
+ )
+ ))
+
+ # Rule init:
+ habapp_rules.actors.energy_save_switch.EnergySaveSwitch(config)
+ """
+
+ states: typing.ClassVar = [
+ {"name": "Manual"},
+ {"name": "Hand", "timeout": 0, "on_timeout": ["_auto_hand_timeout"]},
+ {
+ "name": "Auto",
+ "initial": "Init",
+ "children": [
+ {"name": "Init"},
+ {"name": "On"},
+ {"name": "Off"},
+ {"name": "WaitCurrent"},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
+ {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
+ {"trigger": "hand_detected", "source": "Auto", "dest": "Hand"},
+ {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
+ {"trigger": "on_conditions_met", "source": ["Auto_Off", "Auto_WaitCurrent"], "dest": "Auto_On"},
+ {"trigger": "off_conditions_met", "source": "Auto_On", "dest": "Auto_Off", "unless": "_current_above_threshold"},
+ {"trigger": "off_conditions_met", "source": "Auto_On", "dest": "Auto_WaitCurrent", "conditions": "_current_above_threshold"},
+ {"trigger": "current_below_threshold", "source": "Auto_WaitCurrent", "dest": "Auto_Off"},
+ {"trigger": "max_on_countdown", "source": ["Auto_On", "Auto_WaitCurrent", "Hand"], "dest": "Auto_Off"},
+ ]
+
+ def __init__(self, config: habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig) -> None:
+ """Init of energy save switch.
+
+ Args:
+ config: energy save switch config
+ """
+ self._config = config
+ self._switch_observer = habapp_rules.actors.state_observer.StateObserverSwitch(config.items.switch.name, self._cb_hand, self._cb_hand)
+
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.switch.name)
+
+ # init state machine
+ self._previous_state = None
+ self._restore_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+
+ self._max_on_countdown = self.run.countdown(self._config.parameter.max_on_time, self._cb_max_on_countdown) if self._config.parameter.max_on_time is not None else None
+ self._set_timeouts()
+ self._set_state(self._get_initial_state())
+
+ # callbacks
+ self._config.items.switch.listen_event(self._cb_switch, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ if self._config.items.manual is not None:
+ self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.presence_state is not None:
+ self._config.items.presence_state.listen_event(self._cb_presence_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.sleeping_state is not None:
+ self._config.items.sleeping_state.listen_event(self._cb_sleeping_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.external_request is not None:
+ self._config.items.external_request.listen_event(self._cb_external_request, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.current is not None:
+ self._config.items.current.listen_event(self._cb_current_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ LOGGER.info(self.get_initial_log_message())
+
+ def _set_timeouts(self) -> None:
+ """Set timeouts."""
+ self.state_machine.states["Hand"].timeout = self._config.parameter.hand_timeout or 0
+
+ def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+
+ """
+ if self._config.items.manual is not None and self._config.items.manual.is_on():
+ return "Manual"
+
+ if self._get_on_off_conditions_met():
+ return "Auto_On"
+ if self._current_above_threshold():
+ return "Auto_WaitCurrent"
+ return "Auto_Off"
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This should method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_switch_state()
+ self._previous_state = self.state
+
+ def on_enter_Auto_Init(self) -> None: # noqa: N802
+ """Callback, which is called on enter of init state."""
+ if self._get_on_off_conditions_met():
+ self.to_Auto_On()
+ else:
+ self.to_Auto_Off()
+
+ def _set_switch_state(self) -> None:
+ """Set switch state."""
+ if self.state == "Auto_On":
+ self._switch_observer.send_command("ON")
+ elif self.state == "Auto_Off":
+ self._switch_observer.send_command("OFF")
+
+ def _current_above_threshold(self) -> bool:
+ """Current is above threshold.
+
+ Returns:
+ True if current is above threshold
+ """
+ if self._config.items.current is None or self._config.items.current.value is None:
+ return False
+
+ return self._config.items.current.value > self._config.parameter.current_threshold
+
+ def _get_on_off_conditions_met(self) -> bool:
+ """Check if on/off conditions are met.
+
+ Returns:
+ True if switch should be ON, else False
+ """
+ external_req = self._config.items.external_request is not None and self._config.items.external_request.is_on()
+ present = self._config.items.presence_state is not None and self._config.items.presence_state == PresenceState.PRESENCE.value
+ awake = self._config.items.sleeping_state is not None and self._config.items.sleeping_state == SleepState.AWAKE.value
+
+ return external_req or (present and awake)
+
+ def _conditions_changed(self) -> None:
+ """Sleep, presence or external state changed."""
+ if self._get_on_off_conditions_met():
+ self.on_conditions_met()
+ else:
+ self.off_conditions_met()
+
+ def _cb_max_on_countdown(self) -> None:
+ """Callback which is triggered if max on time is reached."""
+ if self._max_on_countdown is not None:
+ self.max_on_countdown()
+
+ def _cb_switch(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if switch changed.
+
+ Args:
+ event: event which triggered this callback
+ """
+ if self._max_on_countdown is not None:
+ if event.value == "ON":
+ self._max_on_countdown.reset()
+ else:
+ self._max_on_countdown.stop()
+
+ def _cb_hand(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered by the state observer if a manual change was detected.
+
+ Args:
+ event: event which triggered this callback.
+ """
+ self.hand_detected()
+
+ def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the manual switch has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.manual_on()
+ else:
+ self.manual_off()
+
+ def _cb_presence_state(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if presence_state changed."""
+ self._conditions_changed()
+
+ def _cb_sleeping_state(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if sleeping state changed."""
+ self._conditions_changed()
+
+ def _cb_external_request(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if external request changed."""
+ self._conditions_changed()
+
+ def _cb_current_changed(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if the current value changed."""
+ if self.state == "Auto_WaitCurrent" and not self._current_above_threshold():
+ self.current_below_threshold()
diff --git a/habapp_rules/actors/heating.py b/habapp_rules/actors/heating.py
index 7ea64a0..45bf7d7 100644
--- a/habapp_rules/actors/heating.py
+++ b/habapp_rules/actors/heating.py
@@ -1,75 +1,79 @@
"""Heating rules."""
+
import HABApp
import habapp_rules.actors.config.heating
class KnxHeating(HABApp.Rule):
- """Rule which can be used to control a heating actor which only supports temperature offsets (e.g. MDT).
+ """Rule which can be used to control a heating actor which only supports temperature offsets (e.g. MDT).
- This rule uses a virtual temperature OpenHAB item for the target temperature. If this changes, the new offset is calculated and sent to the actor.
- If the actor feedback temperature changes (e.g. through mode change), the new target temperature is updated to the virtual temperature item.
+ This rule uses a virtual temperature OpenHAB item for the target temperature. If this changes, the new offset is calculated and sent to the actor.
+ If the actor feedback temperature changes (e.g. through mode change), the new target temperature is updated to the virtual temperature item.
- # KNX-things:
- Thing device heating_actor "KNX heating actor"{
+ # KNX-things:
+ Thing device heating_actor "KNX heating actor"{
Type number : target_temperature "Target Temperature" [ ga="9.001:<3/6/11"]
Type number : temperature_offset "Temperature Offset" [ ga="9.002:3/6/22" ]
}
- # Items:
- Number:Temperature target_temperature_OH "Target Temperature" ["Setpoint", "Temperature"] {unit="°C", stateDescription=""[pattern="%.1f %unit%", min=5, max=27, step=0.5]}
- Number:Temperature target_temperature_KNX "Target Temperature KNX" {channel="knx:device:bridge:heating_actor:target_temperature", unit="°C", stateDescription=""[pattern="%.1f %unit%"]}
- Number temperature_offset "Temperature Offset" {channel="knx:device:bridge:heating_actor:temperature_offset", stateDescription=""[pattern="%.1f °C", min=-5, max=5, step=0.5]}
-
- # Config:
- config = habapp_rules.actors.config.heating.KnxHeatingConfig(
- items=habapp_rules.actors.config.heating.KnxHeatingItems(
- virtual_temperature="target_temperature_OH",
- actor_feedback_temperature="target_temperature_KNX",
- temperature_offset="temperature_offset"
- ))
-
- # Rule init:
- habapp_rules.actors.heating.KnxHeating(config)
- """
-
- def __init__(self, config: habapp_rules.actors.config.heating.KnxHeatingConfig):
- """Init of basic light object.
-
- :param config: KNX heating config
- """
- HABApp.Rule.__init__(self)
- self._config = config
-
- self._temperature: float | None = config.items.actor_feedback_temperature.value
- if self._temperature is not None:
- config.items.virtual_temperature.oh_post_update(self._temperature)
-
- config.items.actor_feedback_temperature.listen_event(self._cb_actor_feedback_temperature_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
- config.items.virtual_temperature.listen_event(self._cb_virtual_temperature_command, HABApp.openhab.events.ItemCommandEventFilter())
-
- def _cb_actor_feedback_temperature_changed(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the actor feedback temperature changed.
-
- :param event: trigger event
- """
- self._config.items.virtual_temperature.oh_post_update(event.value)
- self._temperature = event.value
-
- def _cb_virtual_temperature_command(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered if the virtual temperature received a command.
-
- :param event: trigger event
- """
- if self._temperature is None:
- self._temperature = event.value
-
- if self._config.items.temperature_offset.value is None:
- self._config.items.temperature_offset.oh_send_command(0)
-
- # T_offset_new = T_target - T_base
- # T_base = T_old - T_offset_old
- # ==> T_offset_new = T_target - T_old + T_offset_old
- offset_new = event.value - self._temperature + self._config.items.temperature_offset.value
- self._config.items.temperature_offset.oh_send_command(offset_new)
- self._temperature = event.value
+ # Items:
+ Number:Temperature target_temperature_OH "Target Temperature" ["Setpoint", "Temperature"] {unit="°C", stateDescription=""[pattern="%.1f %unit%", min=5, max=27, step=0.5]}
+ Number:Temperature target_temperature_KNX "Target Temperature KNX" {channel="knx:device:bridge:heating_actor:target_temperature", unit="°C", stateDescription=""[pattern="%.1f %unit%"]}
+ Number temperature_offset "Temperature Offset" {channel="knx:device:bridge:heating_actor:temperature_offset", stateDescription=""[pattern="%.1f °C", min=-5, max=5, step=0.5]}
+
+ # Config:
+ config = habapp_rules.actors.config.heating.KnxHeatingConfig(
+ items=habapp_rules.actors.config.heating.KnxHeatingItems(
+ virtual_temperature="target_temperature_OH",
+ actor_feedback_temperature="target_temperature_KNX",
+ temperature_offset="temperature_offset"
+ ))
+
+ # Rule init:
+ habapp_rules.actors.heating.KnxHeating(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.heating.KnxHeatingConfig) -> None:
+ """Init of basic light object.
+
+ Args:
+ config: KNX heating config
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+
+ self._temperature: float | None = config.items.actor_feedback_temperature.value
+ if self._temperature is not None:
+ config.items.virtual_temperature.oh_post_update(self._temperature)
+
+ config.items.actor_feedback_temperature.listen_event(self._cb_actor_feedback_temperature_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
+ config.items.virtual_temperature.listen_event(self._cb_virtual_temperature_command, HABApp.openhab.events.ItemCommandEventFilter())
+
+ def _cb_actor_feedback_temperature_changed(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the actor feedback temperature changed.
+
+ Args:
+ event: trigger event
+ """
+ self._config.items.virtual_temperature.oh_post_update(event.value)
+ self._temperature = event.value
+
+ def _cb_virtual_temperature_command(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is triggered if the virtual temperature received a command.
+
+ Args:
+ event: trigger event
+ """
+ if self._temperature is None:
+ self._temperature = event.value
+
+ if self._config.items.temperature_offset.value is None:
+ self._config.items.temperature_offset.oh_send_command(0)
+
+ # T_offset_new = T_target - T_base # noqa: ERA001
+ # T_base = T_old - T_offset_old # noqa: ERA001
+ # ==> T_offset_new = T_target - T_old + T_offset_old
+ offset_new = event.value - self._temperature + self._config.items.temperature_offset.value
+ self._config.items.temperature_offset.oh_send_command(offset_new)
+ self._temperature = event.value
diff --git a/habapp_rules/actors/irrigation.py b/habapp_rules/actors/irrigation.py
index b5f2219..ee4c5b5 100644
--- a/habapp_rules/actors/irrigation.py
+++ b/habapp_rules/actors/irrigation.py
@@ -1,4 +1,5 @@
"""Rules to for garden watering."""
+
import datetime
import logging
@@ -7,108 +8,118 @@
import habapp_rules.actors.config.irrigation
import habapp_rules.core.exceptions
import habapp_rules.core.logger
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
class Irrigation(HABApp.Rule):
- """Rule for easy irrigation control.
+ """Rule for easy irrigation control.
- # Items:
+ # Items:
Switch I999_valve "Valve state" {channel="some_channel_config"}
- Switch I999_irrigation_active "Irrigation active"
- Number I999_irrigation_hour "Start hour [%d]"
- Number I999_irrigation_minute "Start minute[%d]"
- Number I999_irrigation_duration "Duration [%d]"
-
- # Config:
- config = habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve=HABApp.openhab.items.SwitchItem("I999_valve"),
- active=HABApp.openhab.items.SwitchItem("I999_irrigation_active"),
- hour=HABApp.openhab.items.NumberItem("I999_irrigation_hour"),
- minute=HABApp.openhab.items.NumberItem("I999_irrigation_minute"),
- duration=HABApp.openhab.items.NumberItem("I999_irrigation_duration"),
- )
- )
-
- # Rule init:
- habapp_rules.actors.irrigation.Irrigation(config)
- """
-
- def __init__(self, config: habapp_rules.actors.config.irrigation.IrrigationConfig) -> None:
- """Init of irrigation object.
-
- :param config: config for the rule
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not correct
- """
- self._config = config
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.valve.name)
-
- self.run.soon(self._cb_set_valve_state)
- self.run.every_minute(self._cb_set_valve_state)
-
- self._config.items.active.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._config.items.minute.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._config.items.hour.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.repetitions is not None:
- self._config.items.repetitions.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.brake is not None:
- self._config.items.brake.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful.")
-
- def _get_target_valve_state(self) -> bool:
- """Get target valve state, depending on the OpenHAB item states
-
- :return: True if valve should be on, otherwise False
- :raises habapp_rules.core.exceptions.HabAppRulesException: if value for hour / minute / duration is not valid
- """
- if not self._config.items.active.is_on():
- return False
-
- if any(item.value is None for item in (self._config.items.hour, self._config.items.minute, self._config.items.duration)):
- self._instance_logger.warning(
- f"OpenHAB item values are not valid for hour / minute / duration. Will return False. See current values: hour={self._config.items.hour.value} | minute={self._config.items.minute.value} | duration={self._config.items.duration.value}")
- return False
-
- repetitions = self._config.items.repetitions.value if self._config.items.repetitions else 0
- brake = int(self._config.items.brake.value) if self._config.items.brake else 0
-
- now = datetime.datetime.now()
- hour = int(self._config.items.hour.value)
- minute = int(self._config.items.minute.value)
- duration = int(self._config.items.duration.value)
-
- for idx in range(repetitions + 1):
- start_time = datetime.datetime.combine(date=now, time=datetime.time(hour, minute)) + datetime.timedelta(minutes=idx * (duration + brake))
- end_time = start_time + datetime.timedelta(minutes=duration)
- if self._is_in_time_range(start_time.time(), end_time.time(), now.time()):
- return True
- return False
-
- @staticmethod
- def _is_in_time_range(start_time: datetime.time, end_time: datetime.time, time_to_check: datetime.time) -> bool:
- """Check if a time is in a given range.
-
- :param start_time: start time of the time range
- :param end_time: end time of the time range
- :param time_to_check: time, which should be checked
- :return: True if the time, which should be checked is between start and stop time
- """
- if end_time < start_time:
- return start_time <= time_to_check or end_time > time_to_check
- return start_time <= time_to_check < end_time
-
- def _cb_set_valve_state(self, _: HABApp.openhab.events.ItemStateChangedEvent | None = None) -> None:
- """Callback to set the valve state, triggered by cyclic call or item event"""
- try:
- target_value = self._get_target_valve_state()
- except habapp_rules.core.exceptions.HabAppRulesException as exc:
- self._instance_logger.warning(f"Could not get target valve state, set it to false. Error: {exc}")
- target_value = False
-
- if self._config.items.valve.is_on() != target_value:
- self._instance_logger.info(f"Valve state changed to {target_value}")
- self._config.items.valve.oh_send_command("ON" if target_value else "OFF")
+ Switch I999_irrigation_active "Irrigation active"
+ Number I999_irrigation_hour "Start hour [%d]"
+ Number I999_irrigation_minute "Start minute[%d]"
+ Number I999_irrigation_duration "Duration [%d]"
+
+ # Config:
+ config = habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(
+ valve=HABApp.openhab.items.SwitchItem("I999_valve"),
+ active=HABApp.openhab.items.SwitchItem("I999_irrigation_active"),
+ hour=HABApp.openhab.items.NumberItem("I999_irrigation_hour"),
+ minute=HABApp.openhab.items.NumberItem("I999_irrigation_minute"),
+ duration=HABApp.openhab.items.NumberItem("I999_irrigation_duration"),
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.irrigation.Irrigation(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.irrigation.IrrigationConfig) -> None:
+ """Init of irrigation object.
+
+ Args:
+ config: config for the rule
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not correct
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.valve.name)
+
+ self.run.soon(self._cb_set_valve_state)
+ self.run.at(self.run.trigger.interval(None, 60), self._cb_set_valve_state)
+
+ self._config.items.active.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._config.items.minute.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._config.items.hour.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.repetitions is not None:
+ self._config.items.repetitions.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.brake is not None:
+ self._config.items.brake.listen_event(self._cb_set_valve_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful.")
+
+ def _get_target_valve_state(self) -> bool:
+ """Get target valve state, depending on the OpenHAB item states.
+
+ Returns:
+ True if valve should be on, otherwise False
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesError: if value for hour / minute / duration is not valid
+ """
+ if not self._config.items.active.is_on():
+ return False
+
+ if any(item.value is None for item in (self._config.items.hour, self._config.items.minute, self._config.items.duration)):
+ self._instance_logger.warning(
+ f"OpenHAB item values are not valid for hour / minute / duration. Will return False. See current values: hour={self._config.items.hour.value} | minute={self._config.items.minute.value} | duration={self._config.items.duration.value}"
+ )
+ return False
+
+ repetitions = self._config.items.repetitions.value if self._config.items.repetitions else 0
+ brake = int(self._config.items.brake.value) if self._config.items.brake else 0
+
+ now = datetime.datetime.now(tz=TIMEZONE)
+ hour = int(self._config.items.hour.value)
+ minute = int(self._config.items.minute.value)
+ duration = int(self._config.items.duration.value)
+
+ for idx in range(repetitions + 1):
+ start_time = datetime.datetime.combine(date=now, time=datetime.time(hour, minute)) + datetime.timedelta(minutes=idx * (duration + brake))
+ end_time = start_time + datetime.timedelta(minutes=duration)
+ if self._is_in_time_range(start_time.time(), end_time.time(), now.time()):
+ return True
+ return False
+
+ @staticmethod
+ def _is_in_time_range(start_time: datetime.time, end_time: datetime.time, time_to_check: datetime.time) -> bool:
+ """Check if a time is in a given range.
+
+ Args:
+ start_time: start time of the time range
+ end_time: end time of the time range
+ time_to_check: time, which should be checked
+
+ Returns:
+ True if the time, which should be checked is between start and stop time
+ """
+ if end_time < start_time:
+ return start_time <= time_to_check or end_time > time_to_check
+ return start_time <= time_to_check < end_time
+
+ def _cb_set_valve_state(self, _: HABApp.openhab.events.ItemStateChangedEvent | None = None) -> None:
+ """Callback to set the valve state, triggered by cyclic call or item event."""
+ try:
+ target_value = self._get_target_valve_state()
+ except habapp_rules.core.exceptions.HabAppRulesError as exc:
+ self._instance_logger.warning(f"Could not get target valve state, set it to false. Error: {exc}")
+ target_value = False
+
+ if self._config.items.valve.is_on() != target_value:
+ self._instance_logger.info(f"Valve state changed to {target_value}")
+ self._config.items.valve.oh_send_command("ON" if target_value else "OFF")
diff --git a/habapp_rules/actors/light.py b/habapp_rules/actors/light.py
index 24ee6e2..335c706 100644
--- a/habapp_rules/actors/light.py
+++ b/habapp_rules/actors/light.py
@@ -1,4 +1,5 @@
"""Rules to manage lights."""
+
from __future__ import annotations
import abc
@@ -24,732 +25,778 @@
LOGGER = logging.getLogger(__name__)
-BrightnessTypes = typing.Union[list[typing.Union[float, bool]], float, bool]
-
-
-# pylint: disable=no-member, too-many-instance-attributes
-class _LightBase(habapp_rules.core.state_machine_rule.StateMachineRule, metaclass=abc.ABCMeta):
- """Base class for lights."""
- states = [
- {"name": "manual"},
- {"name": "auto", "initial": "init", "children": [
- {"name": "init"},
- {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
- {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
- {"name": "off"},
- {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
- {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
- {"name": "restoreState"}
- ]}
- ]
-
- trans = [
- {"trigger": "manual_on", "source": "auto", "dest": "manual"},
- {"trigger": "manual_off", "source": "manual", "dest": "auto"},
- {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
- {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
- {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
- {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
- {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
- {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
- {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
- ]
- _state_observer: habapp_rules.actors.state_observer.StateObserverSwitch | habapp_rules.actors.state_observer.StateObserverDimmer
-
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init of basic light object.
-
- :param config: light config
- """
- self._config = config
-
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.light.name)
-
- # init state machine
- self._previous_state = None
- self._restore_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
-
- self._brightness_before = -1
- self._timeout_on = 0
- self._timeout_pre_off = 0
- self._timeout_pre_sleep = 0
- self._timeout_leaving = 0
- self.__time_sleep_start = 0
- self._set_timeouts()
- self._set_initial_state()
-
- # callbacks
- self._config.items.manual.listen_event(self._cb_manu, HABApp.openhab.events.ItemStateUpdatedEventFilter())
- if self._config.items.sleeping_state is not None:
- self._config.items.sleeping_state.listen_event(self._cb_sleeping, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.presence_state is not None:
- self._config.items.presence_state.listen_event(self._cb_presence, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._config.items.day.listen_event(self._cb_day, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._update_openhab_state()
- self._instance_logger.debug(super().get_initial_log_message())
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.manual.is_on():
- return "manual"
- if self._config.items.light.is_on():
- if (self._config.items.presence_state is not None and self._config.items.presence_state.value == habapp_rules.system.PresenceState.PRESENCE.value and
- getattr(self._config.items.sleeping_state, "value", "awake") in (habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.LOCKED.value)):
- return "auto_on"
- if (self._pre_sleep_configured() and
- self._config.items.presence_state is not None and
- self._config.items.presence_state.value in (habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value) and
- getattr(self._config.items.sleeping_state, "value", "") in (habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)):
- return "auto_presleep"
- if self._leaving_configured():
- return "auto_leaving"
- return "auto_on"
- return "auto_off"
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This should method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_light_state()
- self._previous_state = self.state
-
- def _pre_off_configured(self) -> bool:
- """Check whether pre-off is configured for the current day/night/sleep state
-
- :return: True if pre-off is configured
- """
- return bool(self._timeout_pre_off)
-
- def _leaving_configured(self) -> bool:
- """Check whether leaving is configured for the current day/night/sleep state
-
- :return: True if leaving is configured
- """
- if self._config.parameter.leaving_only_if_on and self._config.items.light.is_off():
- return False
-
- return bool(self._timeout_leaving)
-
- def _pre_sleep_configured(self) -> bool:
- """Check whether pre-sleep is configured for the current day/night state
-
- :return: True if pre-sleep is configured
- """
- if self._config.items.sleeping_state is None:
- return False
-
- pre_sleep_prevent = False
- if self._config.items.pre_sleep_prevent is not None:
- pre_sleep_prevent = self._config.items.pre_sleep_prevent.is_on()
- elif self._config.parameter.pre_sleep_prevent is not None:
- try:
- pre_sleep_prevent = self._config.parameter.pre_sleep_prevent()
- except Exception as ex: # pylint: disable=broad-except
- self._instance_logger.error(f"Could not execute pre_sleep_prevent function: {ex}. pre_sleep_prevent will be set to False.")
- pre_sleep_prevent = False
-
- return bool(self._timeout_pre_sleep) and not pre_sleep_prevent
-
- def on_enter_auto_restoreState(self): # pylint: disable=invalid-name
- """On enter of state auto_restoreState."""
- self._restore_state = "auto_off" if self._restore_state == "auto_preoff" else self._restore_state
-
- if self._restore_state:
- self._set_state(self._restore_state)
-
- def _was_on_before(self) -> bool:
- """Check whether the dimmer was on before
-
- :return: True if the dimmer was on before, else False
- """
- return bool(self._brightness_before)
-
- def _set_timeouts(self) -> None:
- """Set timeouts depending on the current day/night/sleep state."""
- if self._get_sleeping_activ():
- self._timeout_on = self._config.parameter.on.sleeping.timeout
- self._timeout_pre_off = getattr(self._config.parameter.pre_off.sleeping if self._config.parameter.pre_off else 0, "timeout", 0)
- self._timeout_leaving = getattr(self._config.parameter.leaving.sleeping if self._config.parameter.leaving else 0, "timeout", 0)
- self._timeout_pre_sleep = 0
-
- elif self._config.items.day.is_on():
- self._timeout_on = self._config.parameter.on.day.timeout
- self._timeout_pre_off = getattr(self._config.parameter.pre_off.day if self._config.parameter.pre_off else 0, "timeout", 0)
- self._timeout_leaving = getattr(self._config.parameter.leaving.day if self._config.parameter.leaving else 0, "timeout", 0)
- self._timeout_pre_sleep = getattr(self._config.parameter.pre_sleep.day if self._config.parameter.pre_sleep else 0, "timeout", 0)
- else:
- self._timeout_on = self._config.parameter.on.night.timeout
- self._timeout_pre_off = getattr(self._config.parameter.pre_off.night if self._config.parameter.pre_off else 0, "timeout", 0)
- self._timeout_leaving = getattr(self._config.parameter.leaving.night if self._config.parameter.leaving else 0, "timeout", 0)
- self._timeout_pre_sleep = getattr(self._config.parameter.pre_sleep.night if self._config.parameter.pre_sleep else 0, "timeout", 0)
-
- self.state_machine.states["auto"].states["on"].timeout = self._timeout_on
- self.state_machine.states["auto"].states["preoff"].timeout = self._timeout_pre_off
- self.state_machine.states["auto"].states["leaving"].timeout = self._timeout_leaving
- self.state_machine.states["auto"].states["presleep"].timeout = self._timeout_pre_sleep
-
- @abc.abstractmethod
- def _set_light_state(self) -> None:
- """Set brightness to light."""
-
- # pylint: disable=too-many-branches, too-many-return-statements
- def _get_target_brightness(self) -> bool | float | None:
- """Get configured brightness for the current day/night/sleep state
-
- :return: brightness value
- """
- sleeping_active = self._get_sleeping_activ(True)
-
- if self.state == "auto_on":
- if self._previous_state == "manual":
- return None
- if self._previous_state in {"auto_preoff", "auto_leaving", "auto_presleep"}:
- return self._brightness_before
-
- # starting from here: previous state == auto_off
- if isinstance(man_value := self._state_observer.last_manual_event.value, (int, float)) and 0 < man_value < 100:
- return None
- if self._state_observer.last_manual_event.value == "INCREASE":
- return None
-
- if sleeping_active:
- brightness_from_config = self._config.parameter.on.sleeping.brightness
- elif self._config.items.day.is_on():
- brightness_from_config = self._config.parameter.on.day.brightness
- else:
- brightness_from_config = self._config.parameter.on.night.brightness
-
- if brightness_from_config is True and self._state_observer.last_manual_event.value == "ON":
- return None
-
- return brightness_from_config
-
- if self.state == "auto_preoff":
- self._brightness_before = self._state_observer.value
-
- if sleeping_active:
- brightness_from_config = getattr(self._config.parameter.pre_off.sleeping if self._config.parameter.pre_off else None, "brightness", None)
- elif self._config.items.day.is_on():
- brightness_from_config = getattr(self._config.parameter.pre_off.day if self._config.parameter.pre_off else None, "brightness", None)
- else:
- brightness_from_config = getattr(self._config.parameter.pre_off.night if self._config.parameter.pre_off else None, "brightness", None)
-
- if brightness_from_config is None:
- return None
-
- if isinstance(self._state_observer.value, (float, int)) and brightness_from_config > self._state_observer.value:
- return math.ceil(self._state_observer.value / 2)
- return brightness_from_config
-
- if self.state == "auto_off":
- if self._previous_state == "manual":
- return None
- return False
-
- if self.state == "auto_presleep":
- if self._config.items.day.is_on():
- return getattr(self._config.parameter.pre_sleep.day if self._config.parameter.pre_sleep else None, "brightness", None)
- return getattr(self._config.parameter.pre_sleep.night if self._config.parameter.pre_sleep else None, "brightness", None)
-
- if self.state == "auto_leaving":
- if sleeping_active:
- return getattr(self._config.parameter.leaving.sleeping if self._config.parameter.leaving else None, "brightness", None)
- if self._config.items.day.is_on():
- return getattr(self._config.parameter.leaving.day if self._config.parameter.leaving else None, "brightness", None)
- return getattr(self._config.parameter.leaving.night if self._config.parameter.leaving else None, "brightness", None)
-
- return None
-
- def on_enter_auto_init(self):
- """Callback, which is called on enter of init state"""
- if self._config.items.light.is_on():
- self.to_auto_on()
- else:
- self.to_auto_off()
-
- def _get_sleeping_activ(self, include_pre_sleep: bool = False) -> bool:
- """Get if sleeping is active.
-
- :param include_pre_sleep: if true, also pre sleep will be handled as sleeping
- :return: true if sleeping active
- """
- sleep_states = [habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value] if include_pre_sleep else [habapp_rules.system.SleepState.SLEEPING.value]
- return getattr(self._config.items.sleeping_state, "value", "") in sleep_states
-
- def _cb_hand_on(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered by the state observer if a manual ON command was detected.
-
- :param event: original trigger event
- """
- self._instance_logger.debug("Hand 'ON' detected")
- self.hand_on()
-
- def _cb_hand_off(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered by the state observer if a manual OFF command was detected.
-
- :param event: original trigger event
- """
- self._instance_logger.debug("Hand 'OFF' detected")
- self.hand_off()
-
- def _cb_manu(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
- """Callback, which is triggered if the manual switch has a state event.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.manual_on()
- else:
- self.manual_off()
-
- def _cb_day(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the day/night switch has a state change event.
-
- :param event: trigger event
- """
- self._set_timeouts()
-
- def _cb_presence(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the presence state has a state change event.
-
- :param event: trigger event
- """
- self._set_timeouts()
- if event.value == habapp_rules.system.PresenceState.LEAVING.value:
- self._brightness_before = self._state_observer.value
- self._restore_state = self._previous_state
- self.leaving_started()
- elif event.value == habapp_rules.system.PresenceState.PRESENCE.value and self.state == "auto_leaving":
- self.leaving_aborted()
-
- def _cb_sleeping(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the sleep state has a state change event.
-
- :param event: trigger event
- """
- self._set_timeouts()
- if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
- self._brightness_before = self._state_observer.value
- self._restore_state = self._previous_state
- self.__time_sleep_start = time.time()
- self.sleep_started()
- elif event.value == habapp_rules.system.SleepState.AWAKE.value and time.time() - self.__time_sleep_start <= 60:
- self.sleep_aborted()
+DIMMER_VALUE_TOLERANCE = 5
+MINUTE_IN_SEC = 60
+
+
+class _LightBase(habapp_rules.core.state_machine_rule.StateMachineRule, abc.ABC):
+ """Base class for lights."""
+
+ states: typing.ClassVar = [
+ {"name": "manual"},
+ {
+ "name": "auto",
+ "initial": "init",
+ "children": [
+ {"name": "init"},
+ {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
+ {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
+ {"name": "off"},
+ {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
+ {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
+ {"name": "restoreState"},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "manual_on", "source": "auto", "dest": "manual"},
+ {"trigger": "manual_off", "source": "manual", "dest": "auto"},
+ {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
+ {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
+ {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
+ {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
+ {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
+ {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
+ ]
+ _state_observer: habapp_rules.actors.state_observer.StateObserverSwitch | habapp_rules.actors.state_observer.StateObserverDimmer
+
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init of basic light object.
+
+ Args:
+ config: light config
+ """
+ self._config = config
+
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.light.name)
+
+ # init state machine
+ self._previous_state = None
+ self._restore_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+
+ self._brightness_before = -1
+ self._timeout_on = 0
+ self._timeout_pre_off = 0
+ self._timeout_pre_sleep = 0
+ self._timeout_leaving = 0
+ self.__time_sleep_start = 0
+ self._set_timeouts()
+ self._set_initial_state()
+
+ # callbacks
+ self._config.items.manual.listen_event(self._cb_manu, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+ if self._config.items.sleeping_state is not None:
+ self._config.items.sleeping_state.listen_event(self._cb_sleeping, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.presence_state is not None:
+ self._config.items.presence_state.listen_event(self._cb_presence, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._config.items.day.listen_event(self._cb_day, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._update_openhab_state()
+ self._instance_logger.debug(super().get_initial_log_message())
+
+ def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.manual.is_on():
+ return "manual"
+ if self._config.items.light.is_on():
+ if (
+ self._config.items.presence_state is not None
+ and self._config.items.presence_state.value == habapp_rules.system.PresenceState.PRESENCE.value
+ and getattr(self._config.items.sleeping_state, "value", "awake") in {habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.LOCKED.value}
+ ):
+ return "auto_on"
+ if (
+ self._pre_sleep_configured()
+ and self._config.items.presence_state is not None
+ and self._config.items.presence_state.value in {habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value}
+ and getattr(self._config.items.sleeping_state, "value", "") in {habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value}
+ ):
+ return "auto_presleep"
+ if self._leaving_configured():
+ return "auto_leaving"
+ return "auto_on"
+ return "auto_off"
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This should method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_light_state()
+ self._previous_state = self.state
+
+ def _pre_off_configured(self) -> bool:
+ """Check whether pre-off is configured for the current day/night/sleep state.
+
+ Returns:
+ True if pre-off is configured
+ """
+ return bool(self._timeout_pre_off)
+
+ def _leaving_configured(self) -> bool:
+ """Check whether leaving is configured for the current day/night/sleep state.
+
+ Returns:
+ True if leaving is configured
+ """
+ if self._config.parameter.leaving_only_if_on and self._config.items.light.is_off():
+ return False
+
+ return bool(self._timeout_leaving)
+
+ def _pre_sleep_configured(self) -> bool:
+ """Check whether pre-sleep is configured for the current day/night state.
+
+ Returns:
+ True if pre-sleep is configured
+ """
+ if self._config.items.sleeping_state is None:
+ return False
+
+ pre_sleep_prevent = False
+ if self._config.items.pre_sleep_prevent is not None:
+ pre_sleep_prevent = self._config.items.pre_sleep_prevent.is_on()
+ elif self._config.parameter.pre_sleep_prevent is not None:
+ try:
+ pre_sleep_prevent = self._config.parameter.pre_sleep_prevent()
+ except Exception:
+ self._instance_logger.exception("Could not execute pre_sleep_prevent function. pre_sleep_prevent will be set to False.")
+ pre_sleep_prevent = False
+
+ return bool(self._timeout_pre_sleep) and not pre_sleep_prevent
+
+ def on_enter_auto_restoreState(self) -> None: # noqa: N802
+ """On enter of state auto_restoreState."""
+ self._restore_state = "auto_off" if self._restore_state == "auto_preoff" else self._restore_state
+
+ if self._restore_state:
+ self._set_state(self._restore_state)
+
+ def _was_on_before(self) -> bool:
+ """Check whether the dimmer was on before.
+
+ Returns:
+ True if the dimmer was on before, else False
+ """
+ return bool(self._brightness_before)
+
+ def _set_timeouts(self) -> None:
+ """Set timeouts depending on the current day/night/sleep state."""
+ if self._get_sleeping_activ():
+ self._timeout_on = self._config.parameter.on.sleeping.timeout
+ self._timeout_pre_off = getattr(self._config.parameter.pre_off.sleeping if self._config.parameter.pre_off else 0, "timeout", 0)
+ self._timeout_leaving = getattr(self._config.parameter.leaving.sleeping if self._config.parameter.leaving else 0, "timeout", 0)
+ self._timeout_pre_sleep = 0
+
+ elif self._config.items.day.is_on():
+ self._timeout_on = self._config.parameter.on.day.timeout
+ self._timeout_pre_off = getattr(self._config.parameter.pre_off.day if self._config.parameter.pre_off else 0, "timeout", 0)
+ self._timeout_leaving = getattr(self._config.parameter.leaving.day if self._config.parameter.leaving else 0, "timeout", 0)
+ self._timeout_pre_sleep = getattr(self._config.parameter.pre_sleep.day if self._config.parameter.pre_sleep else 0, "timeout", 0)
+ else:
+ self._timeout_on = self._config.parameter.on.night.timeout
+ self._timeout_pre_off = getattr(self._config.parameter.pre_off.night if self._config.parameter.pre_off else 0, "timeout", 0)
+ self._timeout_leaving = getattr(self._config.parameter.leaving.night if self._config.parameter.leaving else 0, "timeout", 0)
+ self._timeout_pre_sleep = getattr(self._config.parameter.pre_sleep.night if self._config.parameter.pre_sleep else 0, "timeout", 0)
+
+ self.state_machine.states["auto"].states["on"].timeout = self._timeout_on
+ self.state_machine.states["auto"].states["preoff"].timeout = self._timeout_pre_off
+ self.state_machine.states["auto"].states["leaving"].timeout = self._timeout_leaving
+ self.state_machine.states["auto"].states["presleep"].timeout = self._timeout_pre_sleep
+
+ @abc.abstractmethod
+ def _set_light_state(self) -> None:
+ """Set brightness to light."""
+
+ def _get_target_brightness(self) -> bool | float | None: # noqa: C901, PLR0912
+ """Get configured brightness for the current day/night/sleep state.
+
+ Returns:
+ brightness value
+ """
+ sleeping_active = self._get_sleeping_activ(include_pre_sleep=True)
+
+ if self.state == "auto_on":
+ if self._previous_state == "manual":
+ return None
+ if self._previous_state in {"auto_preoff", "auto_leaving", "auto_presleep"}:
+ return self._brightness_before
+
+ # starting from here: previous state == auto_off
+ if isinstance(man_value := self._state_observer.last_manual_event.value, int | float) and 0 < man_value < 100: # noqa: PLR2004
+ return None
+ if self._state_observer.last_manual_event.value == "INCREASE":
+ return None
+
+ if sleeping_active:
+ brightness_from_config = self._config.parameter.on.sleeping.brightness
+ elif self._config.items.day.is_on():
+ brightness_from_config = self._config.parameter.on.day.brightness
+ else:
+ brightness_from_config = self._config.parameter.on.night.brightness
+
+ if brightness_from_config is True and self._state_observer.last_manual_event.value == "ON":
+ return None
+
+ return brightness_from_config
+
+ if self.state == "auto_preoff":
+ self._brightness_before = self._state_observer.value
+
+ if sleeping_active:
+ brightness_from_config = getattr(self._config.parameter.pre_off.sleeping if self._config.parameter.pre_off else None, "brightness", None)
+ elif self._config.items.day.is_on():
+ brightness_from_config = getattr(self._config.parameter.pre_off.day if self._config.parameter.pre_off else None, "brightness", None)
+ else:
+ brightness_from_config = getattr(self._config.parameter.pre_off.night if self._config.parameter.pre_off else None, "brightness", None)
+
+ if brightness_from_config is None:
+ return None
+
+ if isinstance(self._state_observer.value, float | int) and brightness_from_config > self._state_observer.value:
+ return math.ceil(self._state_observer.value / 2)
+ return brightness_from_config
+
+ if self.state == "auto_off":
+ if self._previous_state == "manual":
+ return None
+ return False
+
+ if self.state == "auto_presleep":
+ if self._config.items.day.is_on():
+ return getattr(self._config.parameter.pre_sleep.day if self._config.parameter.pre_sleep else None, "brightness", None)
+ return getattr(self._config.parameter.pre_sleep.night if self._config.parameter.pre_sleep else None, "brightness", None)
+
+ if self.state == "auto_leaving":
+ if sleeping_active:
+ return getattr(self._config.parameter.leaving.sleeping if self._config.parameter.leaving else None, "brightness", None)
+ if self._config.items.day.is_on():
+ return getattr(self._config.parameter.leaving.day if self._config.parameter.leaving else None, "brightness", None)
+ return getattr(self._config.parameter.leaving.night if self._config.parameter.leaving else None, "brightness", None)
+
+ return None
+
+ def on_enter_auto_init(self) -> None:
+ """Callback, which is called on enter of init state."""
+ if self._config.items.light.is_on():
+ self.to_auto_on()
+ else:
+ self.to_auto_off()
+
+ def _get_sleeping_activ(self, include_pre_sleep: bool = False) -> bool:
+ """Get if sleeping is active.
+
+ Args:
+ include_pre_sleep: if true, also pre sleep will be handled as sleeping
+
+ Returns:
+ true if sleeping active
+ """
+ sleep_states = [habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value] if include_pre_sleep else [habapp_rules.system.SleepState.SLEEPING.value]
+ return getattr(self._config.items.sleeping_state, "value", "") in sleep_states
+
+ def _cb_hand_on(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered by the state observer if a manual ON command was detected.
+
+ Args:
+ event: original trigger event
+ """
+ self._instance_logger.debug("Hand 'ON' detected")
+ self.hand_on()
+
+ def _cb_hand_off(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered by the state observer if a manual OFF command was detected.
+
+ Args:
+ event: original trigger event
+ """
+ self._instance_logger.debug("Hand 'OFF' detected")
+ self.hand_off()
+
+ def _cb_manu(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
+ """Callback, which is triggered if the manual switch has a state event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.manual_on()
+ else:
+ self.manual_off()
+
+ def _cb_day(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered if the day/night switch has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ self._set_timeouts()
+
+ def _cb_presence(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the presence state has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ self._set_timeouts()
+ if event.value == habapp_rules.system.PresenceState.LEAVING.value:
+ self._brightness_before = self._state_observer.value
+ self._restore_state = self._previous_state
+ self.leaving_started()
+ elif event.value == habapp_rules.system.PresenceState.PRESENCE.value and self.state == "auto_leaving":
+ self.leaving_aborted()
+
+ def _cb_sleeping(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the sleep state has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ self._set_timeouts()
+ if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
+ self._brightness_before = self._state_observer.value
+ self._restore_state = self._previous_state
+ self.__time_sleep_start = time.time()
+ self.sleep_started()
+ elif event.value == habapp_rules.system.SleepState.AWAKE.value and time.time() - self.__time_sleep_start <= MINUTE_IN_SEC:
+ self.sleep_aborted()
class LightSwitch(_LightBase):
- """Rules class to manage basic light states.
-
- # KNX-things:
- Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
- Type switch : light "Light" [ switch="1/1/10+1/1/13" ]
- }
-
- # Items:
- Switch I01_01_Light "Light [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light"}
- Switch I00_00_Light_manual "Light manual"
-
- # Config:
- config = habapp_rules.actors.config.light.LightConfig(
- items = habapp_rules.actors.config.light.LightItems(
- light="I01_01_Light",
- manual="I00_00_Light_manual",
- presence_state="I999_00_Presence_state",
- sleeping_state="I999_00_Sleeping_state",
- day="I999_00_Day"
- )
- )
-
- # Rule init:
- habapp_rules.actors.light.LightSwitch(config)
- """
-
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init of basic light object.
-
- :param config: light config
- :raises TypeError: if type of light_item is not supported
- """
- if not isinstance(config.items.light, HABApp.openhab.items.switch_item.SwitchItem):
- raise TypeError(f"type: {type(config.items.light)} is not supported!")
-
- self._state_observer = habapp_rules.actors.state_observer.StateObserverSwitch(config.items.light.name, self._cb_hand_on, self._cb_hand_off)
-
- _LightBase.__init__(self, config)
-
- def _update_openhab_state(self) -> None:
- _LightBase._update_openhab_state(self)
-
- if self.state == "auto_preoff":
- timeout = self.state_machine.get_state(self.state).timeout
-
- warn_thread_1 = threading.Thread(target=self.__trigger_warning, args=("auto_preoff", 0, 1), daemon=True)
- warn_thread_1.start()
-
- if timeout > 60:
- # add additional warning for long timeouts
- warn_thread_2 = threading.Thread(target=self.__trigger_warning, args=("auto_preoff", timeout / 2, 2), daemon=True)
- warn_thread_2.start()
-
- def __trigger_warning(self, state_name: str, wait_time: float, switch_off_amount: int) -> None:
- """Trigger light switch off warning.
-
- :param state_name: name of state where the warning should be triggered. If different no command will be sent
- :param wait_time: time between start of the thread and switch off / on
- :param switch_off_amount: number of switch off
- """
-
- if wait_time:
- time.sleep(wait_time)
-
- for idx in range(switch_off_amount):
- if self.state != state_name:
- break
- self._state_observer.send_command("OFF")
- time.sleep(0.2)
- if self.state != state_name:
- break
- self._state_observer.send_command("ON")
- if idx + 1 < switch_off_amount:
- time.sleep(0.5)
-
- def _set_light_state(self) -> None:
- """Set brightness to light."""
- target_value = self._get_target_brightness()
- if target_value is None or self._previous_state is None:
- # don't change value if target_value is None or _set_light_state will be called during init (_previous_state == None)
- return
-
- target_value = "ON" if target_value else "OFF"
- self._instance_logger.debug(f"set brightness {target_value}")
- self._state_observer.send_command(target_value)
+ """Rules class to manage basic light states.
+
+ # KNX-things:
+ Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
+ Type switch : light "Light" [ switch="1/1/10+1/1/13" ]
+ }
+
+ # Items:
+ Switch I01_01_Light "Light [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light"}
+ Switch I00_00_Light_manual "Light manual"
+
+ # Config:
+ config = habapp_rules.actors.config.light.LightConfig(
+ items = habapp_rules.actors.config.light.LightItems(
+ light="I01_01_Light",
+ manual="I00_00_Light_manual",
+ presence_state="I999_00_Presence_state",
+ sleeping_state="I999_00_Sleeping_state",
+ day="I999_00_Day"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.light.LightSwitch(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init of basic light object.
+
+ Args:
+ config: light config
+
+ Raises:
+ TypeError: if type of light_item is not supported
+ """
+ if not isinstance(config.items.light, HABApp.openhab.items.switch_item.SwitchItem):
+ msg = f"type: {type(config.items.light)} is not supported!"
+ raise TypeError(msg)
+
+ self._state_observer = habapp_rules.actors.state_observer.StateObserverSwitch(config.items.light.name, self._cb_hand_on, self._cb_hand_off)
+
+ _LightBase.__init__(self, config)
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states."""
+ _LightBase._update_openhab_state(self) # noqa: SLF001
+
+ if self.state == "auto_preoff":
+ timeout = self.state_machine.get_state(self.state).timeout
+
+ warn_thread_1 = threading.Thread(target=self.__trigger_warning, args=("auto_preoff", 0, 1), daemon=True)
+ warn_thread_1.start()
+
+ if timeout > MINUTE_IN_SEC:
+ # add additional warning for long timeouts
+ warn_thread_2 = threading.Thread(target=self.__trigger_warning, args=("auto_preoff", timeout / 2, 2), daemon=True)
+ warn_thread_2.start()
+
+ def __trigger_warning(self, state_name: str, wait_time: float, switch_off_amount: int) -> None:
+ """Trigger light switch off warning.
+
+ Args:
+ state_name: name of state where the warning should be triggered. If different no command will be sent
+ wait_time: time between start of the thread and switch off / on
+ switch_off_amount: number of switch off
+ """
+ if wait_time:
+ time.sleep(wait_time)
+
+ for idx in range(switch_off_amount):
+ if self.state != state_name:
+ break
+ self._state_observer.send_command("OFF")
+ time.sleep(0.2)
+ if self.state != state_name:
+ break
+ self._state_observer.send_command("ON")
+ if idx + 1 < switch_off_amount:
+ time.sleep(0.5)
+
+ def _set_light_state(self) -> None:
+ """Set brightness to light."""
+ target_value = self._get_target_brightness()
+ if target_value is None or self._previous_state is None:
+ # don't change value if target_value is None or _set_light_state will be called during init (_previous_state == None)
+ return
+
+ target_value = "ON" if target_value else "OFF"
+ self._instance_logger.debug(f"set brightness {target_value}")
+ self._state_observer.send_command(target_value)
class LightDimmer(_LightBase):
- """Rules class to manage basic light states.
-
- # KNX-things:
- Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
- Type dimmer : light "Light" [ switch="1/1/10", position="1/1/13+<1/1/15" ]
- Type dimmer-control : light_ctr "Light control" [ increaseDecrease="1/1/12"]
- Type dimmer : light_group "Light Group" [ switch="1/1/240", position="1/1/243"]
- }
-
- # Items:
- Dimmer I01_01_Light "Light [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light"}
- Dimmer I01_01_Light_ctr "Light ctr" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_ctr"}
- Dimmer I01_01_Light_group "Light Group" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_group"}
- Switch I00_00_Light_manual "Light manual"
-
- # Config:
- config = habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="I01_01_Light",
- light_control=["I01_01_Light_ctr"],
- manual="I00_00_Light_manual",
- presence_state="I999_00_Presence_state",
- sleeping_state="I999_00_Sleeping_state",
- day="I999_00_Day"
- )
- )
-
- # Rule init:
- habapp_rules.actors.light.LightDimmer(config)
- """
-
- trans = copy.deepcopy(_LightBase.trans)
- trans.append({"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"})
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init of basic light object.
-
- :param config: light config
- :raises TypeError: if type of light_item is not supported
- """
- if not isinstance(config.items.light, HABApp.openhab.items.dimmer_item.DimmerItem):
- raise TypeError(f"type: {type(config.items.light)} is not supported!")
-
- control_names = [item.name for item in config.items.light_control]
- group_names = [item.name for item in config.items.light_groups]
- self._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer(config.items.light.name, self._cb_hand_on, self._cb_hand_off, self._cb_hand_changed, control_names=control_names, group_names=group_names)
-
- _LightBase.__init__(self, config)
-
- def _set_light_state(self) -> None:
- """Set brightness to light."""
- target_value = self._get_target_brightness()
- if target_value is None or self._previous_state is None:
- # don't change value if target_value is None or _set_light_state will be called during init (_previous_state == None)
- return
-
- if isinstance(target_value, bool):
- if target_value:
- target_value = "ON"
- else:
- target_value = "OFF"
- self._instance_logger.debug(f"set brightness {target_value}")
- self._state_observer.send_command(target_value)
-
- def _cb_hand_changed(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent | HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered by the state observer if a manual OFF command was detected.
-
- :param event: original trigger event
- """
- if isinstance(event, HABApp.openhab.events.ItemStateChangedEvent) and abs(event.value - event.old_value) > 5:
- self.hand_changed()
-
-
-# pylint: disable=protected-access
+ """Rules class to manage basic light states.
+
+ # KNX-things:
+ Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
+ Type dimmer : light "Light" [ switch="1/1/10", position="1/1/13+<1/1/15" ]
+ Type dimmer-control : light_ctr "Light control" [ increaseDecrease="1/1/12"]
+ Type dimmer : light_group "Light Group" [ switch="1/1/240", position="1/1/243"]
+ }
+
+ # Items:
+ Dimmer I01_01_Light "Light [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light"}
+ Dimmer I01_01_Light_ctr "Light ctr" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_ctr"}
+ Dimmer I01_01_Light_group "Light Group" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_group"}
+ Switch I00_00_Light_manual "Light manual"
+
+ # Config:
+ config = habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(
+ light="I01_01_Light",
+ light_control=["I01_01_Light_ctr"],
+ manual="I00_00_Light_manual",
+ presence_state="I999_00_Presence_state",
+ sleeping_state="I999_00_Sleeping_state",
+ day="I999_00_Day"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.light.LightDimmer(config)
+ """
+
+ trans = copy.deepcopy(_LightBase.trans)
+ trans.append({"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"})
+
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init of basic light object.
+
+ Args:
+ config: light config
+
+ Raises:
+ TypeError: if type of light_item is not supported
+ """
+ if not isinstance(config.items.light, HABApp.openhab.items.dimmer_item.DimmerItem):
+ msg = f"type: {type(config.items.light)} is not supported!"
+ raise TypeError(msg)
+
+ control_names = [item.name for item in config.items.light_control]
+ group_names = [item.name for item in config.items.light_groups]
+ self._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer(config.items.light.name, self._cb_hand_on, self._cb_hand_off, self._cb_hand_changed, control_names=control_names, group_names=group_names)
+
+ _LightBase.__init__(self, config)
+
+ def _set_light_state(self) -> None:
+ """Set brightness to light."""
+ target_value = self._get_target_brightness()
+ if target_value is None or self._previous_state is None:
+ # don't change value if target_value is None or _set_light_state will be called during init (_previous_state == None)
+ return
+
+ if isinstance(target_value, bool):
+ target_value = "ON" if target_value else "OFF"
+ self._instance_logger.debug(f"set brightness {target_value}")
+ self._state_observer.send_command(target_value)
+
+ def _cb_hand_changed(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent | HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered by the state observer if a manual OFF command was detected.
+
+ Args:
+ event: original trigger event
+ """
+ if isinstance(event, HABApp.openhab.events.ItemStateChangedEvent) and abs(event.value - event.old_value) > DIMMER_VALUE_TOLERANCE:
+ self.hand_changed()
+
+
class _LightExtendedMixin:
- """Mixin class for adding door and motion functionality"""
- states: dict
- trans: list
- state: str
- _config: habapp_rules.actors.config.light.LightConfig
- state_machine: habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout
- _get_sleeping_activ: typing.Callable[[bool | None], bool]
-
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init mixin class.
-
- :param config: light config
- """
- self.states = _LightExtendedMixin._add_additional_states(self.states)
- self.trans = _LightExtendedMixin._add_additional_transitions(self.trans)
-
- self._timeout_motion = 0
- self._timeout_door = 0
-
- self._hand_off_lock_time = config.parameter.hand_off_lock_time
- self._hand_off_timestamp = 0
-
- @staticmethod
- def _add_additional_states(states_dict: dict) -> dict:
- """Add additional states for door and motion.
-
- :param states_dict: current state dictionary
- :return: current + new states
- """
- states_dict = copy.deepcopy(states_dict)
- states_dict[1]["children"].append({"name": "door", "timeout": 999, "on_timeout": "door_timeout"})
- states_dict[1]["children"].append({"name": "motion", "timeout": 999, "on_timeout": "motion_timeout"})
- return states_dict
-
- @staticmethod
- def _add_additional_transitions(transitions_list: list[dict]) -> list[dict]:
- """Add additional transitions for door and motion
-
- :param transitions_list: current transitions
- :return: current + new transitions
- """
- transitions_list = copy.deepcopy(transitions_list)
-
- transitions_list.append({"trigger": "motion_on", "source": "auto_door", "dest": "auto_motion", "conditions": "_motion_configured"})
- transitions_list.append({"trigger": "motion_on", "source": "auto_off", "dest": "auto_motion", "conditions": ["_motion_configured", "_motion_door_allowed"]})
- transitions_list.append({"trigger": "motion_on", "source": "auto_preoff", "dest": "auto_motion", "conditions": "_motion_configured"})
- transitions_list.append({"trigger": "motion_off", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured"})
- transitions_list.append({"trigger": "motion_off", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured"})
- transitions_list.append({"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured", "before": "_log_motion_timeout_warning"})
- transitions_list.append({"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured", "before": "_log_motion_timeout_warning"})
- transitions_list.append({"trigger": "hand_off", "source": "auto_motion", "dest": "auto_off"})
-
- transitions_list.append({"trigger": "door_opened", "source": ["auto_off", "auto_preoff", "auto_door"], "dest": "auto_door", "conditions": ["_door_configured", "_motion_door_allowed"]})
- transitions_list.append({"trigger": "door_timeout", "source": "auto_door", "dest": "auto_preoff", "conditions": "_pre_off_configured"})
- transitions_list.append({"trigger": "door_timeout", "source": "auto_door", "dest": "auto_off", "unless": "_pre_off_configured"})
- transitions_list.append({"trigger": "door_closed", "source": "auto_leaving", "dest": "auto_off", "conditions": "_door_off_leaving_configured"})
- transitions_list.append({"trigger": "hand_off", "source": "auto_door", "dest": "auto_off"})
-
- transitions_list.append({"trigger": "leaving_started", "source": ["auto_motion", "auto_door"], "dest": "auto_leaving", "conditions": "_leaving_configured"})
- transitions_list.append({"trigger": "sleep_started", "source": ["auto_motion", "auto_door"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"})
-
- return transitions_list
-
- def add_additional_callbacks(self) -> None:
- """Add additional callbacks for motion and door items."""
- if self._config.items.motion is not None:
- self._config.items.motion.listen_event(self._cb_motion, HABApp.openhab.events.ItemStateChangedEventFilter())
- for item_door in self._config.items.doors:
- item_door.listen_event(self._cb_door, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- initial_state = _LightBase._get_initial_state(self, default_value)
-
- if initial_state == "auto_on" and self._config.items.motion is not None and self._config.items.motion.is_on() and self._motion_configured():
- initial_state = "auto_motion"
- return initial_state
-
- def _set_timeouts(self) -> None:
- """Set timeouts depending on the current day/night/sleep state."""
- _LightBase._set_timeouts(self)
-
- # set timeouts of additional states
- if self._get_sleeping_activ():
- self._timeout_motion = getattr(self._config.parameter.motion.sleeping if self._config.parameter.motion else 0, "timeout", 0)
- self._timeout_door = getattr(self._config.parameter.door.sleeping if self._config.parameter.door else 0, "timeout", 0)
-
- elif self._config.items.day.is_on():
- self._timeout_motion = getattr(self._config.parameter.motion.day if self._config.parameter.motion else 0, "timeout", 0)
- self._timeout_door = getattr(self._config.parameter.door.day if self._config.parameter.door else 0, "timeout", 0)
- else:
- self._timeout_motion = getattr(self._config.parameter.motion.night if self._config.parameter.motion else 0, "timeout", 0)
- self._timeout_door = getattr(self._config.parameter.door.night if self._config.parameter.door else 0, "timeout", 0)
-
- self.state_machine.states["auto"].states["motion"].timeout = self._timeout_motion
- self.state_machine.states["auto"].states["door"].timeout = self._timeout_door
-
- def _get_target_brightness(self) -> bool | float | None:
- """Get configured brightness for the current day/night/sleep state. Must be called before _get_target_brightness of base class
-
- :return: brightness value
- :raises habapp_rules.core.exceptions.HabAppRulesException: if current state is not supported
- """
- if self.state == "auto_motion":
- if self._get_sleeping_activ(True):
- return getattr(self._config.parameter.motion.sleeping if self._config.parameter.motion else None, "brightness", None)
- if self._config.items.day.is_on():
- return getattr(self._config.parameter.motion.day if self._config.parameter.motion else None, "brightness", None)
- return getattr(self._config.parameter.motion.night if self._config.parameter.motion else None, "brightness", None)
-
- if self.state == "auto_door":
- if self._get_sleeping_activ(True):
- return getattr(self._config.parameter.door.sleeping if self._config.parameter.door else None, "brightness", None)
- if self._config.items.day.is_on():
- return getattr(self._config.parameter.door.day if self._config.parameter.door else None, "brightness", None)
- return getattr(self._config.parameter.door.night if self._config.parameter.door else None, "brightness", None)
-
- return _LightBase._get_target_brightness(self)
-
- def _door_configured(self) -> bool:
- """Check whether door functionality is configured for the current day/night state
-
- :return: True if door functionality is configured
- """
- if not self._config.items.doors:
- return False
- return bool(self._timeout_door)
-
- def _door_off_leaving_configured(self) -> bool:
- """Check whether door-off functionality is configured for the current day/night state
-
- :return: True if door-off is configured
- """
- return self._config.parameter.off_at_door_closed_during_leaving
-
- def _motion_configured(self) -> bool:
- """Check whether motion functionality is configured for the current day/night state
-
- :return: True if motion functionality is configured
- """
- if self._config.items.motion is None:
- return False
- return bool(self._timeout_motion)
-
- def _motion_door_allowed(self) -> bool:
- """Check if transition to motion and door state is allowed
-
- :return: True if transition is allowed
- """
- return time.time() - self._hand_off_timestamp > self._hand_off_lock_time
-
- def _cb_hand_off(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered by the state observer if a manual OFF command was detected.
-
- :param event: original trigger event
- """
- self._hand_off_timestamp = time.time()
- _LightBase._cb_hand_off(self, event)
-
- def _cb_motion(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the motion state changed.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.motion_on()
- else:
- self.motion_off()
-
- def _cb_door(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if a door state changed.
-
- :param event: trigger event
- """
- if event.value == "OPEN":
- # every open of a single door calls door_opened()
- self.door_opened()
-
- if event.value == "CLOSED" and all(door.is_closed() for door in self._config.items.doors):
- # only if all doors are closed door_closed() is called
- self.door_closed()
-
- def _log_motion_timeout_warning(self):
- """Log warning if motion state was left because of timeout."""
- self._instance_logger.warning("Timeout of motion was triggered, before motion stopped. Thing about to increase motion timeout!")
-
-
-# pylint: disable=protected-access
+ """Mixin class for adding door and motion functionality."""
+
+ states: dict
+ trans: list
+ state: str
+ _config: habapp_rules.actors.config.light.LightConfig
+ state_machine: habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout
+ _get_sleeping_activ: typing.Callable[[bool | None], bool]
+
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init mixin class.
+
+ Args:
+ config: light config
+ """
+ self.states = _LightExtendedMixin._add_additional_states(self.states)
+ self.trans = _LightExtendedMixin._add_additional_transitions(self.trans)
+
+ self._timeout_motion = 0
+ self._timeout_door = 0
+
+ self._hand_off_lock_time = config.parameter.hand_off_lock_time
+ self._hand_off_timestamp = 0
+
+ @staticmethod
+ def _add_additional_states(states_dict: dict) -> dict:
+ """Add additional states for door and motion.
+
+ Args:
+ states_dict: current state dictionary
+
+ Returns:
+ current + new states
+ """
+ states_dict = copy.deepcopy(states_dict)
+ states_dict[1]["children"].append({"name": "door", "timeout": 999, "on_timeout": "door_timeout"})
+ states_dict[1]["children"].append({"name": "motion", "timeout": 999, "on_timeout": "motion_timeout"})
+ return states_dict
+
+ @staticmethod
+ def _add_additional_transitions(transitions_list: list[dict]) -> list[dict]:
+ """Add additional transitions for door and motion.
+
+ Args:
+ transitions_list: current transitions
+
+ Returns:
+ current + new transitions
+ """
+ transitions_list = copy.deepcopy(transitions_list)
+
+ transitions_list.append({"trigger": "motion_on", "source": "auto_door", "dest": "auto_motion", "conditions": "_motion_configured"})
+ transitions_list.append({"trigger": "motion_on", "source": "auto_off", "dest": "auto_motion", "conditions": ["_motion_configured", "_motion_door_allowed"]})
+ transitions_list.append({"trigger": "motion_on", "source": "auto_preoff", "dest": "auto_motion", "conditions": "_motion_configured"})
+ transitions_list.append({"trigger": "motion_off", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured"})
+ transitions_list.append({"trigger": "motion_off", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured"})
+ transitions_list.append({"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured", "before": "_log_motion_timeout_warning"})
+ transitions_list.append({"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured", "before": "_log_motion_timeout_warning"})
+ transitions_list.append({"trigger": "hand_off", "source": "auto_motion", "dest": "auto_off"})
+
+ transitions_list.append({"trigger": "door_opened", "source": ["auto_off", "auto_preoff", "auto_door"], "dest": "auto_door", "conditions": ["_door_configured", "_motion_door_allowed"]})
+ transitions_list.append({"trigger": "door_timeout", "source": "auto_door", "dest": "auto_preoff", "conditions": "_pre_off_configured"})
+ transitions_list.append({"trigger": "door_timeout", "source": "auto_door", "dest": "auto_off", "unless": "_pre_off_configured"})
+ transitions_list.append({"trigger": "door_closed", "source": "auto_leaving", "dest": "auto_off", "conditions": "_door_off_leaving_configured"})
+ transitions_list.append({"trigger": "hand_off", "source": "auto_door", "dest": "auto_off"})
+
+ transitions_list.append({"trigger": "leaving_started", "source": ["auto_motion", "auto_door"], "dest": "auto_leaving", "conditions": "_leaving_configured"})
+ transitions_list.append({"trigger": "sleep_started", "source": ["auto_motion", "auto_door"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"})
+
+ return transitions_list
+
+ def add_additional_callbacks(self) -> None:
+ """Add additional callbacks for motion and door items."""
+ if self._config.items.motion is not None:
+ self._config.items.motion.listen_event(self._cb_motion, HABApp.openhab.events.ItemStateChangedEventFilter())
+ for item_door in self._config.items.doors:
+ item_door.listen_event(self._cb_door, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ def _get_initial_state(self, default_value: str = "") -> str:
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ initial_state = _LightBase._get_initial_state(self, default_value) # noqa: SLF001
+
+ if initial_state == "auto_on" and self._config.items.motion is not None and self._config.items.motion.is_on() and self._motion_configured():
+ initial_state = "auto_motion"
+ return initial_state
+
+ def _set_timeouts(self) -> None:
+ """Set timeouts depending on the current day/night/sleep state."""
+ _LightBase._set_timeouts(self) # noqa: SLF001
+
+ # set timeouts of additional states
+ if self._get_sleeping_activ():
+ self._timeout_motion = getattr(self._config.parameter.motion.sleeping if self._config.parameter.motion else 0, "timeout", 0)
+ self._timeout_door = getattr(self._config.parameter.door.sleeping if self._config.parameter.door else 0, "timeout", 0)
+
+ elif self._config.items.day.is_on():
+ self._timeout_motion = getattr(self._config.parameter.motion.day if self._config.parameter.motion else 0, "timeout", 0)
+ self._timeout_door = getattr(self._config.parameter.door.day if self._config.parameter.door else 0, "timeout", 0)
+ else:
+ self._timeout_motion = getattr(self._config.parameter.motion.night if self._config.parameter.motion else 0, "timeout", 0)
+ self._timeout_door = getattr(self._config.parameter.door.night if self._config.parameter.door else 0, "timeout", 0)
+
+ self.state_machine.states["auto"].states["motion"].timeout = self._timeout_motion
+ self.state_machine.states["auto"].states["door"].timeout = self._timeout_door
+
+ def _get_target_brightness(self) -> bool | float | None:
+ """Get configured brightness for the current day/night/sleep state. Must be called before _get_target_brightness of base class.
+
+ Returns:
+ configured brightness value
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesError: if current state is not supported
+ """
+ if self.state == "auto_motion":
+ if self._get_sleeping_activ(include_pre_sleep=True):
+ return getattr(self._config.parameter.motion.sleeping if self._config.parameter.motion else None, "brightness", None)
+ if self._config.items.day.is_on():
+ return getattr(self._config.parameter.motion.day if self._config.parameter.motion else None, "brightness", None)
+ return getattr(self._config.parameter.motion.night if self._config.parameter.motion else None, "brightness", None)
+
+ if self.state == "auto_door":
+ if self._get_sleeping_activ(include_pre_sleep=True):
+ return getattr(self._config.parameter.door.sleeping if self._config.parameter.door else None, "brightness", None)
+ if self._config.items.day.is_on():
+ return getattr(self._config.parameter.door.day if self._config.parameter.door else None, "brightness", None)
+ return getattr(self._config.parameter.door.night if self._config.parameter.door else None, "brightness", None)
+
+ return _LightBase._get_target_brightness(self) # noqa: SLF001
+
+ def _door_configured(self) -> bool:
+ """Check whether door functionality is configured for the current day/night state.
+
+ Returns:
+ True if door functionality is configured
+ """
+ if not self._config.items.doors:
+ return False
+ return bool(self._timeout_door)
+
+ def _door_off_leaving_configured(self) -> bool:
+ """Check whether door-off functionality is configured for the current day/night state.
+
+ Returns:
+ True if door-off is configured
+ """
+ return self._config.parameter.off_at_door_closed_during_leaving
+
+ def _motion_configured(self) -> bool:
+ """Check whether motion functionality is configured for the current day/night state.
+
+ Returns:
+ True if motion functionality is configured
+ """
+ if self._config.items.motion is None:
+ return False
+ return bool(self._timeout_motion)
+
+ def _motion_door_allowed(self) -> bool:
+ """Check if transition to motion and door state is allowed.
+
+ Returns:
+ True if transition is allowed
+ """
+ return time.time() - self._hand_off_timestamp > self._hand_off_lock_time
+
+ def _cb_hand_off(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is triggered by the state observer if a manual OFF command was detected.
+
+ Args:
+ event: original trigger event
+ """
+ self._hand_off_timestamp = time.time()
+ _LightBase._cb_hand_off(self, event) # noqa: SLF001
+
+ def _cb_motion(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the motion state changed.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.motion_on()
+ else:
+ self.motion_off()
+
+ def _cb_door(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if a door state changed.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "OPEN":
+ # every open of a single door calls door_opened()
+ self.door_opened()
+
+ if event.value == "CLOSED" and all(door.is_closed() for door in self._config.items.doors):
+ # only if all doors are closed door_closed() is called
+ self.door_closed()
+
+ def _log_motion_timeout_warning(self) -> None:
+ """Log warning if motion state was left because of timeout."""
+ self._instance_logger.warning("Timeout of motion was triggered, before motion stopped. Thing about to increase motion timeout!")
+
+
class LightSwitchExtended(_LightExtendedMixin, LightSwitch):
- """Extended Light.
+ """Extended Light.
- Example config is given at Light base class.
- With this class additionally motion or door items can be given.
- """
+ Example config is given at Light base class.
+ With this class additionally motion or door items can be given.
+ """
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init of extended light object.
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init of extended light object.
- :param config: light config
- """
- _LightExtendedMixin.__init__(self, config)
- LightSwitch.__init__(self, config)
+ Args:
+ config: light config
+ """
+ _LightExtendedMixin.__init__(self, config)
+ LightSwitch.__init__(self, config)
- _LightExtendedMixin.add_additional_callbacks(self)
+ _LightExtendedMixin.add_additional_callbacks(self)
-# pylint: disable=protected-access
class LightDimmerExtended(_LightExtendedMixin, LightDimmer):
- """Extended Light.
+ """Extended Light.
- Example config is given at Light base class.
- With this class additionally motion or door items can be given.
- """
+ Example config is given at Light base class.
+ With this class additionally motion or door items can be given.
+ """
- # pylint:disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
- """Init of extended light object.
+ def __init__(self, config: habapp_rules.actors.config.light.LightConfig) -> None:
+ """Init of extended light object.
- :param config: light config
- """
- _LightExtendedMixin.__init__(self, config)
- LightDimmer.__init__(self, config)
+ Args:
+ config: light config
+ """
+ _LightExtendedMixin.__init__(self, config)
+ LightDimmer.__init__(self, config)
- _LightExtendedMixin.add_additional_callbacks(self)
+ _LightExtendedMixin.add_additional_callbacks(self)
diff --git a/habapp_rules/actors/light_hcl.py b/habapp_rules/actors/light_hcl.py
index 624ab2d..ca73e74 100644
--- a/habapp_rules/actors/light_hcl.py
+++ b/habapp_rules/actors/light_hcl.py
@@ -1,7 +1,9 @@
"""Light HCL rules."""
+
import abc
import datetime
import logging
+import typing
import HABApp
@@ -13,334 +15,340 @@
import habapp_rules.core.state_machine_rule
import habapp_rules.core.type_of_day
import habapp_rules.system
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member
class _HclBase(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Base class for HCL rules."""
-
- states = [
- {"name": "Manual"},
- {"name": "Hand", "timeout": 99, "on_timeout": "hand_timeout"},
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "HCL"},
- {"name": "Sleep", "initial": "Active", "children": [
- {"name": "Active"},
- {"name": "Post", "timeout": 1, "on_timeout": "post_sleep_timeout"}
- ]},
- {"name": "Focus"}
- ]}
- ]
-
- trans = [
- {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
- {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
-
- {"trigger": "hand_on", "source": "Auto", "dest": "Hand"},
- {"trigger": "hand_timeout", "source": "Hand", "dest": "Auto"},
-
- {"trigger": "sleep_start", "source": ["Auto_HCL", "Auto_Focus"], "dest": "Auto_Sleep"},
- {"trigger": "sleep_end", "source": "Auto_Sleep_Active", "dest": "Auto_Sleep_Post"},
- {"trigger": "post_sleep_timeout", "source": "Auto_Sleep_Post", "dest": "Auto_HCL"},
-
- {"trigger": "focus_start", "source": ["Auto_HCL", "Auto_Sleep"], "dest": "Auto_Focus"},
- {"trigger": "focus_end", "source": "Auto_Focus", "dest": "Auto_HCL"},
- ]
-
- def __init__(self, config: habapp_rules.actors.config.light_hcl.HclElevationConfig | habapp_rules.actors.config.light_hcl.HclTimeConfig) -> None:
- """Init base class.
-
- :param config: config of HCL light.
- """
- self._config = config
-
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.color.name)
-
- self._state_observer = habapp_rules.actors.state_observer.StateObserverNumber(self._config.items.color.name, self._cb_hand, value_tolerance=10)
-
- # init state machine
- self._previous_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- self._set_timeouts()
-
- # set callbacks
- self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.sleep_state is not None:
- self._config.items.sleep_state.listen_event(self._cb_sleep, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.focus is not None:
- self._config.items.focus.listen_event(self._cb_focus, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.switch_on is not None:
- self._config.items.switch_on.listen_event(self._cb_switch_on, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- def _set_timeouts(self) -> None:
- """Set timeouts."""
- self.state_machine.get_state("Auto_Sleep_Post").timeout = self._config.parameter.post_sleep_timeout
- self.state_machine.get_state("Hand").timeout = self._config.parameter.hand_timeout
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.manual.is_on():
- return "Manual"
- if self._config.items.sleep_state is not None and self._config.items.sleep_state.value in (habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value):
- return "Auto_Sleep"
- if self._config.items.focus is not None and self._config.items.focus.is_on():
- return "Auto_Focus"
- return "Auto_HCL"
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This should method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_light_color()
- self._previous_state = self.state
-
- def _set_light_color(self):
- """Set light color."""
- target_color = None
-
- if self.state == "Auto_HCL":
- target_color = self._get_hcl_color()
- elif self.state == "Auto_Focus":
- target_color = self._config.parameter.focus_color
- elif self.state == "Auto_Sleep_Active":
- target_color = self._config.parameter.sleep_color
-
- if target_color is not None:
- self._state_observer.send_command(target_color)
-
- def on_enter_Auto_Init(self) -> None: # pylint: disable=invalid-name
- """Is called on entering of init state"""
- self._set_initial_state()
-
- @abc.abstractmethod
- def _get_hcl_color(self) -> int | None:
- """Get HCL color.
-
- :return: HCL light color
- """
-
- @staticmethod
- def _get_interpolated_value(config_start: tuple[float, float], config_end: tuple[float, float], value: float) -> float:
- """Get interpolated value
-
- :param config_start: start config
- :param config_end: end config
- :param value: input value which is the input for the interpolation
- :return: interpolated value
- """
- fit_m = (config_end[1] - config_start[1]) / (config_end[0] - config_start[0])
- fit_t = config_end[1] - fit_m * config_end[0]
-
- return fit_m * value + fit_t
-
- def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the manual switch has a state change event.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.manual_on()
- else:
- self.manual_off()
-
- def _cb_hand(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered by the state observer if a manual change was detected.
-
- :param event: original trigger event
- """
- self._instance_logger.debug("Hand detected")
- self.hand_on()
-
- def _cb_focus(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the focus switch has a state change event.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.focus_start()
- else:
- self.focus_end()
-
- def _cb_sleep(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the sleep state has a state change event.
-
- :param event: trigger event
- """
- if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
- self.sleep_start()
- if self._config.items.focus is not None and self._config.items.focus.is_on():
- self._config.items.focus.oh_send_command("OFF")
- elif event.value == habapp_rules.system.SleepState.AWAKE.value:
- self.sleep_end()
-
- def _cb_switch_on(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the switch_on_item has a state change event.
-
- :param event: trigger event
- """
- if self.state == "Auto_HCL" and event.value == "ON" or isinstance(event.value, (int, float)) and event.value > 0:
- if (target_color := self._get_hcl_color()) is not None:
- self.run.at(1, self._state_observer.send_command, target_color)
+ """Base class for HCL rules."""
+
+ states: typing.ClassVar = [
+ {"name": "Manual"},
+ {"name": "Hand", "timeout": 99, "on_timeout": "hand_timeout"},
+ {
+ "name": "Auto",
+ "initial": "Init",
+ "children": [{"name": "Init"}, {"name": "HCL"}, {"name": "Sleep", "initial": "Active", "children": [{"name": "Active"}, {"name": "Post", "timeout": 1, "on_timeout": "post_sleep_timeout"}]}, {"name": "Focus"}],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
+ {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
+ {"trigger": "hand_on", "source": "Auto", "dest": "Hand"},
+ {"trigger": "hand_timeout", "source": "Hand", "dest": "Auto"},
+ {"trigger": "sleep_start", "source": ["Auto_HCL", "Auto_Focus"], "dest": "Auto_Sleep"},
+ {"trigger": "sleep_end", "source": "Auto_Sleep_Active", "dest": "Auto_Sleep_Post"},
+ {"trigger": "post_sleep_timeout", "source": "Auto_Sleep_Post", "dest": "Auto_HCL"},
+ {"trigger": "focus_start", "source": ["Auto_HCL", "Auto_Sleep"], "dest": "Auto_Focus"},
+ {"trigger": "focus_end", "source": "Auto_Focus", "dest": "Auto_HCL"},
+ ]
+
+ def __init__(self, config: habapp_rules.actors.config.light_hcl.HclElevationConfig | habapp_rules.actors.config.light_hcl.HclTimeConfig) -> None:
+ """Init base class.
+
+ Args:
+ config: config of HCL light.
+ """
+ self._config = config
+
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.color.name)
+
+ self._state_observer = habapp_rules.actors.state_observer.StateObserverNumber(self._config.items.color.name, self._cb_hand, value_tolerance=10)
+
+ # init state machine
+ self._previous_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ self._set_timeouts()
+
+ # set callbacks
+ self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.sleep_state is not None:
+ self._config.items.sleep_state.listen_event(self._cb_sleep, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.focus is not None:
+ self._config.items.focus.listen_event(self._cb_focus, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.switch_on is not None:
+ self._config.items.switch_on.listen_event(self._cb_switch_on, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ def _set_timeouts(self) -> None:
+ """Set timeouts."""
+ self.state_machine.get_state("Auto_Sleep_Post").timeout = self._config.parameter.post_sleep_timeout
+ self.state_machine.get_state("Hand").timeout = self._config.parameter.hand_timeout
+
+ def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.manual.is_on():
+ return "Manual"
+ if self._config.items.sleep_state is not None and self._config.items.sleep_state.value in {habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value}:
+ return "Auto_Sleep"
+ if self._config.items.focus is not None and self._config.items.focus.is_on():
+ return "Auto_Focus"
+ return "Auto_HCL"
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This should method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_light_color()
+ self._previous_state = self.state
+
+ def _set_light_color(self) -> None:
+ """Set light color."""
+ target_color = None
+
+ if self.state == "Auto_HCL":
+ target_color = self._get_hcl_color()
+ elif self.state == "Auto_Focus":
+ target_color = self._config.parameter.focus_color
+ elif self.state == "Auto_Sleep_Active":
+ target_color = self._config.parameter.sleep_color
+
+ if target_color is not None:
+ self._state_observer.send_command(target_color)
+
+ def on_enter_Auto_Init(self) -> None: # noqa: N802
+ """Is called on entering of init state."""
+ self._set_initial_state()
+
+ @abc.abstractmethod
+ def _get_hcl_color(self) -> int | None:
+ """Get HCL color.
+
+ Returns:
+ HCL light color
+ """
+
+ @staticmethod
+ def _get_interpolated_value(config_start: tuple[float, float], config_end: tuple[float, float], value: float) -> float:
+ """Get interpolated value.
+
+ Args:
+ config_start: start config
+ config_end: end config
+ value: input value which is the input for the interpolation
+
+ Returns:
+ interpolated value
+ """
+ fit_m = (config_end[1] - config_start[1]) / (config_end[0] - config_start[0])
+ fit_t = config_end[1] - fit_m * config_end[0]
+
+ return fit_m * value + fit_t
+
+ def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the manual switch has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.manual_on()
+ else:
+ self.manual_off()
+
+ def _cb_hand(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered by the state observer if a manual change was detected.
+
+ Args:
+ event: original trigger event
+ """
+ self._instance_logger.debug("Hand detected")
+ self.hand_on()
+
+ def _cb_focus(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the focus switch has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.focus_start()
+ else:
+ self.focus_end()
+
+ def _cb_sleep(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the sleep state has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
+ self.sleep_start()
+ if self._config.items.focus is not None and self._config.items.focus.is_on():
+ self._config.items.focus.oh_send_command("OFF")
+ elif event.value == habapp_rules.system.SleepState.AWAKE.value:
+ self.sleep_end()
+
+ def _cb_switch_on(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the switch_on_item has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if ((self.state == "Auto_HCL" and event.value == "ON") or (isinstance(event.value, int | float) and event.value > 0)) and (target_color := self._get_hcl_color()) is not None:
+ self.run.once(1, self._state_observer.send_command, target_color)
class HclElevation(_HclBase):
- """Sun elevation based HCL.
+ """Sun elevation based HCL.
- # Items:
- Number Elevation "Elevation [%s]" {channel="astro:sun:home:position#elevation"}
- Number HCL_Color_Elevation "HCL Color Elevation"
- Switch HCL_Color_Elevation_manual "HCL Color Elevation manual"
+ # Items:
+ Number Elevation "Elevation [%s]" {channel="astro:sun:home:position#elevation"}
+ Number HCL_Color_Elevation "HCL Color Elevation"
+ Switch HCL_Color_Elevation_manual "HCL Color Elevation manual"
- # Config
- config = habapp_rules.actors.config.light_hcl.HclElevationConfig(
- items=habapp_rules.actors.config.light_hcl.HclElevationItems(
- elevation="Elevation",
- color="HCL_Color_Elevation",
- manual="HCL_Color_Elevation_manual",
- ),
- parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(
- color_map=[(0, 2000), (10, 4000), (30, 5000)]
- )
- )
+ # Config
+ config = habapp_rules.actors.config.light_hcl.HclElevationConfig(
+ items=habapp_rules.actors.config.light_hcl.HclElevationItems(
+ elevation="Elevation",
+ color="HCL_Color_Elevation",
+ manual="HCL_Color_Elevation_manual",
+ ),
+ parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(
+ color_map=[(0, 2000), (10, 4000), (30, 5000)]
+ )
+ )
- # Rule init:
- habapp_rules.actors.light_hcl.HclElevation(config)
- """
+ # Rule init:
+ habapp_rules.actors.light_hcl.HclElevation(config)
+ """
- def __init__(self, config: habapp_rules.actors.config.light_hcl.HclElevationConfig) -> None:
- """Init sun elevation based HCL rule.
+ def __init__(self, config: habapp_rules.actors.config.light_hcl.HclElevationConfig) -> None:
+ """Init sun elevation based HCL rule.
- :param config: config for HCL rule
- """
- _HclBase.__init__(self, config)
- self._config = config
+ Args:
+ config: config for HCL rule
+ """
+ _HclBase.__init__(self, config)
+ self._config = config
- self._config.items.elevation.listen_event(self._cb_elevation, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._cb_elevation(None)
+ self._config.items.elevation.listen_event(self._cb_elevation, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._cb_elevation(None)
- def _get_hcl_color(self) -> int | None:
- """Get HCL color depending on elevation
+ def _get_hcl_color(self) -> int | None:
+ """Get HCL color depending on elevation.
- :return: HCL light color
- """
- elevation = self._config.items.elevation.value
+ Returns:
+ HCL light color
+ """
+ elevation = self._config.items.elevation.value
- if elevation is None:
- return None
+ if elevation is None:
+ return None
- return_value = 0
- if elevation <= self._config.parameter.color_map[0][0]:
- return_value = self._config.parameter.color_map[0][1]
+ return_value = 0
+ if elevation <= self._config.parameter.color_map[0][0]:
+ return_value = self._config.parameter.color_map[0][1]
- elif elevation >= self._config.parameter.color_map[-1][0]:
- return_value = self._config.parameter.color_map[-1][1]
+ elif elevation >= self._config.parameter.color_map[-1][0]:
+ return_value = self._config.parameter.color_map[-1][1]
- else:
- for idx, config_itm in enumerate(self._config.parameter.color_map): # pragma: no cover
- if config_itm[0] <= elevation <= self._config.parameter.color_map[idx + 1][0]:
- return_value = self._get_interpolated_value(config_itm, self._config.parameter.color_map[idx + 1], elevation)
- break
+ else:
+ for idx, config_itm in enumerate(self._config.parameter.color_map): # pragma: no cover
+ if config_itm[0] <= elevation <= self._config.parameter.color_map[idx + 1][0]:
+ return_value = self._get_interpolated_value(config_itm, self._config.parameter.color_map[idx + 1], elevation)
+ break
- return round(return_value)
+ return round(return_value)
- def _cb_elevation(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
- """Callback which is called if elevation changed"""
- if self.state == "Auto_HCL" and self._config.items.elevation.value is not None:
- self._state_observer.send_command(self._get_hcl_color())
+ def _cb_elevation(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
+ """Callback which is called if elevation changed."""
+ if self.state == "Auto_HCL" and self._config.items.elevation.value is not None:
+ self._state_observer.send_command(self._get_hcl_color())
class HclTime(_HclBase):
- """Time based HCL.
- # Items:
- Number HCL_Color_Time "HCL Color Time"
- Switch HCL_Color_Time_manual "HCL Color Time manual"
-
- # Config
- config = habapp_rules.actors.config.light_hcl.HclTimeConfig(
- items=habapp_rules.actors.config.light_hcl.HclTimeItems(
- color="HCL_Color_Time",
- manual="HCL_Color_Time_manual",
- ),
- parameter=habapp_rules.actors.config.light_hcl.HclTimeParameter(
- [(6, 2000), (12, 4000), (20, 3000)],
- )
- )
-
- # Rule init:
- habapp_rules.actors.light_hcl.HclTime(config)
- """
-
- def __init__(self, config: habapp_rules.actors.config.light_hcl.HclTimeConfig) -> None:
- """Init time based HCL rule.
-
- :param config: config for HCL light rule
- """
- _HclBase.__init__(self, config)
- self.run.every(None, 300, self._update_color) # every 5 minutes
-
- def _one_hour_later(self, current_time: datetime.datetime) -> bool:
- """Check if today the color values will be shifted one hour later in the evening
-
- :param current_time: current time
- :return: True if next day is a weekend / holiday day
- """
- if not self._config.parameter.shift_weekend_holiday:
- return False
-
- if current_time.hour > 12 and (habapp_rules.core.type_of_day.is_holiday(1) or habapp_rules.core.type_of_day.is_weekend(1)):
- return True
- if current_time.hour <= 4 and (habapp_rules.core.type_of_day.is_holiday() or habapp_rules.core.type_of_day.is_weekend()):
- return True
- return False
-
- def _get_hcl_color(self) -> int | None:
- """Get HCL color depending on time
-
- :return: HCL light color
- """
- current_time = datetime.datetime.now()
-
- if self._one_hour_later(current_time):
- current_time -= datetime.timedelta(hours=1)
-
- if current_time.hour < self._config.parameter.color_map[0][0]:
- start_config = (self._config.parameter.color_map[-1][0] - 24, self._config.parameter.color_map[-1][1])
- end_config = self._config.parameter.color_map[0]
-
- elif current_time.hour >= self._config.parameter.color_map[-1][0]:
- start_config = self._config.parameter.color_map[-1]
- end_config = (self._config.parameter.color_map[0][0] + 24, self._config.parameter.color_map[0][1])
-
- else:
- for idx, config_itm in enumerate(self._config.parameter.color_map): # pragma: no cover
- if config_itm[0] <= current_time.hour < self._config.parameter.color_map[idx + 1][0]:
- start_config = config_itm
- end_config = self._config.parameter.color_map[idx + 1]
- break
-
- return round(self._get_interpolated_value(start_config, end_config, current_time.hour + current_time.minute / 60))
-
- def _update_color(self) -> None:
- """Callback which is called every 5 minutes"""
- if self.state == "Auto_HCL":
- self._state_observer.send_command(self._get_hcl_color())
+ """Time based HCL.
+
+ # Items:
+ Number HCL_Color_Time "HCL Color Time"
+ Switch HCL_Color_Time_manual "HCL Color Time manual".
+
+ # Config
+ config = habapp_rules.actors.config.light_hcl.HclTimeConfig(
+ items=habapp_rules.actors.config.light_hcl.HclTimeItems(
+ color="HCL_Color_Time",
+ manual="HCL_Color_Time_manual",
+ ),
+ parameter=habapp_rules.actors.config.light_hcl.HclTimeParameter(
+ [(6, 2000), (12, 4000), (20, 3000)],
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.light_hcl.HclTime(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.light_hcl.HclTimeConfig) -> None:
+ """Init time based HCL rule.
+
+ Args:
+ config: config for HCL light rule
+ """
+ _HclBase.__init__(self, config)
+ self.run.at(self.run.trigger.interval(None, 300), self._update_color) # every 5 minutes
+
+ def _one_hour_later(self, current_time: datetime.datetime) -> bool:
+ """Check if today the color values will be shifted one hour later in the evening.
+
+ Args:
+ current_time: current time
+
+ Returns:
+ True if next day is a weekend / holiday day
+ """
+ if not self._config.parameter.shift_weekend_holiday:
+ return False
+
+ if current_time.hour > 12 and (habapp_rules.core.type_of_day.is_holiday(1) or habapp_rules.core.type_of_day.is_weekend(1)): # noqa: PLR2004
+ return True
+ return bool(current_time.hour <= 4 and (habapp_rules.core.type_of_day.is_holiday() or habapp_rules.core.type_of_day.is_weekend())) # noqa: PLR2004
+
+ def _get_hcl_color(self) -> int | None:
+ """Get HCL color depending on time.
+
+ Returns:
+ HCL light color
+ """
+ current_time = datetime.datetime.now(TIMEZONE)
+
+ if self._one_hour_later(current_time):
+ current_time -= datetime.timedelta(hours=1)
+
+ if current_time.hour < self._config.parameter.color_map[0][0]:
+ start_config = (self._config.parameter.color_map[-1][0] - 24, self._config.parameter.color_map[-1][1])
+ end_config = self._config.parameter.color_map[0]
+
+ elif current_time.hour >= self._config.parameter.color_map[-1][0]:
+ start_config = self._config.parameter.color_map[-1]
+ end_config = (self._config.parameter.color_map[0][0] + 24, self._config.parameter.color_map[0][1])
+
+ else:
+ for idx, config_itm in enumerate(self._config.parameter.color_map): # pragma: no cover
+ if config_itm[0] <= current_time.hour < self._config.parameter.color_map[idx + 1][0]:
+ start_config = config_itm
+ end_config = self._config.parameter.color_map[idx + 1]
+ break
+
+ return round(self._get_interpolated_value(start_config, end_config, current_time.hour + current_time.minute / 60))
+
+ def _update_color(self) -> None:
+ """Callback which is called every 5 minutes."""
+ if self.state == "Auto_HCL":
+ self._state_observer.send_command(self._get_hcl_color())
diff --git a/habapp_rules/actors/power.py b/habapp_rules/actors/power.py
deleted file mode 100644
index af4e3bf..0000000
--- a/habapp_rules/actors/power.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""Deprecated"""
-import warnings # pragma: no cover
-
-from habapp_rules.sensors.current_switch import CurrentSwitch # noqa: F401 pylint: disable=unused-import # pragma: no cover
-
-warnings.warn(
- "CurrentSwitch has been moved to 'habapp_rules.sensors.current_switch.py'. Importing it from 'habapp_rules.actors.power.py' is deprecated and will be removed in a future release.",
- DeprecationWarning,
- stacklevel=2
-) # pragma: no cover
diff --git a/habapp_rules/actors/shading.py b/habapp_rules/actors/shading.py
index e6c29e1..8dcf498 100644
--- a/habapp_rules/actors/shading.py
+++ b/habapp_rules/actors/shading.py
@@ -1,7 +1,9 @@
"""Rules to manage shading objects."""
+
import abc
import logging
import time
+import typing
import HABApp
@@ -14,649 +16,672 @@
LOGGER = logging.getLogger(__name__)
+HAND_IGNORE_TIME = 1.5
+
-# pylint: disable=no-member, too-many-instance-attributes, too-many-locals
class _ShadingBase(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Base class for shading objects."""
-
- states = [
- {"name": "WindAlarm"},
- {"name": "Manual"},
- {"name": "Hand", "timeout": 20 * 3600, "on_timeout": "_auto_hand_timeout"},
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "Open"},
- {"name": "DoorOpen", "initial": "Open", "children": [
- {"name": "Open"},
- {"name": "PostOpen", "timeout": 5 * 60, "on_timeout": "_timeout_post_door_open"}
- ]},
- {"name": "NightClose"},
- {"name": "SleepingClose"},
- {"name": "SunProtection"},
- ]}
- ]
-
- trans = [
- # wind alarm
- {"trigger": "_wind_alarm_on", "source": ["Auto", "Hand", "Manual"], "dest": "WindAlarm"},
- {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Manual", "conditions": "_manual_active"},
- {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Auto", "unless": "_manual_active"},
-
- # manual
- {"trigger": "_manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
- {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
-
- # hand
- {"trigger": "_hand_command", "source": ["Auto"], "dest": "Hand"},
- {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
-
- # sun
- {"trigger": "_sun_on", "source": "Auto_Open", "dest": "Auto_SunProtection"},
- {"trigger": "_sun_off", "source": "Auto_SunProtection", "dest": "Auto_Open"},
-
- # sleep
- {"trigger": "_sleep_started", "source": ["Auto_Open", "Auto_NightClose", "Auto_SunProtection", "Auto_DoorOpen"], "dest": "Auto_SleepingClose"},
- {"trigger": "_sleep_started", "source": "Hand", "dest": "Auto"},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_NightClose", "conditions": ["_night_active_and_configured"]},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_Open", "unless": ["_night_active_and_configured", "_sun_protection_active_and_configured"]},
-
- # door
- {"trigger": "_door_open", "source": ["Auto_NightClose", "Auto_SunProtection", "Auto_SleepingClose", "Auto_Open"], "dest": "Auto_DoorOpen"},
- {"trigger": "_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_DoorOpen_Open"},
- {"trigger": "_door_closed", "source": "Auto_DoorOpen_Open", "dest": "Auto_DoorOpen_PostOpen"},
- {"trigger": "_timeout_post_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_Init"},
-
- # night close
- {"trigger": "_night_started", "source": ["Auto_Open", "Auto_SunProtection"], "dest": "Auto_NightClose", "conditions": "_night_active_and_configured"},
- {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
- {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_Open", "unless": ["_sun_protection_active_and_configured"]}
-
- ]
- _state_observer_pos: habapp_rules.actors.state_observer.StateObserverRollerShutter | habapp_rules.actors.state_observer.StateObserverDimmer
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
- """Init of _ShadingBase.
-
- :param config: shading config
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if given config / items are not valid
- """
- self._config = config
- self._set_shading_state_timestamp = 0
-
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.shading_position.name)
-
- # init state machine
- self._previous_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
- self._apply_config()
-
- self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value)
-
- if isinstance(self._config.items.shading_position, HABApp.openhab.items.rollershutter_item.RollershutterItem):
- self._state_observer_pos = habapp_rules.actors.state_observer.StateObserverRollerShutter(
- self._config.items.shading_position.name,
- self._cb_hand,
- [item.name for item in self._config.items.shading_position_control],
- [item.name for item in self._config.items.shading_position_group],
- self._config.parameter.value_tolerance
- )
- else:
- # self._config.items.shading_position is instance of HABApp.openhab.items.dimmer_item.DimmerItem
- self._state_observer_pos = habapp_rules.actors.state_observer.StateObserverDimmer(
- self._config.items.shading_position.name,
- self._cb_hand,
- self._cb_hand,
- self._cb_hand,
- [item.name for item in self._config.items.shading_position_control],
- [item.name for item in self._config.items.shading_position_group],
- self._config.parameter.value_tolerance
- )
-
- # callbacks
- self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.wind_alarm is not None:
- self._config.items.wind_alarm.listen_event(self._cb_wind_alarm, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.sun_protection is not None:
- self._config.items.sun_protection.listen_event(self._cb_sun, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.sleeping_state is not None:
- self._config.items.sleeping_state.listen_event(self._cb_sleep_state, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.night is not None:
- self._config.items.night.listen_event(self._cb_night, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.door is not None:
- self._config.items.door.listen_event(self._cb_door, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._update_openhab_state()
-
- def _apply_config(self) -> None:
- """Apply config to state machine."""
- # set timeouts
- self.state_machine.states["Auto"].states["DoorOpen"].states["PostOpen"].timeout = self._config.parameter.door_post_time
- self.state_machine.states["Manual"].timeout = self._config.parameter.manual_timeout
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.wind_alarm is not None and self._config.items.wind_alarm.is_on():
- return "WindAlarm"
- if self._config.items.manual.is_on():
- return "Manual"
- if self._config.items.door is not None and self._config.items.door.is_open(): # self._item_door.is_open():
- return "Auto_DoorOpen_Open"
- if self._config.items.sleeping_state in (habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value):
- return "Auto_SleepingClose"
- if self._config.items.night is not None and self._config.items.night.is_on() and self._night_active_and_configured():
- return "Auto_NightClose"
- if self._sun_protection_active_and_configured():
- return "Auto_SunProtection"
- return "Auto_Open"
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_shading_state()
-
- if self._config.items.hand_manual_is_active_feedback is not None:
- self._config.items.hand_manual_is_active_feedback.oh_post_update("ON" if self.state in {"Manual", "Hand"} else "OFF")
-
- self._previous_state = self.state
-
- def _set_shading_state(self) -> None:
- """Set shading state"""
- if self._previous_state is None:
- # don't change value if called during init (_previous_state == None)
- return
-
- self._set_shading_state_timestamp = time.time()
- self._apply_target_position(self._get_target_position())
-
- @abc.abstractmethod
- def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
- """Apply target position by sending it via the observer(s).
-
- :param target_position: target position of the shading object
- """
-
- def _get_target_position(self) -> habapp_rules.actors.config.shading.ShadingPosition | None:
- """Get target position for shading object.
-
- :return: target shading position
- """
- if self.state in {"Hand", "Manual"}:
- if self._previous_state == "WindAlarm":
- return self._position_before
- return None
-
- if self.state == "WindAlarm":
- return self._config.parameter.pos_wind_alarm
-
- if self.state == "Auto_Open":
- return self._config.parameter.pos_auto_open
-
- if self.state == "Auto_SunProtection":
- return self._config.parameter.pos_sun_protection
-
- if self.state == "Auto_SleepingClose":
- if self._config.items.night is None:
- return self._config.parameter.pos_sleeping_night
- return self._config.parameter.pos_sleeping_night if self._config.items.night.is_on() else self._config.parameter.pos_sleeping_day
-
- if self.state == "Auto_NightClose":
- if self._config.items.summer is not None and self._config.items.summer.is_on():
- return self._config.parameter.pos_night_close_summer
- return self._config.parameter.pos_night_close_winter
-
- if self.state == "Auto_DoorOpen_Open":
- return self._config.parameter.pos_door_open
-
- return None
-
- def on_enter_Auto_Init(self) -> None: # pylint: disable=invalid-name
- """Is called on entering of init state"""
- self._set_initial_state()
-
- def on_exit_Manual(self) -> None: # pylint: disable=invalid-name
- """Is called if state Manual is left."""
- self._set_position_before()
-
- def on_exit_Hand(self) -> None: # pylint: disable=invalid-name
- """Is called if state Hand is left."""
- self._set_position_before()
-
- def _set_position_before(self) -> None:
- """Set / save position before manual state is entered. This is used to restore the previous position"""
- self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value)
-
- def _manual_active(self) -> bool:
- """Check if manual is active.
-
- :return: True if night is active
- """
- return self._config.items.manual.is_on()
-
- def _sun_protection_active_and_configured(self) -> bool:
- """Check if sun protection is active.
-
- :return: True if night is active
- """
- return self._config.items.sun_protection is not None and self._config.items.sun_protection.is_on() and self._config.parameter.pos_sun_protection is not None
-
- def _night_active_and_configured(self) -> bool:
- """Check if night is active and configured.
+ """Base class for shading objects."""
+
+ states: typing.ClassVar = [
+ {"name": "WindAlarm"},
+ {"name": "Manual"},
+ {"name": "Hand", "timeout": 20 * 3600, "on_timeout": "_auto_hand_timeout"},
+ {
+ "name": "Auto",
+ "initial": "Init",
+ "children": [
+ {"name": "Init"},
+ {"name": "Open"},
+ {"name": "DoorOpen", "initial": "Open", "children": [{"name": "Open"}, {"name": "PostOpen", "timeout": 5 * 60, "on_timeout": "_timeout_post_door_open"}]},
+ {"name": "NightClose"},
+ {"name": "SleepingClose"},
+ {"name": "SunProtection"},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ # wind alarm
+ {"trigger": "_wind_alarm_on", "source": ["Auto", "Hand", "Manual"], "dest": "WindAlarm"},
+ {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Manual", "conditions": "_manual_active"},
+ {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Auto", "unless": "_manual_active"},
+ # manual
+ {"trigger": "_manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
+ {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
+ # hand
+ {"trigger": "_hand_command", "source": ["Auto"], "dest": "Hand"},
+ {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
+ # sun
+ {"trigger": "_sun_on", "source": "Auto_Open", "dest": "Auto_SunProtection"},
+ {"trigger": "_sun_off", "source": "Auto_SunProtection", "dest": "Auto_Open"},
+ # sleep
+ {"trigger": "_sleep_started", "source": ["Auto_Open", "Auto_NightClose", "Auto_SunProtection", "Auto_DoorOpen"], "dest": "Auto_SleepingClose"},
+ {"trigger": "_sleep_started", "source": "Hand", "dest": "Auto"},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_NightClose", "conditions": ["_night_active_and_configured"]},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_Open", "unless": ["_night_active_and_configured", "_sun_protection_active_and_configured"]},
+ # door
+ {"trigger": "_door_open", "source": ["Auto_NightClose", "Auto_SunProtection", "Auto_SleepingClose", "Auto_Open"], "dest": "Auto_DoorOpen"},
+ {"trigger": "_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_DoorOpen_Open"},
+ {"trigger": "_door_closed", "source": "Auto_DoorOpen_Open", "dest": "Auto_DoorOpen_PostOpen"},
+ {"trigger": "_timeout_post_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_Init"},
+ # night close
+ {"trigger": "_night_started", "source": ["Auto_Open", "Auto_SunProtection"], "dest": "Auto_NightClose", "conditions": "_night_active_and_configured"},
+ {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
+ {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_Open", "unless": ["_sun_protection_active_and_configured"]},
+ ]
+ _state_observer_pos: habapp_rules.actors.state_observer.StateObserverRollerShutter | habapp_rules.actors.state_observer.StateObserverDimmer
+
+ def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
+ """Init of _ShadingBase.
+
+ Args:
+ config: shading config
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationException: if given config / items are not valid
+ """
+ self._config = config
+ self._set_shading_state_timestamp = 0
+
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.shading_position.name)
+
+ # init state machine
+ self._previous_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+ self._apply_config()
+
+ self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value)
+
+ if isinstance(self._config.items.shading_position, HABApp.openhab.items.rollershutter_item.RollershutterItem):
+ self._state_observer_pos = habapp_rules.actors.state_observer.StateObserverRollerShutter(
+ self._config.items.shading_position.name, self._cb_hand, [item.name for item in self._config.items.shading_position_control], [item.name for item in self._config.items.shading_position_group], self._config.parameter.value_tolerance
+ )
+ else:
+ # self._config.items.shading_position is instance of HABApp.openhab.items.dimmer_item.DimmerItem
+ self._state_observer_pos = habapp_rules.actors.state_observer.StateObserverDimmer(
+ self._config.items.shading_position.name,
+ self._cb_hand,
+ self._cb_hand,
+ self._cb_hand,
+ [item.name for item in self._config.items.shading_position_control],
+ [item.name for item in self._config.items.shading_position_group],
+ self._config.parameter.value_tolerance,
+ )
+
+ # callbacks
+ self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.wind_alarm is not None:
+ self._config.items.wind_alarm.listen_event(self._cb_wind_alarm, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.sun_protection is not None:
+ self._config.items.sun_protection.listen_event(self._cb_sun, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.sleeping_state is not None:
+ self._config.items.sleeping_state.listen_event(self._cb_sleep_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.night is not None:
+ self._config.items.night.listen_event(self._cb_night, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.door is not None:
+ self._config.items.door.listen_event(self._cb_door, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._update_openhab_state()
+
+ def _apply_config(self) -> None:
+ """Apply config to state machine."""
+ # set timeouts
+ self.state_machine.states["Auto"].states["DoorOpen"].states["PostOpen"].timeout = self._config.parameter.door_post_time
+ self.state_machine.states["Manual"].timeout = self._config.parameter.manual_timeout
+
+ def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.wind_alarm is not None and self._config.items.wind_alarm.is_on():
+ return "WindAlarm"
+ if self._config.items.manual.is_on():
+ return "Manual"
+ if self._config.items.door is not None and self._config.items.door.is_open(): # self._item_door.is_open():
+ return "Auto_DoorOpen_Open"
+ if self._config.items.sleeping_state is not None and self._config.items.sleeping_state.value in {habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value}:
+ return "Auto_SleepingClose"
+ if self._config.items.night is not None and self._config.items.night.is_on() and self._night_active_and_configured():
+ return "Auto_NightClose"
+ if self._sun_protection_active_and_configured():
+ return "Auto_SunProtection"
+ return "Auto_Open"
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_shading_state()
+
+ if self._config.items.hand_manual_is_active_feedback is not None:
+ self._config.items.hand_manual_is_active_feedback.oh_post_update("ON" if self.state in {"Manual", "Hand"} else "OFF")
+
+ self._previous_state = self.state
+
+ def _set_shading_state(self) -> None:
+ """Set shading state."""
+ if self._previous_state is None:
+ # don't change value if called during init (_previous_state == None)
+ return
+
+ self._set_shading_state_timestamp = time.time()
+ self._apply_target_position(self._get_target_position())
+
+ @abc.abstractmethod
+ def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
+ """Apply target position by sending it via the observer(s).
+
+ Args:
+ target_position: target position of the shading object
+ """
+
+ def _get_target_position(self) -> habapp_rules.actors.config.shading.ShadingPosition | None: # noqa: C901
+ """Get target position for shading object.
+
+ Returns:
+ target shading position
+ """
+ if self.state in {"Hand", "Manual"}:
+ if self._previous_state == "WindAlarm":
+ return self._position_before
+ return None
+
+ if self.state == "WindAlarm":
+ return self._config.parameter.pos_wind_alarm
+
+ if self.state == "Auto_Open":
+ return self._config.parameter.pos_auto_open
+
+ if self.state == "Auto_SunProtection":
+ return self._config.parameter.pos_sun_protection
+
+ if self.state == "Auto_SleepingClose":
+ if self._config.items.night is None:
+ return self._config.parameter.pos_sleeping_night
+ return self._config.parameter.pos_sleeping_night if self._config.items.night.is_on() else self._config.parameter.pos_sleeping_day
+
+ if self.state == "Auto_NightClose":
+ if self._config.items.summer is not None and self._config.items.summer.is_on():
+ return self._config.parameter.pos_night_close_summer
+ return self._config.parameter.pos_night_close_winter
+
+ if self.state == "Auto_DoorOpen_Open":
+ return self._config.parameter.pos_door_open
+
+ return None
+
+ def on_enter_Auto_Init(self) -> None: # noqa: N802
+ """Is called on entering of init state."""
+ self._set_initial_state()
+
+ def on_exit_Manual(self) -> None: # noqa: N802
+ """Is called if state Manual is left."""
+ self._set_position_before()
+
+ def on_exit_Hand(self) -> None: # noqa: N802
+ """Is called if state Hand is left."""
+ self._set_position_before()
+
+ def _set_position_before(self) -> None:
+ """Set / save position before manual state is entered. This is used to restore the previous position."""
+ self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value)
+
+ def _manual_active(self) -> bool:
+ """Check if manual is active.
+
+ Returns:
+ True if night is active
+ """
+ return self._config.items.manual.is_on()
+
+ def _sun_protection_active_and_configured(self) -> bool:
+ """Check if sun protection is active.
+
+ Returns:
+ True if night is active
+ """
+ return self._config.items.sun_protection is not None and self._config.items.sun_protection.is_on() and self._config.parameter.pos_sun_protection is not None
+
+ def _night_active_and_configured(self) -> bool:
+ """Check if night is active and configured.
+
+ Returns:
+ True if night is active
+ """
+ night_config = self._config.parameter.pos_night_close_summer if self._config.items.summer is not None and self._config.items.summer.is_on() else self._config.parameter.pos_night_close_winter
+ return self._config.items.night is not None and self._config.items.night.is_on() and night_config is not None
+
+ def _cb_hand(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if a external control was detected.
+
+ Args:
+ event: original trigger event
+ """
+ if time.time() - self._set_shading_state_timestamp > HAND_IGNORE_TIME:
+ # ignore hand commands one second after this rule triggered a position change
+ self._instance_logger.debug(f"Detected hand command. The event was {event}")
+ self._hand_command()
+ else:
+ self._instance_logger.debug(f"Detected hand command, ignoring it. The event was {event}")
+
+ def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if manual mode changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._manual_on()
+ else:
+ self._manual_off()
+
+ def _cb_wind_alarm(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if wind alarm changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._wind_alarm_on()
+ else:
+ self._wind_alarm_off()
+
+ def _cb_sun(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if sun state changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._sun_on()
+ else:
+ self._sun_off()
+
+ def _cb_sleep_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if sleeping state changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
+ self._sleep_started()
+ elif event.value == habapp_rules.system.SleepState.POST_SLEEPING.value:
+ self._sleep_stopped()
+
+ def _cb_night(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if night / dark state changed.
+
+ Args:
+ event: original trigger event
+ """
+ if self.state == "Auto_SleepingClose":
+ target_position = self._config.parameter.pos_sleeping_night if event.value == "ON" else self._config.parameter.pos_sleeping_day
+ self._apply_target_position(target_position)
+
+ if event.value == "ON":
+ self._night_started()
+ else:
+ self._night_stopped()
+
+ def _cb_door(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if door state changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "OPEN":
+ self._door_open()
+ else:
+ self._door_closed()
- :return: True if night is active
- """
- night_config = self._config.parameter.pos_night_close_summer if self._config.items.summer is not None and self._config.items.summer.is_on() else self._config.parameter.pos_night_close_winter
- return self._config.items.night is not None and self._config.items.night.is_on() and night_config is not None
-
- def _cb_hand(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if a external control was detected.
- :param event: original trigger event
- """
- if time.time() - self._set_shading_state_timestamp > 1.5:
- # ignore hand commands one second after this rule triggered a position change
- self._instance_logger.debug(f"Detected hand command. The event was {event}")
- self._hand_command()
- else:
- self._instance_logger.debug(f"Detected hand command, ignoring it. The event was {event}")
-
- def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if manual mode changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._manual_on()
- else:
- self._manual_off()
-
- def _cb_wind_alarm(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if wind alarm changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._wind_alarm_on()
- else:
- self._wind_alarm_off()
-
- def _cb_sun(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if sun state changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._sun_on()
- else:
- self._sun_off()
-
- def _cb_sleep_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if sleeping state changed.
-
- :param event: original trigger event
- """
- if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
- self._sleep_started()
- elif event.value == habapp_rules.system.SleepState.POST_SLEEPING.value:
- self._sleep_stopped()
-
- def _cb_night(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if night / dark state changed.
-
- :param event: original trigger event
- """
- if self.state == "Auto_SleepingClose":
- target_position = self._config.parameter.pos_sleeping_night if event.value == "ON" else self._config.parameter.pos_sleeping_day
- self._apply_target_position(target_position)
-
- if event.value == "ON":
- self._night_started()
- else:
- self._night_stopped()
-
- def _cb_door(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if door state changed.
-
- :param event: original trigger event
- """
- if event.value == "OPEN":
- self._door_open()
- else:
- self._door_closed()
+class Shutter(_ShadingBase):
+ """Rules class to manage a normal shutters (or curtains).
+
+ # KNX-things:
+ Thing device KNX_Shading "KNX OpenHAB dimmer observer"{
+ Type dimmer : shading_position "Shading position" [ position="5.001:4/1/12+<4/1/15" ]
+ Type dimmer-control : shading_position_ctr "Shading position ctr" [ position="5.001:4/1/12+<4/1/15" ]
+ Type dimmer-control : shading_group_all_ctr "Shading all ctr" [ position="5.001:4/1/112+<4/1/115""]
+ Type switch-control : shading_hand_manual_ctr "Shading hand / manual" [ ga="4/1/20" ]
+ }
+
+ # Items:
+ Rollershutter shading_position "Shading position [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position"}
+ Rollershutter shading_position_ctr "Shading position ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position_ctr"}
+ Dimmer shading_slat "Shading slat" {channel="knx:device:bridge:KNX_Shading:shading_slat"}
+ Switch shading_manual "Shading manual"
+ Rollershutter shading_all_ctr "Shading all ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_group_all_ctr"}
+ Switch shading_hand_manual "Shading in Hand / Manual state" {channel="knx:device:bridge:KNX_Shading:shading_hand_manual_ctr"}
+
+ # Config:
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items = habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="shading_position",
+ shading_position_control=["shading_position_ctr", "shading_all_ctr"],
+ slat="shading_slat",
+ manual="shading_manual",
+ wind_alarm="I99_99_WindAlarm",
+ sun_protection="I99_99_SunProtection",
+ sleeping_state="I99_99_Sleeping_State",
+ night="I99_99_Night",
+ door="I99_99_Door",
+ summer="I99_99_Summer",
+ hand_manual_is_active_feedback="shading_hand_manual"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.shading.Shutter(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
+ """Init of Raffstore object.
+
+ Args:
+ config: shading config
+ """
+ _ShadingBase.__init__(self, config)
+
+ self._instance_logger.debug(self.get_initial_log_message())
+
+ def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
+ """Apply target position by sending it via the observer(s).
+
+ Args:
+ target_position: target position of the shading object
+ """
+ if target_position is None:
+ return
+
+ if (position := target_position.position) is not None:
+ self._state_observer_pos.send_command(position)
+ self._instance_logger.debug(f"set position {target_position.position}")
-class Shutter(_ShadingBase):
- """Rules class to manage a normal shutters (or curtains).
-
- # KNX-things:
- Thing device KNX_Shading "KNX OpenHAB dimmer observer"{
- Type dimmer : shading_position "Shading position" [ position="5.001:4/1/12+<4/1/15" ]
- Type dimmer-control : shading_position_ctr "Shading position ctr" [ position="5.001:4/1/12+<4/1/15" ]
- Type dimmer-control : shading_group_all_ctr "Shading all ctr" [ position="5.001:4/1/112+<4/1/115""]
- Type switch-control : shading_hand_manual_ctr "Shading hand / manual" [ ga="4/1/20" ]
- }
-
- # Items:
- Rollershutter shading_position "Shading position [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position"}
- Rollershutter shading_position_ctr "Shading position ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position_ctr"}
- Dimmer shading_slat "Shading slat" {channel="knx:device:bridge:KNX_Shading:shading_slat"}
- Switch shading_manual "Shading manual"
- Rollershutter shading_all_ctr "Shading all ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_group_all_ctr"}
- Switch shading_hand_manual "Shading in Hand / Manual state" {channel="knx:device:bridge:KNX_Shading:shading_hand_manual_ctr"}
-
- # Config:
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items = habapp_rules.actors.config.shading.ShadingItems(
- shading_position="shading_position",
- shading_position_control=["shading_position_ctr", "shading_all_ctr"],
- slat="shading_slat",
- manual="shading_manual",
- wind_alarm="I99_99_WindAlarm",
- sun_protection="I99_99_SunProtection",
- sleeping_state="I99_99_Sleeping_State",
- night="I99_99_Night",
- door="I99_99_Door",
- summer="I99_99_Summer",
- hand_manual_is_active_feedback="shading_hand_manual"
- )
- )
-
- # Rule init:
- habapp_rules.actors.shading.Shutter(config)
- """
-
- # pylint: disable=too-many-arguments,too-many-locals
- def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
- """Init of Raffstore object.
-
- :param config: shading config
- """
- _ShadingBase.__init__(self, config)
-
- self._instance_logger.debug(self.get_initial_log_message())
-
- def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
- """Apply target position by sending it via the observer(s).
-
- :param target_position: target position of the shading object
- """
- if target_position is None:
- return
-
- if (position := target_position.position) is not None:
- self._state_observer_pos.send_command(position)
- self._instance_logger.debug(f"set position {target_position.position}")
-
-
-# pylint: disable=too-many-arguments
class Raffstore(_ShadingBase):
- """Rules class to manage a raffstore.
-
- # KNX-things:
- Thing device KNX_Shading "KNX OpenHAB dimmer observer"{
- Type rollershutter : shading_position "Shading position" [ upDown="4/1/10", stopMove="4/1/11", position="5.001:4/1/12+<4/1/15" ]
- Type rollershutter-control : shading_position_ctr "Shading position ctr" [ upDown="4/1/10", stopMove="4/1/11" ]
- Type dimmer : shading_slat "Shading slat" [ position="5.001:4/1/13+<4/1/16" ]
- Type rollershutter-control : shading_group_all_ctr "Shading all ctr" [ upDown="4/1/110", stopMove="4/1/111"]
- Type switch-control : shading_hand_manual_ctr "Shading hand / manual" [ ga="4/1/20" ]
- }
-
- # Items:
- Rollershutter shading_position "Shading position [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position"}
- Rollershutter shading_position_ctr "Shading position ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position_ctr"}
- Dimmer shading_slat "Shading slat [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_slat"}
- Switch shading_manual "Shading manual"
- Rollershutter shading_all_ctr "Shading all ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_group_all_ctr"}
- Switch shading_hand_manual "Shading in Hand / Manual state" {channel="knx:device:bridge:KNX_Shading:shading_hand_manual_ctr"}
-
- # Config:
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items = habapp_rules.actors.config.shading.ShadingItems(
- shading_position="shading_position",
- shading_position_control=["shading_position_ctr", "shading_all_ctr"],
- manual="shading_manual",
- wind_alarm="I99_99_WindAlarm",
- sun_protection="I99_99_SunProtection",
- sleeping_state="I99_99_Sleeping_State",
- night="I99_99_Night",
- door="I99_99_Door",
- summer="I99_99_Summer",
- hand_manual_is_active_feedback="shading_hand_manual"
- )
- )
-
- # Rule init:
- habapp_rules.actors.shading.Raffstore(config)
- """
-
- # pylint: disable=too-many-locals
- def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
- """Init of Raffstore object.
-
- :param config: shading config
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if the correct items are given for sun protection mode
- """
- # check if the correct items are given for sun protection mode
- if (config.items.sun_protection is None) != (config.items.sun_protection_slat is None):
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException("Ether items.sun_protection AND items.sun_protection_slat item must be given or None of them.")
- if config.items.slat is None:
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException("Item for setting the slat value must be given.")
-
- _ShadingBase.__init__(self, config)
-
- self._state_observer_slat = habapp_rules.actors.state_observer.StateObserverSlat(config.items.slat.name, self._cb_hand, config.parameter.value_tolerance)
-
- # init items
- self.__verify_items()
-
- # callbacks
- if self._config.items.sun_protection_slat is not None:
- self._config.items.sun_protection_slat.listen_event(self._cb_slat_target, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._instance_logger.debug(self.get_initial_log_message())
-
- def __verify_items(self) -> None:
- """Check if given items are valid
-
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if given items are not valid
- """
- # check type of rollershutter item
- if not isinstance(self._config.items.shading_position, HABApp.openhab.items.rollershutter_item.RollershutterItem):
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"The shading position item must be of type RollershutterItem. Given: {type(self._config.items.shading_position)}")
-
- def _get_target_position(self) -> habapp_rules.actors.config.shading.ShadingPosition | None:
- """Get target position for shading object(s).
-
- :return: target shading position
- """
- target_position = super()._get_target_position()
-
- if self.state == "Auto_SunProtection" and target_position is not None:
- target_position.slat = self._config.items.sun_protection_slat.value
-
- return target_position
-
- def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
- """Apply target position by sending it via the observer(s).
-
- :param target_position: target position of the shading object
- """
- if target_position is None:
- return
-
- if (position := target_position.position) is not None:
- self._state_observer_pos.send_command(position)
-
- if (slat := target_position.slat) is not None:
- self._state_observer_slat.send_command(slat)
-
- if any(pos is not None for pos in (position, slat)):
- self._instance_logger.debug(f"set position {target_position}")
-
- def _set_position_before(self) -> None:
- """Set / save position before manual state is entered. This is used to restore the previous position"""
- self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value, self._config.items.slat.value)
-
- def _cb_slat_target(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if the target slat value changed.
-
- :param event: original trigger event
- """
- if self.state == "Auto_SunProtection":
- self._state_observer_slat.send_command(event.value)
+ """Rules class to manage a raffstore.
+
+ # KNX-things:
+ Thing device KNX_Shading "KNX OpenHAB dimmer observer"{
+ Type rollershutter : shading_position "Shading position" [ upDown="4/1/10", stopMove="4/1/11", position="5.001:4/1/12+<4/1/15" ]
+ Type rollershutter-control : shading_position_ctr "Shading position ctr" [ upDown="4/1/10", stopMove="4/1/11" ]
+ Type dimmer : shading_slat "Shading slat" [ position="5.001:4/1/13+<4/1/16" ]
+ Type rollershutter-control : shading_group_all_ctr "Shading all ctr" [ upDown="4/1/110", stopMove="4/1/111"]
+ Type switch-control : shading_hand_manual_ctr "Shading hand / manual" [ ga="4/1/20" ]
+ }
+
+ # Items:
+ Rollershutter shading_position "Shading position [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position"}
+ Rollershutter shading_position_ctr "Shading position ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_position_ctr"}
+ Dimmer shading_slat "Shading slat [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_slat"}
+ Switch shading_manual "Shading manual"
+ Rollershutter shading_all_ctr "Shading all ctr [%s %%]" {channel="knx:device:bridge:KNX_Shading:shading_group_all_ctr"}
+ Switch shading_hand_manual "Shading in Hand / Manual state" {channel="knx:device:bridge:KNX_Shading:shading_hand_manual_ctr"}
+
+ # Config:
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items = habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="shading_position",
+ shading_position_control=["shading_position_ctr", "shading_all_ctr"],
+ manual="shading_manual",
+ wind_alarm="I99_99_WindAlarm",
+ sun_protection="I99_99_SunProtection",
+ sleeping_state="I99_99_Sleeping_State",
+ night="I99_99_Night",
+ door="I99_99_Door",
+ summer="I99_99_Summer",
+ hand_manual_is_active_feedback="shading_hand_manual"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.shading.Raffstore(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.shading.ShadingConfig) -> None:
+ """Init of Raffstore object.
+
+ Args:
+ config: shading config
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if the correct items are given for sun protection mode
+ """
+ # check if the correct items are given for sun protection mode
+ if (config.items.sun_protection is None) != (config.items.sun_protection_slat is None):
+ msg = "Ether items.sun_protection AND items.sun_protection_slat item must be given or None of them."
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
+ if config.items.slat is None:
+ msg = "Item for setting the slat value must be given."
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
+
+ _ShadingBase.__init__(self, config)
+
+ self._state_observer_slat = habapp_rules.actors.state_observer.StateObserverSlat(config.items.slat.name, self._cb_hand, config.parameter.value_tolerance)
+
+ # init items
+ self.__verify_items()
+
+ # callbacks
+ if self._config.items.sun_protection_slat is not None:
+ self._config.items.sun_protection_slat.listen_event(self._cb_slat_target, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._instance_logger.debug(self.get_initial_log_message())
+
+ def __verify_items(self) -> None:
+ """Check if given items are valid.
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if given items are not valid
+ """
+ # check type of rollershutter item
+ if not isinstance(self._config.items.shading_position, HABApp.openhab.items.rollershutter_item.RollershutterItem):
+ msg = f"The shading position item must be of type RollershutterItem. Given: {type(self._config.items.shading_position)}"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
+
+ def _get_target_position(self) -> habapp_rules.actors.config.shading.ShadingPosition | None:
+ """Get target position for shading object(s).
+
+ Returns:
+ target shading position
+ """
+ target_position = super()._get_target_position()
+
+ if self.state == "Auto_SunProtection" and target_position is not None:
+ target_position.slat = self._config.items.sun_protection_slat.value
+
+ return target_position
+
+ def _apply_target_position(self, target_position: habapp_rules.actors.config.shading.ShadingPosition) -> None:
+ """Apply target position by sending it via the observer(s).
+
+ Args:
+ target_position: target position of the shading object
+ """
+ if target_position is None:
+ return
+
+ if (position := target_position.position) is not None:
+ self._state_observer_pos.send_command(position)
+
+ if (slat := target_position.slat) is not None:
+ self._state_observer_slat.send_command(slat)
+
+ if any(pos is not None for pos in (position, slat)):
+ self._instance_logger.debug(f"set position {target_position}")
+
+ def _set_position_before(self) -> None:
+ """Set / save position before manual state is entered. This is used to restore the previous position."""
+ self._position_before = habapp_rules.actors.config.shading.ShadingPosition(self._config.items.shading_position.value, self._config.items.slat.value)
+
+ def _cb_slat_target(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if the target slat value changed.
+
+ Args:
+ event: original trigger event
+ """
+ if self.state == "Auto_SunProtection":
+ self._state_observer_slat.send_command(event.value)
class ResetAllManualHand(HABApp.Rule):
- """Clear the state hand / manual state of all shading
+ """Clear the state hand / manual state of all shading.
- # Items:
- Switch clear_hand_manual "Clear Hand / Manual state of all shading objects"
+ # Items:
+ Switch clear_hand_manual "Clear Hand / Manual state of all shading objects"
- # Config
- config = habapp_rules.actors.config.shading.ResetAllManualHandConfig(
- items=habapp_rules.actors.config.shading.ResetAllManualHandItems(
- reset_manual_hand="clear_hand_manual"
- )
- )
+ # Config
+ config = habapp_rules.actors.config.shading.ResetAllManualHandConfig(
+ items=habapp_rules.actors.config.shading.ResetAllManualHandItems(
+ reset_manual_hand="clear_hand_manual"
+ )
+ )
- # Rule init:
- habapp_rules.actors.shading.ResetAllManualHand(config)
- """
+ # Rule init:
+ habapp_rules.actors.shading.ResetAllManualHand(config)
+ """
- def __init__(self, config: habapp_rules.actors.config.shading.ResetAllManualHandConfig) -> None:
- """Init of reset class.
+ def __init__(self, config: habapp_rules.actors.config.shading.ResetAllManualHandConfig) -> None:
+ """Init of reset class.
- :param config: config for reset all manual / hand rule
- """
- self._config = config
- HABApp.Rule.__init__(self)
+ Args:
+ config: config for reset all manual / hand rule
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
- self._config.items.reset_manual_hand.listen_event(self._cb_reset_all, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+ self._config.items.reset_manual_hand.listen_event(self._cb_reset_all, HABApp.openhab.events.ItemStateUpdatedEventFilter())
- def __get_shading_objects(self) -> list[_ShadingBase]:
- """Get all shading objects.
+ def __get_shading_objects(self) -> list[_ShadingBase]:
+ """Get all shading objects.
- :return: list of shading objects
- """
- if self._config.parameter.shading_objects:
- return self._config.parameter.shading_objects
- return [rule for rule in self.get_rule(None) if issubclass(rule.__class__, _ShadingBase)]
+ Returns:
+ list of shading objects
+ """
+ if self._config.parameter.shading_objects:
+ return self._config.parameter.shading_objects
+ return [rule for rule in self.get_rule(None) if issubclass(rule.__class__, _ShadingBase)]
- def _cb_reset_all(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback which is called if reset is requested
+ def _cb_reset_all(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback which is called if reset is requested.
- :param event: trigger event
- """
- if event.value == "OFF":
- return
+ Args:
+ event: trigger event
+ """
+ if event.value == "OFF":
+ return
- for shading_object in self.__get_shading_objects():
- state = shading_object.state
- manual_item = shading_object._config.items.manual # pylint: disable=protected-access
+ for shading_object in self.__get_shading_objects():
+ state = shading_object.state
+ manual_item = shading_object._config.items.manual # noqa: SLF001
- if state == "Manual":
- manual_item.oh_send_command("OFF")
+ if state == "Manual":
+ manual_item.oh_send_command("OFF")
- elif state == "Hand":
- manual_item.oh_send_command("ON")
- manual_item.oh_send_command("OFF")
+ elif state == "Hand":
+ manual_item.oh_send_command("ON")
+ manual_item.oh_send_command("OFF")
- self._config.items.reset_manual_hand.oh_send_command("OFF")
+ self._config.items.reset_manual_hand.oh_send_command("OFF")
class SlatValueSun(HABApp.Rule):
- """Rules class to get slat value depending on sun elevation.
-
- # Items:
- Number elevation "Sun elevation [%s]" {channel="astro...}
- Number sun_protection_slat "Slat value [%s %%]"
-
- # Config
- config = habapp_rules.actors.config.shading.SlatValueConfig(
- items=habapp_rules.actors.config.shading.SlatValueItems(
- sun_elevation="elevation",
- slat_value="sun_protection_slat",
- summer="I99_99_Summer",
- )
- )
-
- # Rule init:
- habapp_rules.actors.shading.SlatValueSun(config)
- """
-
- def __init__(self, config: habapp_rules.actors.config.shading.SlatValueConfig) -> None:
- """Init SlatValueSun
-
- :param config: configuration of slat value
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not valid
- """
- self._config = config
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.slat_value.name)
-
- # slat characteristics
- self._slat_characteristic_active = self._config.parameter.elevation_slat_characteristic_summer if self._config.items.summer is not None and self._config.items.summer.is_on() else self._config.parameter.elevation_slat_characteristic
-
- # callbacks
- self._config.items.sun_elevation.listen_event(self._cb_elevation, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.summer is not None:
- self._config.items.summer.listen_event(self._cb_summer_winter, HABApp.openhab.events.ItemStateChangedEventFilter())
- self.run.soon(self.__send_slat_value)
-
- self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful.")
-
- def __get_slat_value(self, elevation: float) -> float:
- """Get slat value for given elevation.
-
- :param elevation: elevation of the sun
- :return: slat value
- """
- if elevation >= self._slat_characteristic_active[-1].elevation:
- return self._slat_characteristic_active[-1].slat_value
- if elevation < self._slat_characteristic_active[0].elevation:
- return self._slat_characteristic_active[0].slat_value
-
- # no cover because of loop does not finish, but is handled with the two if statements above
- return next(config for idx, config in enumerate(self._slat_characteristic_active) if config.elevation <= elevation < self._slat_characteristic_active[idx + 1].elevation).slat_value # pragma: no cover
-
- def __send_slat_value(self) -> None:
- """Send slat value to OpenHAB item."""
- if self._config.items.sun_elevation.value is None:
- return
- slat_value = self.__get_slat_value(self._config.items.sun_elevation.value)
-
- if self._config.items.slat_value.value != slat_value:
- self._config.items.slat_value.oh_send_command(slat_value)
-
- def _cb_elevation(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is called if sun elevation changed
-
- :param event: elevation event
- """
- self.__send_slat_value()
-
- def _cb_summer_winter(self, event: HABApp.openhab.events.ItemStateChangedEvent):
- """Callback which is called if summer / winter changed
-
- :param event: summer / winter event
- """
- self._slat_characteristic_active = self._config.parameter.elevation_slat_characteristic_summer if event.value == "ON" else self._config.parameter.elevation_slat_characteristic
- self.__send_slat_value()
+ """Rules class to get slat value depending on sun elevation.
+
+ # Items:
+ Number elevation "Sun elevation [%s]" {channel="astro...}
+ Number sun_protection_slat "Slat value [%s %%]"
+
+ # Config
+ config = habapp_rules.actors.config.shading.SlatValueConfig(
+ items=habapp_rules.actors.config.shading.SlatValueItems(
+ sun_elevation="elevation",
+ slat_value="sun_protection_slat",
+ summer="I99_99_Summer",
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.shading.SlatValueSun(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.shading.SlatValueConfig) -> None:
+ """Init SlatValueSun.
+
+ Args:
+ config: configuration of slat value
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not valid
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.slat_value.name)
+
+ # slat characteristics
+ self._slat_characteristic_active = self._config.parameter.elevation_slat_characteristic_summer if self._config.items.summer is not None and self._config.items.summer.is_on() else self._config.parameter.elevation_slat_characteristic
+
+ # callbacks
+ self._config.items.sun_elevation.listen_event(self._cb_elevation, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.summer is not None:
+ self._config.items.summer.listen_event(self._cb_summer_winter, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self.run.soon(self.__send_slat_value)
+
+ self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful.")
+
+ def __get_slat_value(self, elevation: float) -> float:
+ """Get slat value for given elevation.
+
+ Args:
+ elevation: elevation of the sun
+
+ Returns:
+ slat value
+ """
+ if elevation >= self._slat_characteristic_active[-1].elevation:
+ return self._slat_characteristic_active[-1].slat_value
+ if elevation < self._slat_characteristic_active[0].elevation:
+ return self._slat_characteristic_active[0].slat_value
+
+ # no cover because of loop does not finish, but is handled with the two if statements above
+ return next(config for idx, config in enumerate(self._slat_characteristic_active) if config.elevation <= elevation < self._slat_characteristic_active[idx + 1].elevation).slat_value # pragma: no cover
+
+ def __send_slat_value(self) -> None:
+ """Send slat value to OpenHAB item."""
+ if self._config.items.sun_elevation.value is None:
+ return
+ slat_value = self.__get_slat_value(self._config.items.sun_elevation.value)
+
+ if self._config.items.slat_value.value != slat_value:
+ self._config.items.slat_value.oh_send_command(slat_value)
+
+ def _cb_elevation(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
+ """Callback which is called if sun elevation changed.
+
+ Args:
+ event: elevation event
+ """
+ self.__send_slat_value()
+
+ def _cb_summer_winter(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is called if summer / winter changed.
+
+ Args:
+ event: summer / winter event
+ """
+ self._slat_characteristic_active = self._config.parameter.elevation_slat_characteristic_summer if event.value == "ON" else self._config.parameter.elevation_slat_characteristic
+ self.__send_slat_value()
diff --git a/habapp_rules/actors/state_observer.py b/habapp_rules/actors/state_observer.py
index a3819ab..2de0aa0 100644
--- a/habapp_rules/actors/state_observer.py
+++ b/habapp_rules/actors/state_observer.py
@@ -1,4 +1,5 @@
"""Implementations for observing states of switch / dimmer / roller shutter."""
+
from __future__ import annotations
import abc
@@ -16,201 +17,231 @@
LOGGER = logging.getLogger(__name__)
-EventTypes = typing.Union[HABApp.openhab.events.ItemStateChangedEvent, HABApp.openhab.events.ItemCommandEvent]
+EventTypes = HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent
CallbackType = typing.Callable[[EventTypes], None]
class _StateObserverBase(HABApp.Rule, abc.ABC):
- """Base class for observer classes."""
+ """Base class for observer classes."""
+
+ def __init__(self, item_name: str, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0) -> None:
+ """Init state observer for switch item.
+
+ Args:
+ item_name: Name of observed item
+ control_names: list of control items.
+ group_names: list of group items where the item is a part of. Group item type must match with type of item_name
+ value_tolerance: used by all observers which handle numbers. It can be used to allow a difference when comparing new and old values.
+ """
+ self._value_tolerance = value_tolerance
+
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, item_name)
- def __init__(self, item_name: str, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0):
- """Init state observer for switch item.
+ self._last_manual_event = HABApp.openhab.events.ItemCommandEvent("", None)
- :param item_name: Name of observed item
- :param control_names: list of control items.
- :param group_names: list of group items where the item is a part of. Group item type must match with type of item_name
- :param value_tolerance: used by all observers which handle numbers. It can be used to allow a difference when comparing new and old values.
- """
- self._value_tolerance = value_tolerance
+ self._item = HABApp.openhab.items.OpenhabItem.get_item(item_name)
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, item_name)
+ self.__control_items = [HABApp.openhab.items.OpenhabItem.get_item(name) for name in control_names] if control_names else []
+ self.__group_items = [HABApp.openhab.items.OpenhabItem.get_item(name) for name in group_names] if group_names else []
+ self.__check_item_types()
- self._last_manual_event = HABApp.openhab.events.ItemCommandEvent("", None)
+ self._value = self._item.value
+ self._group_last_event = 0
- self._item = HABApp.openhab.items.OpenhabItem.get_item(item_name)
+ self._item.listen_event(self._cb_item, HABApp.openhab.events.ItemStateChangedEventFilter())
+ 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())
- self.__control_items = [HABApp.openhab.items.OpenhabItem.get_item(name) for name in control_names] if control_names else []
- self.__group_items = [HABApp.openhab.items.OpenhabItem.get_item(name) for name in group_names] if group_names else []
- self.__check_item_types()
+ @property
+ def value(self) -> float | bool:
+ """Get the current state / value of the observed item.
- self._value = self._item.value
- self._group_last_event = 0
+ Returns:
+ Current value of the observed item
+ """
+ return self._value
- self._item.listen_event(self._cb_item, HABApp.openhab.events.ItemStateChangedEventFilter())
- 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 last_manual_event(self) -> EventTypes:
+ """Get the last manual event.
- @property
- def value(self) -> float | bool:
- """Get the current state / value of the observed item."""
- return self._value
+ Returns:
+ Last manual event
+ """
+ return self._last_manual_event
- @property
- def last_manual_event(self) -> EventTypes:
- """Get the last manual event."""
- return self._last_manual_event
+ def __check_item_types(self) -> None:
+ """Check if all command and control items have the correct type.
- def __check_item_types(self) -> None:
- """Check if all command and control items have the correct type.
+ Raises:
+ TypeError: if one item has the wrong type
+ """
+ target_type = type(self._item)
- :raises TypeError: if one item has the wrong type"""
- target_type = type(self._item)
+ wrong_types = [f"{item.name} <{type(item).__name__}>" for item in self.__control_items + self.__group_items if not isinstance(item, target_type)]
- wrong_types = []
- for item in self.__control_items + self.__group_items:
- if not isinstance(item, target_type):
- wrong_types.append(f"{item.name} <{type(item).__name__}>")
+ if wrong_types:
+ self._instance_logger.error(msg := f"Found items with wrong item type. Expected: {target_type.__name__}. Wrong: {' | '.join(wrong_types)}")
+ raise TypeError(msg)
- if wrong_types:
- self._instance_logger.error(msg := f"Found items with wrong item type. Expected: {target_type.__name__}. Wrong: {' | '.join(wrong_types)}")
- raise TypeError(msg)
+ @abc.abstractmethod
+ def send_command(self, value: float | str) -> None:
+ """Send brightness command to light (this should be used by rules, to not trigger a manual action).
- @abc.abstractmethod
- def send_command(self, value: float | str) -> None:
- """Send brightness command to light (this should be used by rules, to not trigger a manual action)
+ Args:
+ value: Value to send to the light
- :param value: Value to send to the light
- :raises ValueError: if value has wrong format
- """
+ Raises:
+ ValueError: if value has wrong format
+ """
- def _cb_item(self, event: HABApp.openhab.events.ItemStateChangedEvent):
- """Callback, which is called if a value change of the light item was detected.
+ def _cb_item(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if a value change of the light item was detected.
- :param event: event, which triggered this callback
- """
- self._check_manual(event)
+ Args:
+ event: event, which triggered this callback
+ """
+ self._check_manual(event)
- def _cb_group_item(self, event: HABApp.openhab.events.ItemStateUpdatedEvent):
- """Callback, which is called if a value change of the light item was detected.
+ def _cb_group_item(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
+ """Callback, which is called if a value change of the light item was detected.
- :param event: event, which triggered this callback
- """
- if event.value in {"ON", "OFF"} and time.time() - self._group_last_event > 0.3: # this is some kind of workaround. For some reason all events are doubled.
- self._group_last_event = time.time()
- self._check_manual(event)
+ Args:
+ event: event, which triggered this callback
+ """
+ if event.value in {"ON", "OFF"} and time.time() - self._group_last_event > 0.3: # this is some kind of workaround. For some reason all events are doubled. # noqa: PLR2004
+ self._group_last_event = time.time()
+ self._check_manual(event)
- @abc.abstractmethod
- def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent):
- """Callback, which is called if a command event of one of the control items was detected.
+ @abc.abstractmethod
+ def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is called if a command event of one of the control items was detected.
- :param event: event, which triggered this callback
- """
+ Args:
+ event: event, which triggered this callback
+ """
- @abc.abstractmethod
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Check if light was triggered by a manual action
+ @abc.abstractmethod
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
- :param event: event which triggered this method. This will be forwarded to the callback
- :raises ValueError: if event is not supported
- """
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
- def _trigger_callback(self, cb_name: str, event: EventTypes) -> None:
- """Trigger a manual detected callback.
+ Raises:
+ ValueError: if event is not supported
+ """
- :param cb_name: name of callback method
- :param event: event which triggered the callback
- """
- self._last_manual_event = event
- callback: CallbackType = getattr(self, cb_name)
- callback(event)
+ def _trigger_callback(self, cb_name: str, event: EventTypes) -> None:
+ """Trigger a manual detected callback.
- def _values_different_with_tolerance(self, value_1: float, value_2: float) -> bool:
- """Check if values are different, including the difference.
+ Args:
+ cb_name: name of callback method
+ event: event which triggered the callback
+ """
+ self._last_manual_event = event
+ callback: CallbackType = getattr(self, cb_name)
+ callback(event)
- :param value_1: first value
- :param value_2: second value
- :return: true if values are different (including the tolerance), false if not
- """
- return abs((value_1 or 0) - (value_2 or 0)) > self._value_tolerance
+ def _values_different_with_tolerance(self, value_1: float, value_2: float) -> bool:
+ """Check if values are different, including the difference.
+
+ Args:
+ value_1: first value
+ value_2: second value
+
+ Returns:
+ true if values are different (including the tolerance), false if not
+ """
+ return abs((value_1 or 0) - (value_2 or 0)) > self._value_tolerance
class StateObserverSwitch(_StateObserverBase):
- """Class to observe the on/off state of a switch item.
+ """Class to observe the on/off state of a switch item.
- This class is normally not used standalone. Anyway here is an example config:
+ This class is normally not used standalone. Anyway here is an example config:
- # KNX-things:
- Thing device T00_99_OpenHab_DimmerSwitch "KNX OpenHAB switch observer"{
+ # KNX-things:
+ Thing device T00_99_OpenHab_DimmerSwitch "KNX OpenHAB switch observer"{
Type switch : switch "Switch" [ switch="1/1/10" ]
}
# Items:
- Switch I01_01_Switch "Switch [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerSwitch:switch"}
+ Switch I01_01_Switch "Switch [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerSwitch:switch"}
+
+ # Rule init:
+ habapp_rules.actors.state_observer.StateObserverSwitch("I01_01_Switch", callback_on, callback_off)
+ """
+
+ def __init__(self, item_name: str, cb_on: CallbackType, cb_off: CallbackType) -> None:
+ """Init state observer for switch item.
- # Rule init:
- habapp_rules.actors.state_observer.StateObserverSwitch("I01_01_Switch", callback_on, callback_off)
- """
+ Args:
+ item_name: Name of switch item
+ cb_on: callback which should be called if manual_on was detected
+ cb_off: callback which should be called if manual_off was detected
+ """
+ self._cb_on = cb_on
+ self._cb_off = cb_off
+ _StateObserverBase.__init__(self, item_name)
+ self._value = self._item.value
- def __init__(self, item_name: str, cb_on: CallbackType, cb_off: CallbackType):
- """Init state observer for switch item.
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
- :param item_name: Name of switch item
- :param cb_on: callback which should be called if manual_on was detected
- :param cb_off: callback which should be called if manual_off was detected
- """
- self._cb_on = cb_on
- self._cb_off = cb_off
- _StateObserverBase.__init__(self, item_name)
- self._value = self._item.value
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Check if light was triggered by a manual action
+ Raises:
+ ValueError: if event is not supported
+ """
+ if event.value == "ON" and not self._value:
+ self._value = True
+ self._trigger_callback("_cb_on", event)
- :param event: event which triggered this method. This will be forwarded to the callback
- :raises ValueError: if event is not supported
- """
- if event.value == "ON" and not self._value:
- self._value = True
- self._trigger_callback("_cb_on", event)
+ elif event.value == "OFF" and self._value:
+ self._value = False
+ self._trigger_callback("_cb_off", event)
- elif event.value == "OFF" and self._value:
- self._value = False
- self._trigger_callback("_cb_off", event)
+ def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent) -> None: # not used by StateObserverSwitch
+ """Callback, which is called if a command event of one of the control items was detected.
- def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent): # not used by StateObserverSwitch
- """Callback, which is called if a command event of one of the control items was detected.
+ Args:
+ event: event, which triggered this callback
+ """
- :param event: event, which triggered this callback
- """
+ def send_command(self, value: str) -> None:
+ """Send brightness command to light (this should be used by rules, to not trigger a manual action).
- def send_command(self, value: str) -> None:
- """Send brightness command to light (this should be used by rules, to not trigger a manual action)
+ Args:
+ value: Value to send to the light
- :param value: Value to send to the light
- :raises ValueError: if value has wrong format
- """
- if value == "ON":
- self._value = True
+ Raises:
+ ValueError: if value has wrong format
+ """
+ if value == "ON":
+ self._value = True
- elif value == "OFF":
- self._value = False
- else:
- raise ValueError(f"The given value is not supported for StateObserverSwitch: {value}")
+ elif value == "OFF":
+ self._value = False
+ else:
+ msg = f"The given value is not supported for StateObserverSwitch: {value}"
+ raise ValueError(msg)
- self._item.oh_send_command(value)
+ self._item.oh_send_command(value)
class StateObserverDimmer(_StateObserverBase):
- """Class to observe the on / off / change events of a dimmer item.
+ """Class to observe the on / off / change events of a dimmer item.
- Known limitation: if the items of group_names are KNX-items, the channel types must be dimmer (not dimmer-control)
- This class is normally not used standalone. Anyway here is an example config:
+ Known limitation: if the items of group_names are KNX-items, the channel types must be dimmer (not dimmer-control)
+ This class is normally not used standalone. Anyway here is an example config:
- # KNX-things:
- Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
+ # KNX-things:
+ Thing device T00_99_OpenHab_DimmerObserver "KNX OpenHAB dimmer observer"{
Type dimmer : light "Light" [ switch="1/1/10", position="1/1/13+<1/1/15" ]
Type dimmer-control : light_ctr "Light control" [ increaseDecrease="1/1/12"]
Type dimmer : light_group "Light Group" [ switch="1/1/240", position="1/1/243"]
@@ -218,253 +249,296 @@ class StateObserverDimmer(_StateObserverBase):
# Items:
Dimmer I01_01_Light "Light [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light"}
- Dimmer I01_01_Light_ctr "Light ctr" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_ctr"}
- Dimmer I01_01_Light_group "Light Group" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_group"}
-
- # Rule init:
- habapp_rules.actors.state_observer.StateObserverDimmer(
- "I01_01_Light",
- control_names=["I01_01_Light_ctr"],
- group_names=["I01_01_Light_group"],
- cb_on=callback_on,
- cb_off=callback_off,
- cb_brightness_change=callback_change)
- """
-
- def __init__(self, item_name: str, cb_on: CallbackType, cb_off: CallbackType, cb_brightness_change: CallbackType | None = None, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0) -> None:
- """Init state observer for dimmer item.
-
- :param item_name: Name of dimmer item
- :param cb_on: callback which is called if manual_on was detected
- :param cb_off: callback which is called if manual_off was detected
- :param cb_brightness_change: callback which is called if dimmer is on and brightness changed
- :param control_names: list of control items. They are used to also respond to switch on/off via INCREASE/DECREASE
- :param group_names: list of group items where the item is a part of. Group item type must match with type of item_name
- :param value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
- """
-
- _StateObserverBase.__init__(self, item_name, control_names, group_names, value_tolerance)
-
- self._cb_on = cb_on
- self._cb_off = cb_off
- self._cb_brightness_change = cb_brightness_change
-
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Check if light was triggered by a manual action
-
- :param event: event which triggered this method. This will be forwarded to the callback
- """
- if isinstance(event.value, (int, float)):
- if event.value > 0 and (self._value is None or self._value == 0):
- self._value = event.value
- self._trigger_callback("_cb_on", event)
+ Dimmer I01_01_Light_ctr "Light ctr" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_ctr"}
+ Dimmer I01_01_Light_group "Light Group" {channel="knx:device:bridge:T00_99_OpenHab_DimmerObserver:light_group"}
+
+ # Rule init:
+ habapp_rules.actors.state_observer.StateObserverDimmer(
+ "I01_01_Light",
+ control_names=["I01_01_Light_ctr"],
+ group_names=["I01_01_Light_group"],
+ cb_on=callback_on,
+ cb_off=callback_off,
+ cb_brightness_change=callback_change)
+ """
+
+ def __init__(self, item_name: str, cb_on: CallbackType, cb_off: CallbackType, cb_brightness_change: CallbackType | None = None, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0) -> None:
+ """Init state observer for dimmer item.
+
+ Args:
+ item_name: Name of dimmer item
+ cb_on: callback which is called if manual_on was detected
+ cb_off: callback which is called if manual_off was detected
+ cb_brightness_change: callback which is called if dimmer is on and brightness changed
+ control_names: list of control items. They are used to also respond to switch on/off via INCREASE/DECREASE
+ group_names: list of group items where the item is a part of. Group item type must match with type of item_name
+ value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
+ """
+ _StateObserverBase.__init__(self, item_name, control_names, group_names, value_tolerance)
+
+ self._cb_on = cb_on
+ self._cb_off = cb_off
+ self._cb_brightness_change = cb_brightness_change
+
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
+
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
+ """
+ if isinstance(event.value, int | float):
+ if event.value > 0 and (self._value is None or self._value == 0):
+ self._value = event.value
+ self._trigger_callback("_cb_on", event)
+
+ elif event.value == 0 and (self._value is None or self._value > 0):
+ self._value = 0
+ self._trigger_callback("_cb_off", event)
+
+ elif self._values_different_with_tolerance(event.value, self._value):
+ self._value = event.value
+ self._trigger_callback("_cb_brightness_change", event)
+
+ elif event.value == "ON" and (self._value is None or self._value == 0):
+ self._value = 100
+ self._trigger_callback("_cb_on", event)
+
+ elif event.value == "OFF" and (self._value is None or self._value > 0):
+ self._value = 0
+ self._trigger_callback("_cb_off", event)
+
+ def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is called if a command event of one of the control items was detected.
+
+ Args:
+ event: event, which triggered this callback
+ """
+ if event.value == "INCREASE" and (self._value is None or self._value == 0):
+ self._value = 100
+ self._trigger_callback("_cb_on", event)
+
+ def send_command(self, value: float | str) -> None:
+ """Send brightness command to light (this should be used by rules, to not trigger a manual action).
+
+ Args:
+ value: Value to send to the light
+
+ Raises:
+ ValueError: if value has wrong format
+ """
+ if isinstance(value, int | float):
+ self._value = value
+
+ elif value == "ON":
+ self._value = 100
+
+ elif value == "OFF":
+ self._value = 0
+
+ else:
+ msg = f"The given value is not supported for StateObserverDimmer: {value}"
+ raise ValueError(msg)
+
+ self._item.oh_send_command(value)
- elif event.value == 0 and (self._value is None or self._value > 0):
- self._value = 0
- self._trigger_callback("_cb_off", event)
- elif self._values_different_with_tolerance(event.value, self._value):
- self._value = event.value
- self._trigger_callback("_cb_brightness_change", event)
-
- elif event.value == "ON" and (self._value is None or self._value == 0):
- self._value = 100
- self._trigger_callback("_cb_on", event)
-
- elif event.value == "OFF" and (self._value is None or self._value > 0):
- self._value = 0
- self._trigger_callback("_cb_off", event)
-
- def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent):
- """Callback, which is called if a command event of one of the control items was detected.
-
- :param event: event, which triggered this callback
- """
- if event.value == "INCREASE" and (self._value is None or self._value == 0):
- self._value = 100
- self._trigger_callback("_cb_on", event)
-
- def send_command(self, value: float | str) -> None:
- """Send brightness command to light (this should be used by rules, to not trigger a manual action)
-
- :param value: Value to send to the light
- :raises ValueError: if value has wrong format
- """
- if isinstance(value, (int, float)):
- self._value = value
-
- elif value == "ON":
- self._value = 100
-
- elif value == "OFF":
- self._value = 0
-
- else:
- raise ValueError(f"The given value is not supported for StateObserverDimmer: {value}")
+class StateObserverRollerShutter(_StateObserverBase):
+ """Class to observe manual controls of a roller shutter item.
- self._item.oh_send_command(value)
+ This class is normally not used standalone. Anyway, here is an example config:
+ # KNX-things:
+ Thing device T00_99_OpenHab_RollershutterObserver "KNX OpenHAB rollershutter observer"{
+ Type rollershutter : shading "Shading" [ upDown="1/1/10", position="1/1/13+<1/1/15" ]
+ Type rollershutter-control : shading_ctr "Shading control" [ upDown="1/1/10", position="1/1/13+<1/1/15" ]
+ Type rollershutter : shading_group "Shading Group" [ upDown="1/1/110", position="1/1/113+<1/1/115" ]
+ }
-class StateObserverRollerShutter(_StateObserverBase):
- """Class to observe manual controls of a roller shutter item.
-
- This class is normally not used standalone. Anyway, here is an example config:
-
- # KNX-things:
- Thing device T00_99_OpenHab_RollershutterObserver "KNX OpenHAB rollershutter observer"{
- Type rollershutter : shading "Shading" [ upDown="1/1/10", position="1/1/13+<1/1/15" ]
- Type rollershutter-control : shading_ctr "Shading control" [ upDown="1/1/10", position="1/1/13+<1/1/15" ]
- Type rollershutter : shading_group "Shading Group" [ upDown="1/1/110", position="1/1/113+<1/1/115" ]
- }
-
- # Items:
- Rollershutter I_Rollershutter "Rollershutter [%s]" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter"}
- Rollershutter I_Rollershutter_ctr "Rollershutter ctr" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter_ctr"}
- Rollershutter I_Rollershutter_group "Rollershutter Group" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter_group"}
-
- # Rule init:
- habapp_rules.actors.state_observer.StateObserverRollerShutter(
- "I_Rollershutter",
- control_names=["I_Rollershutter_ctr"],
- group_names=["I_Rollershutter_group"],
- cb_manual=callback_on
- )
- """
-
- def __init__(self, item_name: str, cb_manual: CallbackType, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0) -> None:
- """Init state observer for dimmer item.
-
- :param item_name: Name of dimmer item
- :param cb_manual: callback which is called if a manual interaction was detected
- :param control_names: list of control items. They are used to also respond to switch on/off via INCREASE/DECREASE
- :param group_names: list of group items where the item is a part of. Group item type must match with type of item_name
- :param value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
- """
- self._value_tolerance = value_tolerance
- _StateObserverBase.__init__(self, item_name, control_names, group_names, value_tolerance)
-
- self._cb_manual = cb_manual
-
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- if isinstance(event.value, (int, float)) and self._values_different_with_tolerance(event.value, self._value):
- self._value = event.value
- self._trigger_callback("_cb_manual", event)
-
- def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent):
- if event.value == "DOWN":
- self._value = 100
- self._trigger_callback("_cb_manual", event)
-
- elif event.value == "UP":
- self._value = 0
- self._trigger_callback("_cb_manual", event)
-
- def send_command(self, value: float) -> None:
- if not isinstance(value, (int, float)):
- raise ValueError(f"The given value is not supported for StateObserverDimmer: {value}")
-
- self._value = value
- self._item.oh_send_command(value)
+ # Items:
+ Rollershutter I_Rollershutter "Rollershutter [%s]" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter"}
+ Rollershutter I_Rollershutter_ctr "Rollershutter ctr" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter_ctr"}
+ Rollershutter I_Rollershutter_group "Rollershutter Group" {channel="knx:device:bridge:T00_99_OpenHab_RollershutterObserver:Rollershutter_group"}
+
+ # Rule init:
+ habapp_rules.actors.state_observer.StateObserverRollerShutter(
+ "I_Rollershutter",
+ control_names=["I_Rollershutter_ctr"],
+ group_names=["I_Rollershutter_group"],
+ cb_manual=callback_on
+ )
+ """
+
+ def __init__(self, item_name: str, cb_manual: CallbackType, control_names: list[str] | None = None, group_names: list[str] | None = None, value_tolerance: float = 0) -> None:
+ """Init state observer for dimmer item.
+
+ Args:
+ item_name: Name of dimmer item
+ cb_manual: callback which is called if a manual interaction was detected
+ control_names: list of control items. They are used to also respond to switch on/off via INCREASE/DECREASE
+ group_names: list of group items where the item is a part of. Group item type must match with type of item_name
+ value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
+ """
+ self._value_tolerance = value_tolerance
+ _StateObserverBase.__init__(self, item_name, control_names, group_names, value_tolerance)
+
+ self._cb_manual = cb_manual
+
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
+
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
+
+ Raises:
+ ValueError: if event is not supported
+ """
+ if isinstance(event.value, int | float) and self._values_different_with_tolerance(event.value, self._value):
+ self._value = event.value
+ self._trigger_callback("_cb_manual", event)
+
+ def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is called if a command event of one of the control items was detected.
+
+ Args:
+ event: event, which triggered this callback
+ """
+ if event.value == "DOWN":
+ self._value = 100
+ self._trigger_callback("_cb_manual", event)
+
+ elif event.value == "UP":
+ self._value = 0
+ self._trigger_callback("_cb_manual", event)
+
+ def send_command(self, value: float) -> None:
+ """Send brightness command to light (this should be used by rules, to not trigger a manual action).
+
+ Args:
+ value: Value to send to the light
+
+ Raises:
+ TypeError: if value has wrong format
+ """
+ if not isinstance(value, int | float):
+ msg = f"The given value is not supported for StateObserverDimmer: {value}"
+ raise TypeError(msg)
+
+ self._value = value
+ self._item.oh_send_command(value)
class StateObserverNumber(_StateObserverBase):
- """Class to observe the state of a number item.
+ """Class to observe the state of a number item.
- This class is normally not used standalone. Anyway here is an example config:
+ This class is normally not used standalone. Anyway here is an example config:
- # KNX-things:
- Thing device T00_99_OpenHab_DimmerNumber "KNX OpenHAB number observer"{
+ # KNX-things:
+ Thing device T00_99_OpenHab_DimmerNumber "KNX OpenHAB number observer"{
Type number : number "Switch" [ ga="1/1/10" ]
}
# Items:
- Number I01_01_Number "Switch [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerNumber:number"}
+ Number I01_01_Number "Switch [%s]" {channel="knx:device:bridge:T00_99_OpenHab_DimmerNumber:number"}
+
+ # Rule init:
+ habapp_rules.actors.state_observer.StateObserverNumber("I01_01_Number", callback_value_changed)
+ """
+
+ def __init__(self, item_name: str, cb_manual: CallbackType, value_tolerance: float = 0) -> None:
+ """Init state observer for switch item.
- # Rule init:
- habapp_rules.actors.state_observer.StateObserverNumber("I01_01_Number", callback_value_changed)
- """
+ Args:
+ item_name: Name of switch item
+ cb_manual: callback which should be called if manual change was detected
+ value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
+ """
+ self._cb_manual = cb_manual
+ _StateObserverBase.__init__(self, item_name, value_tolerance=value_tolerance)
- def __init__(self, item_name: str, cb_manual: CallbackType, value_tolerance: float = 0) -> None:
- """Init state observer for switch item.
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
- :param item_name: Name of switch item
- :param cb_manual: callback which should be called if manual change was detected
- :param value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
- """
- self._cb_manual = cb_manual
- _StateObserverBase.__init__(self, item_name, value_tolerance=value_tolerance)
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Check if light was triggered by a manual action
+ Raises:
+ ValueError: if event is not supported
+ """
+ if self._value is None:
+ self._value = event.value
+ return
- :param event: event which triggered this method. This will be forwarded to the callback
- :raises ValueError: if event is not supported
- """
- if self._value is None:
- self._value = event.value
- return
+ if self._values_different_with_tolerance(event.value, self._value):
+ self._value = event.value
+ self._trigger_callback("_cb_manual", event)
- if self._values_different_with_tolerance(event.value, self._value):
- self._value = event.value
- self._trigger_callback("_cb_manual", event)
+ def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent) -> None: # not used by StateObserverNumber
+ """Callback, which is called if a command event of one of the control items was detected.
- def _cb_control_item(self, event: HABApp.openhab.events.ItemCommandEvent): # not used by StateObserverNumber
- """Callback, which is called if a command event of one of the control items was detected.
+ Args:
+ event: event, which triggered this callback
+ """
- :param event: event, which triggered this callback
- """
+ def send_command(self, value: float) -> None:
+ """Send brightness command to light (this should be used by rules, to not trigger a manual action).
- def send_command(self, value: int | float) -> None:
- """Send brightness command to light (this should be used by rules, to not trigger a manual action)
+ Args:
+ value: Value to send to the light
- :param value: Value to send to the light
- :raises ValueError: if value has wrong format
- """
- if not isinstance(value, (int, float)):
- raise ValueError(f"The given value is not supported for StateObserverNumber: {value}")
- self._value = value
- self._item.oh_send_command(value)
+ Raises:
+ TypeError: if value has wrong format
+ """
+ if not isinstance(value, int | float):
+ msg = f"The given value is not supported for StateObserverNumber: {value}"
+ raise TypeError(msg)
+ self._value = value
+ self._item.oh_send_command(value)
class StateObserverSlat(StateObserverNumber):
- """This is only used for the slat value of shading!"""
-
- def __init__(self, item_name: str, cb_manual: CallbackType, value_tolerance: float = 0) -> None:
- """Init state observer for switch item.
-
- :param item_name: Name of switch item
- :param cb_manual: callback which should be called if manual change was detected
- :param value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
- """
- self.__timer_manual: threading.Timer | None = None
- StateObserverNumber.__init__(self, item_name, cb_manual, value_tolerance)
-
- def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Check if light was triggered by a manual action
-
- :param event: event which triggered this method. This will be forwarded to the callback
- :raises ValueError: if event is not supported
- """
- self._stop_timer_manual()
- if event.value in {0, 100}:
- self.__timer_manual = threading.Timer(3, self.__cb_check_manual_delayed, [event])
- self.__timer_manual.start()
- else:
- StateObserverNumber._check_manual(self, event)
-
- def __cb_check_manual_delayed(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Trigger delayed manual check
-
- :param event: event which should be checked
- """
- StateObserverNumber._check_manual(self, event)
-
- def _stop_timer_manual(self) -> None:
- """Stop timer if running."""
- if self.__timer_manual:
- self.__timer_manual.cancel()
- self.__timer_manual = None
-
- def on_rule_removed(self) -> None:
- """Stop timer if rule is removed."""
- self._stop_timer_manual()
+ """This is only used for the slat value of shading!"""
+
+ def __init__(self, item_name: str, cb_manual: CallbackType, value_tolerance: float = 0) -> None:
+ """Init state observer for switch item.
+
+ Args:
+ item_name: Name of switch item
+ cb_manual: callback which should be called if manual change was detected
+ value_tolerance: the tolerance can be used to allow a difference when comparing new and old values.
+ """
+ self.__timer_manual: threading.Timer | None = None
+ StateObserverNumber.__init__(self, item_name, cb_manual, value_tolerance)
+
+ def _check_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Check if light was triggered by a manual action.
+
+ Args:
+ event: event which triggered this method. This will be forwarded to the callback
+
+ Raises:
+ ValueError: if event is not supported
+ """
+ self._stop_timer_manual()
+ if event.value in {0, 100}:
+ self.__timer_manual = threading.Timer(3, self.__cb_check_manual_delayed, [event])
+ self.__timer_manual.start()
+ else:
+ StateObserverNumber._check_manual(self, event) # noqa: SLF001
+
+ def __cb_check_manual_delayed(self, event: HABApp.openhab.events.ItemStateChangedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Trigger delayed manual check.
+
+ Args:
+ event: event which should be checked
+ """
+ StateObserverNumber._check_manual(self, event) # noqa: SLF001
+
+ def _stop_timer_manual(self) -> None:
+ """Stop timer if running."""
+ if self.__timer_manual:
+ self.__timer_manual.cancel()
+ self.__timer_manual = None
+
+ def on_rule_removed(self) -> None:
+ """Stop timer if rule is removed."""
+ self._stop_timer_manual()
diff --git a/habapp_rules/actors/ventilation.py b/habapp_rules/actors/ventilation.py
index f1dbeea..f0b5b29 100644
--- a/habapp_rules/actors/ventilation.py
+++ b/habapp_rules/actors/ventilation.py
@@ -1,8 +1,10 @@
"""Ventilation rules."""
+
import abc
import copy
import datetime
import logging
+import typing
import HABApp
@@ -12,523 +14,540 @@
import habapp_rules.core.logger
import habapp_rules.core.state_machine_rule
import habapp_rules.system
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
+LEVEL_POWER = 2
+
-# pylint: disable=no-member
class _VentilationBase(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Class for ventilation objects."""
-
- states = [
- {"name": "Manual"},
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "Normal"},
- {"name": "PowerHand", "timeout": 3600, "on_timeout": "_hand_off"},
- {"name": "PowerExternal"},
- {"name": "LongAbsence", "initial": "Off", "children": [
- {"name": "On", "timeout": 3600, "on_timeout": "_long_absence_power_off"},
- {"name": "Off"}
- ]},
- ]}
- ]
-
- trans = [
- # manual
- {"trigger": "_manual_on", "source": ["Auto"], "dest": "Manual"},
- {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
-
- # PowerHand
- {"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence"], "dest": "Auto_PowerHand"},
- {"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_PowerExternal", "conditions": "_external_active_and_configured"},
- {"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_Normal", "unless": "_external_active_and_configured"},
-
- # PowerExternal
- {"trigger": "_external_on", "source": "Auto_Normal", "dest": "Auto_PowerExternal"},
- {"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_Normal"},
-
- # long absence
- {"trigger": "_long_absence_on", "source": ["Auto_Normal", "Auto_PowerExternal"], "dest": "Auto_LongAbsence"},
- {"trigger": "_long_absence_power_on", "source": "Auto_LongAbsence_Off", "dest": "Auto_LongAbsence_On"},
- {"trigger": "_long_absence_power_off", "source": "Auto_LongAbsence_On", "dest": "Auto_LongAbsence_Off"},
- {"trigger": "_long_absence_off", "source": "Auto_LongAbsence", "dest": "Auto_Normal"},
- ]
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationConfig) -> None:
- """Init of ventilation base.
-
- :param config: ventilation config
- """
- self._config = config
- self._ventilation_level: int | None = None
-
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
-
- # init state machine
- self._previous_state = None
- self._state_change_time = datetime.datetime.now()
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- self._apply_config()
-
- # callbacks
- self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.hand_request is not None:
- self._config.items.hand_request.listen_event(self._cb_power_hand_request, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.external_request is not None:
- self._config.items.external_request.listen_event(self._cb_external_request, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.presence_state is not None:
- self._config.items.presence_state.listen_event(self._cb_presence_state, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._update_openhab_state()
-
- def _get_initial_state(self, default_value: str = "initial") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.manual.is_on():
- return "Manual"
- if self._config.items.hand_request is not None and self._config.items.hand_request.is_on():
- return "Auto_PowerHand"
- if self._config.items.presence_state is not None and self._config.items.presence_state.value == habapp_rules.system.PresenceState.LONG_ABSENCE.value:
- return "Auto_LongAbsence"
- if self._config.items.external_request is not None and self._config.items.external_request.is_on():
- return "Auto_PowerExternal"
- return "Auto_Normal"
-
- def _apply_config(self) -> None:
- """Apply values from config."""
- self.state_machine.get_state("Auto_PowerHand").timeout = self._config.parameter.state_hand.timeout
- self.state_machine.get_state("Auto_LongAbsence_On").timeout = self._config.parameter.state_long_absence.duration
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._state_change_time = datetime.datetime.now()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_level()
- self._set_feedback_states()
- self._previous_state = self.state
-
- def _set_level(self) -> None:
- """Set ventilation level"""
- if self.state == "Manual":
- return
-
- if self.state == "Auto_PowerHand":
- self._ventilation_level = self._config.parameter.state_hand.level
- elif self.state == "Auto_Normal":
- self._ventilation_level = self._config.parameter.state_normal.level
- elif self.state == "Auto_PowerExternal":
- self._ventilation_level = self._config.parameter.state_external.level
- elif self.state == "Auto_LongAbsence_On":
- self._ventilation_level = self._config.parameter.state_long_absence.level
- elif self.state == "Auto_LongAbsence_Off":
- self._ventilation_level = 0
- else:
- return
-
- self._set_level_to_ventilation_items()
-
- @abc.abstractmethod
- def _set_level_to_ventilation_items(self) -> None:
- """Set ventilation to output item(s)."""
-
- def _get_display_text(self) -> str | None:
- """Get Text for display.
-
- :return: text for display or None if not defined for this state
- """
- if self.state == "Manual":
- return "Manual"
- if self.state == "Auto_Normal":
- return self._config.parameter.state_normal.display_text
- if self.state == "Auto_PowerExternal":
- return self._config.parameter.state_external.display_text
- if self.state == "Auto_LongAbsence_On":
- return f"{self._config.parameter.state_long_absence.display_text} ON"
- if self.state == "Auto_LongAbsence_Off":
- return f"{self._config.parameter.state_long_absence.display_text} OFF"
-
- return None
-
- def _set_feedback_states(self) -> None:
- """Set feedback sates to the OpenHAB items."""
- if self._config.items.hand_request is not None and self._previous_state == "Auto_PowerHand":
- habapp_rules.core.helper.send_if_different(self._config.items.hand_request, "OFF")
-
- if self._config.items.feedback_on is not None:
- habapp_rules.core.helper.send_if_different(self._config.items.feedback_on, "ON" if self._ventilation_level else "OFF")
-
- if self._config.items.feedback_power is not None:
- target_value = "ON" if self._ventilation_level is not None and self._ventilation_level >= 2 else "OFF"
- habapp_rules.core.helper.send_if_different(self._config.items.feedback_power, target_value)
-
- if self._config.items.display_text is not None:
- if self.state == "Auto_PowerHand":
- self.__set_hand_display_text()
- return
-
- if (display_text := self._get_display_text()) is not None:
- habapp_rules.core.helper.send_if_different(self._config.items.display_text, display_text)
-
- def __set_hand_display_text(self) -> None:
- """Callback to set display text."""
- if self.state != "Auto_PowerHand":
- # state changed and is not PowerHand anymore
- return
-
- # get the remaining minutes and set display text
- remaining_minutes = round((self._config.parameter.state_hand.timeout - (datetime.datetime.now() - self._state_change_time).seconds) / 60)
- remaining_minutes = remaining_minutes if remaining_minutes >= 0 else 0
- habapp_rules.core.helper.send_if_different(self._config.items.display_text, f"{self._config.parameter.state_hand.display_text} {remaining_minutes}min")
-
- # re-trigger this method in 1 minute
- self.run.at(60, self.__set_hand_display_text)
-
- def on_enter_Auto_Init(self) -> None: # pylint: disable=invalid-name
- """Is called on entering of Auto_Init state"""
- self._set_initial_state()
-
- def on_enter_Auto_LongAbsence_Off(self): # pylint: disable=invalid-name
- """Is called on entering of Auto_LongAbsence_Off state."""
- self.run.at(self._config.parameter.state_long_absence.start_time, self._trigger_long_absence_power_on)
-
- def _trigger_long_absence_power_on(self) -> None:
- """Trigger long absence power on."""
- self._long_absence_power_on()
-
- def _external_active_and_configured(self) -> bool:
- """Check if external request is active and configured.
-
- :return: True if external request is active and configured
- """
- return self._config.items.external_request is not None and self._config.items.external_request.is_on()
-
- def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if manual mode changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._manual_on()
- else:
- self._manual_off()
-
- def _cb_power_hand_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if power_hand_request changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._hand_on()
- else:
- self._hand_off()
-
- def _cb_external_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if external request changed.
-
- :param event: original trigger event
- """
- if event.value == "ON":
- self._external_on()
- else:
- self._external_off()
-
- def _cb_presence_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if presence_state changed.
-
- :param event: original trigger event
- """
- if event.value == habapp_rules.system.PresenceState.LONG_ABSENCE.value:
- self._long_absence_on()
- else:
- self._long_absence_off()
+ """Class for ventilation objects."""
+
+ states: typing.ClassVar = [
+ {"name": "Manual"},
+ {
+ "name": "Auto",
+ "initial": "Init",
+ "children": [
+ {"name": "Init"},
+ {"name": "Normal"},
+ {"name": "PowerHand", "timeout": 3600, "on_timeout": "_hand_off"},
+ {"name": "PowerExternal"},
+ {"name": "LongAbsence", "initial": "Off", "children": [{"name": "On", "timeout": 3600, "on_timeout": "_long_absence_power_off"}, {"name": "Off"}]},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ # manual
+ {"trigger": "_manual_on", "source": ["Auto"], "dest": "Manual"},
+ {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
+ # PowerHand
+ {"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence"], "dest": "Auto_PowerHand"},
+ {"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_PowerExternal", "conditions": "_external_active_and_configured"},
+ {"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_Normal", "unless": "_external_active_and_configured"},
+ # PowerExternal
+ {"trigger": "_external_on", "source": "Auto_Normal", "dest": "Auto_PowerExternal"},
+ {"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_Normal"},
+ # long absence
+ {"trigger": "_long_absence_on", "source": ["Auto_Normal", "Auto_PowerExternal"], "dest": "Auto_LongAbsence"},
+ {"trigger": "_long_absence_power_on", "source": "Auto_LongAbsence_Off", "dest": "Auto_LongAbsence_On"},
+ {"trigger": "_long_absence_power_off", "source": "Auto_LongAbsence_On", "dest": "Auto_LongAbsence_Off"},
+ {"trigger": "_long_absence_off", "source": "Auto_LongAbsence", "dest": "Auto_Normal"},
+ ]
+
+ def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationConfig) -> None:
+ """Init of ventilation base.
+
+ Args:
+ config: ventilation config
+ """
+ self._config = config
+ self._ventilation_level: int | None = None
+
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+
+ # init state machine
+ self._previous_state = None
+ self._state_change_time = datetime.datetime.now(tz=TIMEZONE)
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ self._apply_config()
+
+ # callbacks
+ self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.hand_request is not None:
+ self._config.items.hand_request.listen_event(self._cb_power_hand_request, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.external_request is not None:
+ self._config.items.external_request.listen_event(self._cb_external_request, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.presence_state is not None:
+ self._config.items.presence_state.listen_event(self._cb_presence_state, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._update_openhab_state()
+
+ def _get_initial_state(self, default_value: str = "initial") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.manual.is_on():
+ return "Manual"
+ if self._config.items.hand_request is not None and self._config.items.hand_request.is_on():
+ return "Auto_PowerHand"
+ if self._config.items.presence_state is not None and self._config.items.presence_state.value == habapp_rules.system.PresenceState.LONG_ABSENCE.value:
+ return "Auto_LongAbsence"
+ if self._config.items.external_request is not None and self._config.items.external_request.is_on():
+ return "Auto_PowerExternal"
+ return "Auto_Normal"
+
+ def _apply_config(self) -> None:
+ """Apply values from config."""
+ self.state_machine.get_state("Auto_PowerHand").timeout = self._config.parameter.state_hand.timeout
+ self.state_machine.get_state("Auto_LongAbsence_On").timeout = self._config.parameter.state_long_absence.duration
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._state_change_time = datetime.datetime.now(tz=TIMEZONE)
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_level()
+ self._set_feedback_states()
+ self._previous_state = self.state
+
+ def _set_level(self) -> None:
+ """Set ventilation level."""
+ if self.state == "Manual":
+ return
+
+ if self.state == "Auto_PowerHand":
+ self._ventilation_level = self._config.parameter.state_hand.level
+ elif self.state == "Auto_Normal":
+ self._ventilation_level = self._config.parameter.state_normal.level
+ elif self.state == "Auto_PowerExternal":
+ self._ventilation_level = self._config.parameter.state_external.level
+ elif self.state == "Auto_LongAbsence_On":
+ self._ventilation_level = self._config.parameter.state_long_absence.level
+ elif self.state == "Auto_LongAbsence_Off":
+ self._ventilation_level = 0
+ else:
+ return
+
+ self._set_level_to_ventilation_items()
+
+ @abc.abstractmethod
+ def _set_level_to_ventilation_items(self) -> None:
+ """Set ventilation to output item(s)."""
+
+ def _get_display_text(self) -> str | None:
+ """Get Text for display.
+
+ Returns:
+ text for display or None if not defined for this state
+ """
+ if self.state == "Manual":
+ return "Manual"
+ if self.state == "Auto_Normal":
+ return self._config.parameter.state_normal.display_text
+ if self.state == "Auto_PowerExternal":
+ return self._config.parameter.state_external.display_text
+ if self.state == "Auto_LongAbsence_On":
+ return f"{self._config.parameter.state_long_absence.display_text} ON"
+ if self.state == "Auto_LongAbsence_Off":
+ return f"{self._config.parameter.state_long_absence.display_text} OFF"
+
+ return None
+
+ def _set_feedback_states(self) -> None:
+ """Set feedback sates to the OpenHAB items."""
+ if self._config.items.hand_request is not None and self._previous_state == "Auto_PowerHand":
+ habapp_rules.core.helper.send_if_different(self._config.items.hand_request, "OFF")
+
+ if self._config.items.feedback_on is not None:
+ habapp_rules.core.helper.send_if_different(self._config.items.feedback_on, "ON" if self._ventilation_level else "OFF")
+
+ if self._config.items.feedback_power is not None:
+ target_value = "ON" if self._ventilation_level is not None and self._ventilation_level >= LEVEL_POWER else "OFF"
+ habapp_rules.core.helper.send_if_different(self._config.items.feedback_power, target_value)
+
+ if self._config.items.display_text is not None:
+ if self.state == "Auto_PowerHand":
+ self.__set_hand_display_text()
+ return
+
+ if (display_text := self._get_display_text()) is not None:
+ habapp_rules.core.helper.send_if_different(self._config.items.display_text, display_text)
+
+ def __set_hand_display_text(self) -> None:
+ """Callback to set display text."""
+ if self.state != "Auto_PowerHand":
+ # state changed and is not PowerHand anymore
+ return
+
+ # get the remaining minutes and set display text
+ remaining_minutes = round((self._config.parameter.state_hand.timeout - (datetime.datetime.now(tz=TIMEZONE) - self._state_change_time).seconds) / 60)
+ remaining_minutes = max(remaining_minutes, 0)
+ habapp_rules.core.helper.send_if_different(self._config.items.display_text, f"{self._config.parameter.state_hand.display_text} {remaining_minutes}min")
+
+ # re-trigger this method in 1 minute
+ self.run.once(60, self.__set_hand_display_text)
+
+ def on_enter_Auto_Init(self) -> None: # noqa: N802
+ """Is called on entering of Auto_Init state."""
+ self._set_initial_state()
+
+ def on_enter_Auto_LongAbsence_Off(self) -> None: # noqa: N802
+ """Is called on entering of Auto_LongAbsence_Off state."""
+ self.run.once(self._config.parameter.state_long_absence.start_time, self._trigger_long_absence_power_on)
+
+ def _trigger_long_absence_power_on(self) -> None:
+ """Trigger long absence power on."""
+ self._long_absence_power_on()
+
+ def _external_active_and_configured(self) -> bool:
+ """Check if external request is active and configured.
+
+ Returns:
+ True if external request is active and configured
+ """
+ return self._config.items.external_request is not None and self._config.items.external_request.is_on()
+
+ def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if manual mode changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._manual_on()
+ else:
+ self._manual_off()
+
+ def _cb_power_hand_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if power_hand_request changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._hand_on()
+ else:
+ self._hand_off()
+
+ def _cb_external_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if external request changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == "ON":
+ self._external_on()
+ else:
+ self._external_off()
+
+ def _cb_presence_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if presence_state changed.
+
+ Args:
+ event: original trigger event
+ """
+ if event.value == habapp_rules.system.PresenceState.LONG_ABSENCE.value:
+ self._long_absence_on()
+ else:
+ self._long_absence_off()
class Ventilation(_VentilationBase):
- """Rule for managing ventilation systems which can be controlled with ventilation levels
-
- # Items:
- Number Ventilation_level "Ventilation level"
- Switch Manual "Manual"
- Switch Hand_Request "Hand request"
- Switch External_Request "External request"
- String presence_state "Presence state"
- Switch Feedback_On "Feedback is ON"
- Switch Feedback_Power "Feedback is Power"
-
- # Config
- config = habapp_rules.actors.config.ventilation.VentilationConfig(
- items=habapp_rules.actors.config.ventilation.VentilationItems(
- ventilation_level="Ventilation_level",
- manual="Manual",
- hand_request="Hand_Request",
- external_request="External_Request",
- presence_state="presence_state",
- feedback_on="Feedback_On",
- feedback_power="Feedback_Power"
- )
- )
-
- # Rule init:
- habapp_rules.actors.ventilation.Ventilation(config)
- """
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationConfig) -> None:
- """Init of ventilation object.
-
- :param config: config of the ventilation rule
- """
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.ventilation_level.name)
-
- _VentilationBase.__init__(self, config)
- self._instance_logger.info(habapp_rules.core.state_machine_rule.StateMachineRule.get_initial_log_message(self))
-
- def _set_level_to_ventilation_items(self) -> None:
- """Set ventilation to output item(s)."""
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_level, self._ventilation_level)
+ """Rule for managing ventilation systems which can be controlled with ventilation levels.
+
+ # Items:
+ Number Ventilation_level "Ventilation level"
+ Switch Manual "Manual"
+ Switch Hand_Request "Hand request"
+ Switch External_Request "External request"
+ String presence_state "Presence state"
+ Switch Feedback_On "Feedback is ON"
+ Switch Feedback_Power "Feedback is Power"
+
+ # Config
+ config = habapp_rules.actors.config.ventilation.VentilationConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationItems(
+ ventilation_level="Ventilation_level",
+ manual="Manual",
+ hand_request="Hand_Request",
+ external_request="External_Request",
+ presence_state="presence_state",
+ feedback_on="Feedback_On",
+ feedback_power="Feedback_Power"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.ventilation.Ventilation(config)
+ """
+
+ def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationConfig) -> None:
+ """Init of ventilation object.
+
+ Args:
+ config: config of the ventilation rule
+ """
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.ventilation_level.name)
+
+ _VentilationBase.__init__(self, config)
+ self._instance_logger.info(habapp_rules.core.state_machine_rule.StateMachineRule.get_initial_log_message(self))
+
+ def _set_level_to_ventilation_items(self) -> None:
+ """Set ventilation to output item(s)."""
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_level, self._ventilation_level)
class VentilationHeliosTwoStage(_VentilationBase):
- """Rule for managing Helios ventilation systems with humidity sensor (E.g. Helios ELS)
-
- # Items:
- Switch Ventilation_Switch_On "Ventilation relay on"
- Switch Ventilation_Switch_Power "Ventilation relay power"
- Switch Manual "Manual"
- Switch Hand_Request "Hand request"
- Switch External_Request "External request"
- String presence_state "Presence state"
- Switch Feedback_On "Feedback is ON"
- Switch Feedback_Power "Feedback is Power"
-
- # Config
- config = habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Ventilation_Switch_On",
- ventilation_output_power="Ventilation_Switch_Power",
- manual="Manual",
- hand_request="Hand_Request",
- external_request="External_Request",
- presence_state="presence_state",
- feedback_on="Feedback_On",
- feedback_power="Feedback_Power"
- )
- )
-
- # Rule init:
- habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config)
- """
-
- states = copy.deepcopy(_VentilationBase.states)
- __AUTO_STATE = next(state for state in states if state["name"] == "Auto") # pragma: no cover
- __AUTO_STATE["children"].append({"name": "PowerAfterRun", "timeout": 390, "on_timeout": "_after_run_timeout"})
-
- trans = copy.deepcopy(_VentilationBase.trans)
- # remove not needed transitions
- trans.remove({"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence"], "dest": "Auto_PowerHand"}) # will be extended with additional source state
- trans.remove({"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_Normal", "unless": "_external_active_and_configured"}) # this is not needed anymore since there is always Auto_PowerAfterRun after any power state
- trans.remove({"trigger": "_external_on", "source": "Auto_Normal", "dest": "Auto_PowerExternal"}) # will be extended with additional source state
- trans.remove({"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_Normal"}) # this is not needed anymore since there is always Auto_PowerAfterRun after any power state
-
- # add new PowerHand transitions
- trans.append({"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence", "Auto_PowerAfterRun"], "dest": "Auto_PowerHand"})
- trans.append({"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_PowerAfterRun", "unless": "_external_active_and_configured"})
-
- # add new PowerExternal transitions
- trans.append({"trigger": "_external_on", "source": ["Auto_Normal", "Auto_PowerAfterRun"], "dest": "Auto_PowerExternal"})
- trans.append({"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_PowerAfterRun"})
-
- # add new PowerAfterRun transitions
- trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"})
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationTwoStageConfig) -> None:
- """Init of a Helios ventilation object which uses two switches to set the level.
-
- :param config: config for the ventilation rule
- """
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.ventilation_output_on.name)
- _VentilationBase.__init__(self, config)
-
- # set timeout
- self.state_machine.get_state("Auto_PowerAfterRun").timeout = config.parameter.after_run_timeout
-
- self._instance_logger.info(habapp_rules.core.state_machine_rule.StateMachineRule.get_initial_log_message(self))
-
- def _get_display_text(self) -> str | None:
- """Get Text for display.
-
- :return: text for display or None if not defined for this state
- """
- if self.state == "Auto_PowerAfterRun":
- return self._config.parameter.state_after_run.display_text
-
- return _VentilationBase._get_display_text(self)
-
- def _set_level(self) -> None:
- if self.state == "Auto_PowerAfterRun":
- self._ventilation_level = self._config.parameter.state_after_run.level
- self._set_level_to_ventilation_items()
- return
-
- super()._set_level()
-
- def _set_level_to_ventilation_items(self) -> None:
- """Set ventilation to output item(s)."""
- if self.state == "Auto_PowerAfterRun":
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON")
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "OFF")
-
- else:
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON" if self._ventilation_level else "OFF")
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "ON" if self._ventilation_level >= 2 else "OFF")
-
-
-# pylint: disable=no-member, missing-return-doc
+ """Rule for managing Helios ventilation systems with humidity sensor (E.g. Helios ELS).
+
+ # Items:
+ Switch Ventilation_Switch_On "Ventilation relay on"
+ Switch Ventilation_Switch_Power "Ventilation relay power"
+ Switch Manual "Manual"
+ Switch Hand_Request "Hand request"
+ Switch External_Request "External request"
+ String presence_state "Presence state"
+ Switch Feedback_On "Feedback is ON"
+ Switch Feedback_Power "Feedback is Power"
+
+ # Config
+ config = habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Ventilation_Switch_On",
+ ventilation_output_power="Ventilation_Switch_Power",
+ manual="Manual",
+ hand_request="Hand_Request",
+ external_request="External_Request",
+ presence_state="presence_state",
+ feedback_on="Feedback_On",
+ feedback_power="Feedback_Power"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config)
+ """
+
+ states = copy.deepcopy(_VentilationBase.states)
+ __AUTO_STATE = next(state for state in states if state["name"] == "Auto") # pragma: no cover
+ __AUTO_STATE["children"].append({"name": "PowerAfterRun", "timeout": 390, "on_timeout": "_after_run_timeout"})
+
+ trans = copy.deepcopy(_VentilationBase.trans)
+ # remove not needed transitions
+ trans.remove({"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence"], "dest": "Auto_PowerHand"}) # will be extended with additional source state
+ trans.remove({"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_Normal", "unless": "_external_active_and_configured"}) # this is not needed anymore since there is always Auto_PowerAfterRun after any power state
+ trans.remove({"trigger": "_external_on", "source": "Auto_Normal", "dest": "Auto_PowerExternal"}) # will be extended with additional source state
+ trans.remove({"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_Normal"}) # this is not needed anymore since there is always Auto_PowerAfterRun after any power state
+
+ # add new PowerHand transitions
+ trans.append({"trigger": "_hand_on", "source": ["Auto_Normal", "Auto_PowerExternal", "Auto_LongAbsence", "Auto_PowerAfterRun"], "dest": "Auto_PowerHand"})
+ trans.append({"trigger": "_hand_off", "source": "Auto_PowerHand", "dest": "Auto_PowerAfterRun", "unless": "_external_active_and_configured"})
+
+ # add new PowerExternal transitions
+ trans.append({"trigger": "_external_on", "source": ["Auto_Normal", "Auto_PowerAfterRun"], "dest": "Auto_PowerExternal"})
+ trans.append({"trigger": "_external_off", "source": "Auto_PowerExternal", "dest": "Auto_PowerAfterRun"})
+
+ # add new PowerAfterRun transitions
+ trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"})
+
+ def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationTwoStageConfig) -> None:
+ """Init of a Helios ventilation object which uses two switches to set the level.
+
+ Args:
+ config: config for the ventilation rule
+ """
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.ventilation_output_on.name)
+ _VentilationBase.__init__(self, config)
+
+ # set timeout
+ self.state_machine.get_state("Auto_PowerAfterRun").timeout = config.parameter.after_run_timeout
+
+ self._instance_logger.info(habapp_rules.core.state_machine_rule.StateMachineRule.get_initial_log_message(self))
+
+ def _get_display_text(self) -> str | None:
+ """Get Text for display.
+
+ Returns:
+ text for display or None if not defined for this state
+ """
+ if self.state == "Auto_PowerAfterRun":
+ return self._config.parameter.state_after_run.display_text
+
+ return _VentilationBase._get_display_text(self) # noqa: SLF001
+
+ def _set_level(self) -> None:
+ """Set ventilation level."""
+ if self.state == "Auto_PowerAfterRun":
+ self._ventilation_level = self._config.parameter.state_after_run.level
+ self._set_level_to_ventilation_items()
+ return
+
+ super()._set_level()
+
+ def _set_level_to_ventilation_items(self) -> None:
+ """Set ventilation to output item(s)."""
+ if self.state == "Auto_PowerAfterRun":
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON")
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "OFF")
+
+ else:
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON" if self._ventilation_level else "OFF")
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "ON" if self._ventilation_level >= LEVEL_POWER else "OFF")
+
+
class VentilationHeliosTwoStageHumidity(VentilationHeliosTwoStage):
- """Rule for managing Helios ventilation systems with humidity sensor (E.g. Helios ELS)
-
- # Items:
- Switch Ventilation_Switch_On "Ventilation relay on"
- Switch Ventilation_Switch_Power "Ventilation relay power"
- Number Ventilation_Current "Ventilation current"
- Switch Manual "Manual"
- Switch Hand_Request "Hand request"
- Switch External_Request "External request"
- String presence_state "Presence state"
- Switch Feedback_On "Feedback is ON"
- Switch Feedback_Power "Feedback is Power"
-
- # Config
- config = habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Ventilation_Switch_On",
- ventilation_output_power="Ventilation_Switch_Power",
- current="Ventilation_Current",
- manual="Manual",
- hand_request="Hand_Request",
- external_request="External_Request",
- presence_state="presence_state",
- feedback_on="Feedback_On",
- feedback_power="Feedback_Power"
- )
- )
-
- # Rule init:
- habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config)
- """
-
- states = copy.deepcopy(VentilationHeliosTwoStage.states)
- __AUTO_STATE = next(state for state in states if state["name"] == "Auto") # pragma: no cover
- __AUTO_STATE["children"].append({"name": "PowerHumidity"})
-
- trans = copy.deepcopy(VentilationHeliosTwoStage.trans)
- # remove not needed transitions
- trans.remove({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"}) # will be changed to only go to AutoNormal if the current is below the threshold (not humidity)
-
- # add new PowerHumidity transitions
- trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal", "unless": "_current_greater_threshold"})
- trans.append({"trigger": "_end_after_run", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"})
- trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_PowerHumidity", "conditions": "_current_greater_threshold"})
-
- trans.append({"trigger": "_humidity_on", "source": "Auto_Normal", "dest": "Auto_PowerHumidity"})
- trans.append({"trigger": "_humidity_off", "source": "Auto_PowerHumidity", "dest": "Auto_Normal"})
-
- trans.append({"trigger": "_hand_on", "source": "Auto_PowerHumidity", "dest": "Auto_PowerHand"})
- trans.append({"trigger": "_external_on", "source": "Auto_PowerHumidity", "dest": "Auto_PowerExternal"})
-
- # pylint: disable=too-many-locals, too-many-arguments
- def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationTwoStageConfig) -> None:
- """Init of a Helios ventilation object which uses two switches to set the level, including a humidity sensor.
-
- :param config: configuration of the ventilation rule
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if config is missing required items
- """
- if config.items.current is None:
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException("Missing item 'current'")
- self._current_threshold_power = config.parameter.current_threshold_power
-
- VentilationHeliosTwoStage.__init__(self, config)
- config.items.current.listen_event(self._cb_current, HABApp.openhab.events.ItemStateUpdatedEventFilter())
-
- def _get_initial_state(self, default_value: str = "initial") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- state = super()._get_initial_state(default_value)
-
- if state == "Auto_Normal" and self._current_greater_threshold():
- return "Auto_PowerHumidity"
-
- return state
-
- def _get_display_text(self) -> str | None:
- """Get Text for display.
-
- :return: text for display or None if not defined for this state
- """
- if self.state == "Auto_PowerHumidity":
- return self._config.parameter.state_humidity.display_text
-
- return VentilationHeliosTwoStage._get_display_text(self)
-
- def _set_level(self) -> None:
- if self.state == "Auto_PowerHumidity":
- self._ventilation_level = self._config.parameter.state_humidity.level
- self._set_level_to_ventilation_items()
- return
-
- super()._set_level()
-
- def _set_level_to_ventilation_items(self) -> None:
- """Set ventilation to output item(s)."""
- if self.state == "Auto_PowerHumidity":
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON")
- habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "OFF")
- else:
- super()._set_level_to_ventilation_items()
-
- def _current_greater_threshold(self, current: float | None = None) -> bool:
- """Check if current is greater than the threshold
-
- :param current: current which should be checked. If None the value of the current item will be taken
- :return: True if current greater than the threshold, else False
- """
- current = current if current is not None else self._config.items.current.value
-
- if current is None:
- return False
-
- return current > self._current_threshold_power
-
- def _cb_current(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
- """Callback which is triggered if the current changed.
-
- :param event: original trigger event
- """
- if self.state != "Auto_PowerHumidity" and self._current_greater_threshold(event.value):
- self._humidity_on()
- elif self.state == "Auto_PowerHumidity" and not self._current_greater_threshold(event.value):
- self._humidity_off()
- elif self.state == "Auto_PowerAfterRun" and not self._current_greater_threshold(event.value):
- self._end_after_run()
+ """Rule for managing Helios ventilation systems with humidity sensor (E.g. Helios ELS).
+
+ # Items:
+ Switch Ventilation_Switch_On "Ventilation relay on"
+ Switch Ventilation_Switch_Power "Ventilation relay power"
+ Number Ventilation_Current "Ventilation current"
+ Switch Manual "Manual"
+ Switch Hand_Request "Hand request"
+ Switch External_Request "External request"
+ String presence_state "Presence state"
+ Switch Feedback_On "Feedback is ON"
+ Switch Feedback_Power "Feedback is Power"
+
+ # Config
+ config = habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Ventilation_Switch_On",
+ ventilation_output_power="Ventilation_Switch_Power",
+ current="Ventilation_Current",
+ manual="Manual",
+ hand_request="Hand_Request",
+ external_request="External_Request",
+ presence_state="presence_state",
+ feedback_on="Feedback_On",
+ feedback_power="Feedback_Power"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config)
+ """
+
+ states = copy.deepcopy(VentilationHeliosTwoStage.states)
+ __AUTO_STATE = next(state for state in states if state["name"] == "Auto") # pragma: no cover
+ __AUTO_STATE["children"].append({"name": "PowerHumidity"})
+
+ trans = copy.deepcopy(VentilationHeliosTwoStage.trans)
+ # remove not needed transitions
+ trans.remove({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"}) # will be changed to only go to AutoNormal if the current is below the threshold (not humidity)
+
+ # add new PowerHumidity transitions
+ trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal", "unless": "_current_greater_threshold"})
+ trans.append({"trigger": "_end_after_run", "source": "Auto_PowerAfterRun", "dest": "Auto_Normal"})
+ trans.append({"trigger": "_after_run_timeout", "source": "Auto_PowerAfterRun", "dest": "Auto_PowerHumidity", "conditions": "_current_greater_threshold"})
+
+ trans.append({"trigger": "_humidity_on", "source": "Auto_Normal", "dest": "Auto_PowerHumidity"})
+ trans.append({"trigger": "_humidity_off", "source": "Auto_PowerHumidity", "dest": "Auto_Normal"})
+
+ trans.append({"trigger": "_hand_on", "source": "Auto_PowerHumidity", "dest": "Auto_PowerHand"})
+ trans.append({"trigger": "_external_on", "source": "Auto_PowerHumidity", "dest": "Auto_PowerExternal"})
+
+ def __init__(self, config: habapp_rules.actors.config.ventilation.VentilationTwoStageConfig) -> None:
+ """Init of a Helios ventilation object which uses two switches to set the level, including a humidity sensor.
+
+ Args:
+ config: configuration of the ventilation rule
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if config is missing required items
+ """
+ if config.items.current is None:
+ msg = "Missing item 'current'"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
+ self._current_threshold_power = config.parameter.current_threshold_power
+
+ VentilationHeliosTwoStage.__init__(self, config)
+ config.items.current.listen_event(self._cb_current, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+
+ def _get_initial_state(self, default_value: str = "initial") -> str:
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ state = super()._get_initial_state(default_value)
+
+ if state == "Auto_Normal" and self._current_greater_threshold():
+ return "Auto_PowerHumidity"
+
+ return state
+
+ def _get_display_text(self) -> str | None:
+ """Get Text for display.
+
+ Returns:
+ text for display or None if not defined for this state
+ """
+ if self.state == "Auto_PowerHumidity":
+ return self._config.parameter.state_humidity.display_text
+
+ return VentilationHeliosTwoStage._get_display_text(self) # noqa: SLF001
+
+ def _set_level(self) -> None:
+ """Set ventilation level."""
+ if self.state == "Auto_PowerHumidity":
+ self._ventilation_level = self._config.parameter.state_humidity.level
+ self._set_level_to_ventilation_items()
+ return
+
+ super()._set_level()
+
+ def _set_level_to_ventilation_items(self) -> None:
+ """Set ventilation to output item(s)."""
+ if self.state == "Auto_PowerHumidity":
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_on, "ON")
+ habapp_rules.core.helper.send_if_different(self._config.items.ventilation_output_power, "OFF")
+ else:
+ super()._set_level_to_ventilation_items()
+
+ def _current_greater_threshold(self, current: float | None = None) -> bool:
+ """Check if current is greater than the threshold.
+
+ Args:
+ current: current which should be checked. If None the value of the current item will be taken
+
+ Returns:
+ True if current greater than the threshold, else False
+ """
+ current = current if current is not None else self._config.items.current.value
+
+ if current is None:
+ return False
+
+ return current > self._current_threshold_power
+
+ def _cb_current(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
+ """Callback which is triggered if the current changed.
+
+ Args:
+ event: original trigger event
+ """
+ if self.state != "Auto_PowerHumidity" and self._current_greater_threshold(event.value):
+ self._humidity_on()
+ elif self.state == "Auto_PowerHumidity" and not self._current_greater_threshold(event.value):
+ self._humidity_off()
+ elif self.state == "Auto_PowerAfterRun" and not self._current_greater_threshold(event.value):
+ self._end_after_run()
diff --git a/habapp_rules/bridge/config/knx_mqtt.py b/habapp_rules/bridge/config/knx_mqtt.py
index b1a38aa..2c6084d 100644
--- a/habapp_rules/bridge/config/knx_mqtt.py
+++ b/habapp_rules/bridge/config/knx_mqtt.py
@@ -1,37 +1,44 @@
"""Config models for KNX / MQTT bridge."""
-import typing
import HABApp.openhab.items
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class KnxMqttItems(habapp_rules.core.pydantic_base.ItemBase):
- """Configuration of items for KNX MQTT bridge."""
- mqtt_dimmer: HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="")
- knx_switch_ctr: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="")
- knx_dimmer_ctr: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="")
+ """Configuration of items for KNX MQTT bridge."""
- @pydantic.model_validator(mode="after")
- def validate_knx_items(self) -> typing.Self:
- """Validate KNX items
+ mqtt_dimmer: HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="")
+ knx_switch_ctr: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="")
+ knx_dimmer_ctr: HABApp.openhab.items.DimmerItem | None = pydantic.Field(None, description="")
- :return: validated model
- :raises ValueError: if knx_switch_ctr and knx_dimmer_ctr are not set
- """
- if self.knx_switch_ctr is None and self.knx_dimmer_ctr is None:
- raise ValueError("knx_switch_ctr or knx_dimmer_ctr must be set")
- return self
+ @pydantic.model_validator(mode="after")
+ def validate_knx_items(self) -> typing_extensions.Self:
+ """Validate KNX items.
+
+ Returns:
+ validated model
+
+ Raises:
+ ValueError: if knx_switch_ctr and knx_dimmer_ctr are not set
+ """
+ if self.knx_switch_ctr is None and self.knx_dimmer_ctr is None:
+ msg = "knx_switch_ctr or knx_dimmer_ctr must be set"
+ raise ValueError(msg)
+ return self
class KnxMqttParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Configuration of parameters for KNX MQTT bridge."""
- increase_value: int = pydantic.Field(60, description="")
- decrease_value: int = pydantic.Field(30, description="")
+ """Configuration of parameters for KNX MQTT bridge."""
+
+ increase_value: int = pydantic.Field(60, description="")
+ decrease_value: int = pydantic.Field(30, description="")
class KnxMqttConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Configuration of KNX MQTT bridge."""
- items: KnxMqttItems = pydantic.Field(..., description="Items for KNX MQTT bridge")
- parameter: KnxMqttParameter = pydantic.Field(KnxMqttParameter(), description="Parameters for KNX MQTT bridge")
+ """Configuration of KNX MQTT bridge."""
+
+ items: KnxMqttItems = pydantic.Field(..., description="Items for KNX MQTT bridge")
+ parameter: KnxMqttParameter = pydantic.Field(KnxMqttParameter(), description="Parameters for KNX MQTT bridge")
diff --git a/habapp_rules/bridge/knx_mqtt.py b/habapp_rules/bridge/knx_mqtt.py
index 5343204..4212a58 100644
--- a/habapp_rules/bridge/knx_mqtt.py
+++ b/habapp_rules/bridge/knx_mqtt.py
@@ -1,4 +1,5 @@
"""Rules for bridging KNX controller to MQTT items."""
+
import logging
import HABApp
@@ -11,60 +12,66 @@
class KnxMqttDimmerBridge(HABApp.Rule):
- """Create a bridge to control a MQTT dimmer from a KNX controller (e.g. wall switch).
-
- To use this the items must be configured according the following example:
- - mqtt_dimmer: autoupdate should be false, thing: according to OpenHAB documentation
- - knx_switch_ctr: autoupdate must be activated, thing: [ ga="1/1/124+1/1/120" ] for ga: at first always use the RM-GA, second is the control-GA
- - knx_dimmer_ctr: autoupdate must be activated, thing: [ position="1/1/125+1/1/123", increaseDecrease="1/1/122" ] for position: at first always use the RM-GA, second is the control-GA
-
- info: OpenHAB does not support start/stop dimming. Thus, this implementation will set fixed values if INCREASE/DECREASE was received from KNX
- """
-
- def __init__(self, config: habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig) -> None:
- """Create object of KNX to MQTT bridge
-
- :param config: Configuration of the KNX MQTT bridge
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: If config is not valid
- """
- self._config = config
- knx_name = self._config.items.knx_switch_ctr.name if self._config.items.knx_switch_ctr is not None else self._config.items.knx_dimmer_ctr.name
-
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{knx_name}__{self._config.items.mqtt_dimmer.name}")
-
- self._config.items.mqtt_dimmer.listen_event(self._cb_mqtt_event, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.knx_dimmer_ctr is not None:
- self._config.items.knx_dimmer_ctr.listen_event(self._cb_knx_event, HABApp.openhab.events.ItemCommandEventFilter())
- if self._config.items.knx_switch_ctr is not None:
- self._config.items.knx_switch_ctr.listen_event(self._cb_knx_event, HABApp.openhab.events.ItemCommandEventFilter())
- self._instance_logger.debug("successful!")
-
- def _cb_knx_event(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is called if a KNX command received.
-
- :param event: HABApp event
- """
- if isinstance(event.value, (int, float)):
- self._config.items.mqtt_dimmer.oh_send_command(event.value)
- elif event.value in {"ON", "OFF"}:
- self._config.items.mqtt_dimmer.oh_send_command(event.value)
- elif event.value == "INCREASE":
- target_value = self._config.parameter.increase_value if self._config.items.mqtt_dimmer.value < self._config.parameter.increase_value else 100
- self._config.items.mqtt_dimmer.oh_send_command(target_value)
- elif event.value == "DECREASE":
- target_value = self._config.parameter.decrease_value if self._config.items.mqtt_dimmer.value > self._config.parameter.decrease_value else 0
- self._config.items.mqtt_dimmer.oh_send_command(target_value)
- else:
- self._instance_logger.error(f"command '{event.value}' ist not supported!")
-
- def _cb_mqtt_event(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is called if a MQTT state change event happens.
-
- :param event: HABApp event
- """
- if self._config.items.knx_dimmer_ctr is not None:
- self._config.items.knx_dimmer_ctr.oh_post_update(event.value)
-
- if self._config.items.knx_switch_ctr is not None:
- self._config.items.knx_switch_ctr.oh_post_update("ON" if event.value > 0 else "OFF")
+ """Create a bridge to control a MQTT dimmer from a KNX controller (e.g. wall switch).
+
+ To use this the items must be configured according the following example:
+ - mqtt_dimmer: autoupdate should be false, thing: according to OpenHAB documentation
+ - knx_switch_ctr: autoupdate must be activated, thing: [ ga="1/1/124+1/1/120" ] for ga: at first always use the RM-GA, second is the control-GA
+ - knx_dimmer_ctr: autoupdate must be activated, thing: [ position="1/1/125+1/1/123", increaseDecrease="1/1/122" ] for position: at first always use the RM-GA, second is the control-GA
+
+ info: OpenHAB does not support start/stop dimming. Thus, this implementation will set fixed values if INCREASE/DECREASE was received from KNX
+ """
+
+ def __init__(self, config: habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig) -> None:
+ """Create object of KNX to MQTT bridge.
+
+ Args:
+ config: Configuration of the KNX MQTT bridge
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationException: If config is not valid
+ """
+ self._config = config
+ knx_name = self._config.items.knx_switch_ctr.name if self._config.items.knx_switch_ctr is not None else self._config.items.knx_dimmer_ctr.name
+
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{knx_name}__{self._config.items.mqtt_dimmer.name}")
+
+ self._config.items.mqtt_dimmer.listen_event(self._cb_mqtt_event, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.knx_dimmer_ctr is not None:
+ self._config.items.knx_dimmer_ctr.listen_event(self._cb_knx_event, HABApp.openhab.events.ItemCommandEventFilter())
+ if self._config.items.knx_switch_ctr is not None:
+ self._config.items.knx_switch_ctr.listen_event(self._cb_knx_event, HABApp.openhab.events.ItemCommandEventFilter())
+ self._instance_logger.debug("successful!")
+
+ def _cb_knx_event(self, event: HABApp.openhab.events.ItemCommandEvent) -> None:
+ """Callback, which is called if a KNX command received.
+
+ Args:
+ event: HABApp event
+ """
+ if isinstance(event.value, int | float) or event.value in {"ON", "OFF"}:
+ self._config.items.mqtt_dimmer.oh_send_command(event.value)
+ elif event.value == "INCREASE":
+ target_value = self._config.parameter.increase_value if self._config.items.mqtt_dimmer.value < self._config.parameter.increase_value else 100
+ self._config.items.mqtt_dimmer.oh_send_command(target_value)
+ elif event.value == "DECREASE":
+ target_value = self._config.parameter.decrease_value if self._config.items.mqtt_dimmer.value > self._config.parameter.decrease_value else 0
+ self._config.items.mqtt_dimmer.oh_send_command(target_value)
+ else:
+ self._instance_logger.error(f"command '{event.value}' ist not supported!")
+
+ def _cb_mqtt_event(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if a MQTT state change event happens.
+
+ Args:
+ event: HABApp event
+ """
+ if not isinstance(event.value, int | float):
+ return
+
+ if self._config.items.knx_dimmer_ctr is not None:
+ self._config.items.knx_dimmer_ctr.oh_post_update(event.value)
+
+ if self._config.items.knx_switch_ctr is not None:
+ self._config.items.knx_switch_ctr.oh_post_update("ON" if event.value > 0 else "OFF")
diff --git a/habapp_rules/common/config/filter.py b/habapp_rules/common/config/filter.py
index fff69f3..6211705 100644
--- a/habapp_rules/common/config/filter.py
+++ b/habapp_rules/common/config/filter.py
@@ -1,37 +1,44 @@
"""Config models for filter rules."""
-import typing
import HABApp.openhab.items
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class ExponentialFilterItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for exponential filter."""
- raw: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item for raw value")
- filtered: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item for filtered value")
+ """Items for exponential filter."""
+
+ raw: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item for raw value")
+ filtered: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Item for filtered value")
class ExponentialFilterParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for exponential filter."""
- tau: int = pydantic.Field(..., description="filter time constant in seconds. E.g. step from 0 to 1 | tau = 5 seconds -> after 5 seconds the value will be 0,67")
- instant_increase: bool = pydantic.Field(False, description="if set to True, increase of input values will not be filtered")
- instant_decrease: bool = pydantic.Field(False, description="if set to True, decrease of input values will not be filtered")
+ """Parameter for exponential filter."""
+
+ tau: int = pydantic.Field(..., description="filter time constant in seconds. E.g. step from 0 to 1 | tau = 5 seconds -> after 5 seconds the value will be 0,67")
+ instant_increase: bool = pydantic.Field(default=False, description="if set to True, increase of input values will not be filtered")
+ instant_decrease: bool = pydantic.Field(default=False, description="if set to True, decrease of input values will not be filtered")
- @pydantic.model_validator(mode="after")
- def validate_instant_parameters(self) -> typing.Self:
- """Validate instant_increase and instant_decrease.
+ @pydantic.model_validator(mode="after")
+ def validate_instant_parameters(self) -> typing_extensions.Self:
+ """Validate instant_increase and instant_decrease.
- :return: validated model
- :raises ValueError: if both parameters are set
- """
- if self.instant_decrease and self.instant_increase:
- raise ValueError("instant_increase and instant_decrease can not be set to True at the same time!")
- return self
+ Returns:
+ validated model
+
+ Raises:
+ ValueError: if both parameters are set
+ """
+ if self.instant_decrease and self.instant_increase:
+ msg = "instant_increase and instant_decrease can not be set to True at the same time!"
+ raise ValueError(msg)
+ return self
class ExponentialFilterConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for exponential filter."""
- items: ExponentialFilterItems = pydantic.Field(..., description="Items for exponential filter")
- parameter: ExponentialFilterParameter = pydantic.Field(..., description="Parameter for exponential filter")
+ """Config for exponential filter."""
+
+ items: ExponentialFilterItems = pydantic.Field(..., description="Items for exponential filter")
+ parameter: ExponentialFilterParameter = pydantic.Field(..., description="Parameter for exponential filter")
diff --git a/habapp_rules/common/config/logic.py b/habapp_rules/common/config/logic.py
index 0120bfd..27bc34b 100644
--- a/habapp_rules/common/config/logic.py
+++ b/habapp_rules/common/config/logic.py
@@ -1,66 +1,78 @@
"""Config models for logic rules."""
-import typing
import HABApp
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class BinaryLogicItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for binary logic."""
- inputs: list[HABApp.openhab.items.SwitchItem | HABApp.openhab.items.ContactItem] = pydantic.Field(..., description="List of input items (must be either Switch or Contact and all have to match to output_item)")
- output: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.ContactItem = pydantic.Field(..., description="Output item")
+ """Items for binary logic."""
- @pydantic.model_validator(mode="after")
- def validate_items(self) -> typing.Self:
- """Validate if all items are of the same type.
+ inputs: list[HABApp.openhab.items.SwitchItem | HABApp.openhab.items.ContactItem] = pydantic.Field(..., description="List of input items (must be either Switch or Contact and all have to match to output_item)")
+ output: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.ContactItem = pydantic.Field(..., description="Output item")
- :return: validated model
- :raises ValueError: if not all items are of the same type
- """
- for item in self.inputs:
- if not isinstance(item, type(self.output)):
- raise ValueError(f"Item '{item.name}' must have the same type like the output item. Expected: {type(self.output)} | actual : {type(item)}")
- return self
+ @pydantic.model_validator(mode="after")
+ def validate_items(self) -> typing_extensions.Self:
+ """Validate if all items are of the same type.
+
+ Returns:
+ validated model
+
+ Raises:
+ TypeError: if not all items are of the same type
+ """
+ for item in self.inputs:
+ if not isinstance(item, type(self.output)):
+ msg = f"Item '{item.name}' must have the same type like the output item. Expected: {type(self.output)} | actual : {type(item)}"
+ raise TypeError(msg)
+ return self
class BinaryLogicConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for binary logic."""
- items: BinaryLogicItems = pydantic.Field(..., description="Items for binary logic")
- parameter: None = None
+ """Config for binary logic."""
+
+ items: BinaryLogicItems = pydantic.Field(..., description="Items for binary logic")
+ parameter: None = None
class NumericLogicItems(BinaryLogicItems):
- """Items for numeric logic."""
- inputs: list[HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="List of input items (must be either Number or Dimmer and all have to match to output_item)")
- output: HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="Output item")
+ """Items for numeric logic."""
+
+ inputs: list[HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="List of input items (must be either Number or Dimmer and all have to match to output_item)")
+ output: HABApp.openhab.items.NumberItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="Output item")
class NumericLogicParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for numeric logic."""
- ignore_old_values_time: int | None = pydantic.Field(None, description="ignores values which are older than the given time in seconds. If None, all values will be taken")
+ """Parameter for numeric logic."""
+
+ ignore_old_values_time: int | None = pydantic.Field(None, description="ignores values which are older than the given time in seconds. If None, all values will be taken")
class NumericLogicConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for numeric logic."""
- items: NumericLogicItems = pydantic.Field(..., description="Items for numeric logic")
- parameter: NumericLogicParameter = pydantic.Field(NumericLogicParameter(), description="Parameter for numeric logic")
+ """Config for numeric logic."""
+
+ items: NumericLogicItems = pydantic.Field(..., description="Items for numeric logic")
+ parameter: NumericLogicParameter = pydantic.Field(NumericLogicParameter(), description="Parameter for numeric logic")
class InvertValueItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for invert value."""
- input: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Input item")
- output: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Output item")
+ """Items for invert value."""
+
+ input: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Input item")
+ output: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Output item")
class InvertValueParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for invert value."""
- only_positive: bool = pydantic.Field(False, description="if true, only positive values will be set to output item")
- only_negative: bool = pydantic.Field(False, description="if true, only negative values will be set to output item")
+ """Parameter for invert value."""
+
+ only_positive: bool = pydantic.Field(default=False, description="if true, only positive values will be set to output item")
+ only_negative: bool = pydantic.Field(default=False, description="if true, only negative values will be set to output item")
class InvertValueConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for invert value."""
- items: InvertValueItems = pydantic.Field(..., description="Items for invert value")
- parameter: InvertValueParameter = pydantic.Field(InvertValueParameter(), description="Parameter for invert value")
+ """Config for invert value."""
+
+ items: InvertValueItems = pydantic.Field(..., description="Items for invert value")
+ parameter: InvertValueParameter = pydantic.Field(InvertValueParameter(), description="Parameter for invert value")
diff --git a/habapp_rules/common/filter.py b/habapp_rules/common/filter.py
index 142219c..752c4c5 100644
--- a/habapp_rules/common/filter.py
+++ b/habapp_rules/common/filter.py
@@ -1,4 +1,5 @@
"""Module for filter functions / rules."""
+
import logging
import HABApp
@@ -10,84 +11,87 @@
class ExponentialFilter(HABApp.Rule):
- """Rules class to apply a exponential filter to a number value.
-
- # Items:
- Number BrightnessValue "Brightness Value [%d]" {channel="..."}
- Number BrightnessFiltered "Brightness filtered [%d]"
- Number BrightnessFilteredInstantIncrease "Brightness filtered instant increase [%d]"
-
- # Config
- config = habapp_rules.common.config.filter.ExponentialFilterConfig(
- items = habapp_rules.common.config.filter.ExponentialFilterItems(
- raw = "BrightnessValue",
- filtered = "BrightnessFiltered"
- ),
- parameter = habapp_rules.common.config.filter.ExponentialFilterParameter( # filter constant 1 minute
- tau = 60
- )
- )
-
- config2 = habapp_rules.common.config.filter.ExponentialFilterConfig(
- items = habapp_rules.common.config.filter.ExponentialFilterItems(
- raw = "BrightnessValue",
- filtered = "BrightnessFilteredInstantIncrease"
- ),
- parameter = habapp_rules.common.config.filter.ExponentialFilterParameter( # filter constant 10 minutes + instant increase
- tau = 600,
- instant_increase = True
- )
- )
-
- # Rule init:
- habapp_rules.common.filter.ExponentialFilter(config) # filter constant 1 minute
+ """Rules class to apply a exponential filter to a number value.
+
+ # Items:
+ Number BrightnessValue "Brightness Value [%d]" {channel="..."}
+ Number BrightnessFiltered "Brightness filtered [%d]"
+ Number BrightnessFilteredInstantIncrease "Brightness filtered instant increase [%d]"
+
+ # Config
+ config = habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items = habapp_rules.common.config.filter.ExponentialFilterItems(
+ raw = "BrightnessValue",
+ filtered = "BrightnessFiltered"
+ ),
+ parameter = habapp_rules.common.config.filter.ExponentialFilterParameter( # filter constant 1 minute
+ tau = 60
+ )
+ )
+
+ config2 = habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items = habapp_rules.common.config.filter.ExponentialFilterItems(
+ raw = "BrightnessValue",
+ filtered = "BrightnessFilteredInstantIncrease"
+ ),
+ parameter = habapp_rules.common.config.filter.ExponentialFilterParameter( # filter constant 10 minutes + instant increase
+ tau = 600,
+ instant_increase = True
+ )
+ )
+
+ # Rule init:
+ habapp_rules.common.filter.ExponentialFilter(config) # filter constant 1 minute
habapp_rules.common.filter.ExponentialFilter(config2) # filter constant 10 minutes + instant increase
- """
+ """
- def __init__(self, config: habapp_rules.common.config.filter.ExponentialFilterConfig):
- """Init exponential filter rule.
+ def __init__(self, config: habapp_rules.common.config.filter.ExponentialFilterConfig) -> None:
+ """Init exponential filter rule.
- :param config: Config for exponential filter
- """
- HABApp.Rule.__init__(self)
- self._config = config
+ Args:
+ config: Config for exponential filter
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self.rule_name)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self.rule_name)
- self._previous_value = self._config.items.raw.value
+ self._previous_value = self._config.items.raw.value
- sample_time = self._config.parameter.tau / 5 # fifth part of the filter time constant
- self._alpha = 0.2 # always 0.2 since we always have the fifth part of the filter time constant
- self.run.every(None, sample_time, self._cb_cyclic_calculate_and_update_output)
+ sample_time = self._config.parameter.tau / 5 # fifth part of the filter time constant
+ self._alpha = 0.2 # always 0.2 since we always have the fifth part of the filter time constant
+ self.run.at(self.run.trigger.interval(None, sample_time), self._cb_cyclic_calculate_and_update_output)
- if self._config.parameter.instant_increase or self._config.parameter.instant_decrease:
- self._config.items.raw.listen_event(self._cb_item_raw, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.parameter.instant_increase or self._config.parameter.instant_decrease:
+ self._config.items.raw.listen_event(self._cb_item_raw, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._instance_logger.debug(f"Successfully created exponential filter for item {self._config.items.raw.name}")
+ self._instance_logger.debug(f"Successfully created exponential filter for item {self._config.items.raw.name}")
- def _cb_cyclic_calculate_and_update_output(self) -> None:
- """Calculate the new filter output and update the filtered item. This must be called cyclic"""
- new_value = self._config.items.raw.value
+ def _cb_cyclic_calculate_and_update_output(self) -> None:
+ """Calculate the new filter output and update the filtered item. This must be called cyclic."""
+ new_value = self._config.items.raw.value
- if any(not isinstance(value, (int, float)) for value in (self._previous_value, new_value)):
- self._instance_logger.warning(f"New or previous value is not a number: new_value: {new_value} | previous_value: {self._previous_value}")
- return
+ if any(not isinstance(value, int | float) for value in (self._previous_value, new_value)):
+ self._instance_logger.warning(f"New or previous value is not a number: new_value: {new_value} | previous_value: {self._previous_value}")
+ return
- self._send_output(filtered_value := self._alpha * new_value + (1 - self._alpha) * self._previous_value)
- self._previous_value = filtered_value
+ self._send_output(filtered_value := self._alpha * new_value + (1 - self._alpha) * self._previous_value)
+ self._previous_value = filtered_value
- def _cb_item_raw(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is called if the value of the raw item changed.
+ def _cb_item_raw(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is called if the value of the raw item changed.
- :param event: event which triggered this event
- """
- if self._previous_value is None or self._config.parameter.instant_increase and event.value > self._previous_value or self._config.parameter.instant_decrease and event.value < self._previous_value:
- self._send_output(event.value)
- self._previous_value = event.value
+ Args:
+ event: event which triggered this event
+ """
+ if self._previous_value is None or (self._config.parameter.instant_increase and event.value > self._previous_value) or (self._config.parameter.instant_decrease and event.value < self._previous_value):
+ self._send_output(event.value)
+ self._previous_value = event.value
- def _send_output(self, new_value: float) -> None:
- """Send output to the OpenHAB item.
+ def _send_output(self, new_value: float) -> None:
+ """Send output to the OpenHAB item.
- :param new_value: new value which should be sent
- """
- self._config.items.filtered.oh_send_command(new_value)
+ Args:
+ new_value: new value which should be sent
+ """
+ self._config.items.filtered.oh_send_command(new_value)
diff --git a/habapp_rules/common/hysteresis.py b/habapp_rules/common/hysteresis.py
index 21e4ce6..82cd525 100644
--- a/habapp_rules/common/hysteresis.py
+++ b/habapp_rules/common/hysteresis.py
@@ -1,4 +1,4 @@
-"""Module for hysteresis switch"""
+"""Module for hysteresis switch."""
import logging
@@ -6,54 +6,61 @@
class HysteresisSwitch:
- """Hysteresis switch"""
-
- def __init__(self, threshold_on: float, hysteresis: float, return_bool: bool = True):
- """Switch with hysteresis
- :param threshold_on: threshold for switching on
- :param hysteresis: hysteresis offset: threshold_off = threshold_on -hysteresis_offset
- :param return_bool: choose return-type: if true bool will be returned, else 'ON' / 'OFF'
- """
- self._threshold = threshold_on
- self._hysteresis = hysteresis
- self._return_bool = return_bool
- self._on_off_state = False
- self._value_last = 0
-
- def set_threshold_on(self, threshold_on: float) -> None:
- """Update threshold.
-
- :param threshold_on: new threshold value
- """
- self._threshold = threshold_on
- if self._hysteresis == float("inf"): # needed for habapp_rules.sensors.motion
- new_threshold = 0.1 * threshold_on
- LOGGER.warning(f"Hysteresis was not set and changed to {new_threshold} | threshold = {threshold_on}")
- self._hysteresis = new_threshold
-
- def get_output(self, value: float | None = None) -> bool | str:
- """Get output of hysteresis switch
- :param value: value which should be checked
- :return: on / off state
- """
- if self._threshold:
- # get threshold depending on the current state
- threshold = self._threshold - 0.5 * self._hysteresis if self._on_off_state else self._threshold + 0.5 * self._hysteresis
-
- # use new value if given, otherwise last value
- value = value if value is not None else self._value_last
-
- # get on / off state
- self._on_off_state = value >= threshold
- else:
- LOGGER.warning(f"Can not get output value for value = '{value}'. Threshold is not set correctly. self._threshold = {self._threshold}")
- self._on_off_state = False
-
- # save value for next check
- self._value_last = value
-
- # if on/off result is requested convert result
- if self._return_bool:
- return self._on_off_state
-
- return "ON" if self._on_off_state else "OFF"
+ """Hysteresis switch."""
+
+ def __init__(self, threshold_on: float, hysteresis: float, return_bool: bool = True) -> None:
+ """Switch with hysteresis.
+
+ Args:
+ threshold_on: threshold for switching on
+ hysteresis: hysteresis offset: threshold_off = threshold_on -hysteresis_offset
+ return_bool: choose return-type: if true bool will be returned, else 'ON' / 'OFF'.
+ """
+ self._threshold = threshold_on
+ self._hysteresis = hysteresis
+ self._return_bool = return_bool
+ self._on_off_state = False
+ self._value_last = 0
+
+ def set_threshold_on(self, threshold_on: float) -> None:
+ """Update threshold.
+
+ Args:
+ threshold_on: new threshold value
+ """
+ self._threshold = threshold_on
+ if self._hysteresis == float("inf"): # needed for habapp_rules.sensors.motion
+ new_threshold = 0.1 * threshold_on
+ LOGGER.warning(f"Hysteresis was not set and changed to {new_threshold} | threshold = {threshold_on}")
+ self._hysteresis = new_threshold
+
+ def get_output(self, value: float | None = None) -> bool | str:
+ """Get output of hysteresis switch.
+
+ Args:
+ value: value which should be checked
+
+ Returns:
+ on / off state.
+ """
+ if self._threshold:
+ # get threshold depending on the current state
+ threshold = self._threshold - 0.5 * self._hysteresis if self._on_off_state else self._threshold + 0.5 * self._hysteresis
+
+ # use new value if given, otherwise last value
+ value = value if value is not None else self._value_last
+
+ # get on / off state
+ self._on_off_state = value >= threshold
+ else:
+ LOGGER.warning(f"Can not get output value for value = '{value}'. Threshold is not set correctly. self._threshold = {self._threshold}")
+ self._on_off_state = False
+
+ # save value for next check
+ self._value_last = value
+
+ # if on/off result is requested convert result
+ if self._return_bool:
+ return self._on_off_state
+
+ return "ON" if self._on_off_state else "OFF"
diff --git a/habapp_rules/common/logic.py b/habapp_rules/common/logic.py
index 9b5beba..e8b83fc 100644
--- a/habapp_rules/common/logic.py
+++ b/habapp_rules/common/logic.py
@@ -1,4 +1,5 @@
"""Implementations of logical functions."""
+
import abc
import logging
@@ -12,282 +13,314 @@
class _BinaryLogicBase(HABApp.Rule):
- """Base class for binary logical functions."""
+ """Base class for binary logical functions."""
+
+ def __init__(self, config: habapp_rules.common.config.logic.BinaryLogicConfig) -> None:
+ """Init a logical function.
- def __init__(self, config: habapp_rules.common.config.logic.BinaryLogicConfig) -> None:
- """Init a logical function.
+ Args:
+ config: Config for logical function
- :param config: Config for logical function
- :raises TypeError: if unsupported item-type is given for output_name
- """
- HABApp.Rule.__init__(self)
- self._config = config
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
+ Raises:
+ TypeError: if unsupported item-type is given for output_name
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
- if isinstance(self._config.items.output, HABApp.openhab.items.SwitchItem):
- # item type is Switch
- self._positive_state = "ON"
- self._negative_state = "OFF"
- else:
- # item type is Contact (validated by type of config)
- self._positive_state = "CLOSED"
- self._negative_state = "OPEN"
+ if isinstance(self._config.items.output, HABApp.openhab.items.SwitchItem):
+ # item type is Switch
+ self._positive_state = "ON"
+ self._negative_state = "OFF"
+ else:
+ # item type is Contact (validated by type of config)
+ self._positive_state = "CLOSED"
+ self._negative_state = "OPEN"
- for item in self._config.items.inputs:
- item.listen_event(self._cb_input_event, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+ for item in self._config.items.inputs:
+ item.listen_event(self._cb_input_event, HABApp.openhab.events.ItemStateUpdatedEventFilter())
- self._cb_input_event(None)
- input_names = [item.name for item in self._config.items.inputs]
- self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input items = {input_names}")
+ self._cb_input_event(None)
+ input_names = [item.name for item in self._config.items.inputs]
+ self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input items = {input_names}")
- @abc.abstractmethod
- def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None:
- """Callback, which is called if one of the input items had a state event.
+ @abc.abstractmethod
+ def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None:
+ """Callback, which is called if one of the input items had a state event.
- :param event: item event of the updated item
- """
+ Args:
+ event: item event of the updated item
+ """
- def _set_output_state(self, output_state: str) -> None:
- """Set state to the output element
+ def _set_output_state(self, output_state: str) -> None:
+ """Set state to the output element.
- :param output_state: state which will be set
- """
- if isinstance(self._config.items.output, HABApp.openhab.items.ContactItem):
- self._config.items.output.oh_post_update(output_state)
- else:
- habapp_rules.core.helper.send_if_different(self._config.items.output, output_state)
+ Args:
+ output_state: state which will be set
+ """
+ if isinstance(self._config.items.output, HABApp.openhab.items.ContactItem):
+ self._config.items.output.oh_post_update(output_state)
+ else:
+ habapp_rules.core.helper.send_if_different(self._config.items.output, output_state)
class And(_BinaryLogicBase):
- """Logical AND function.
+ """Logical AND function.
- # Config:
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Item_1", "Item_2"],
- output="Item_result",
- )
- )
+ # Config:
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(
+ items=habapp_rules.common.config.logic.BinaryLogicItems(
+ inputs=["Item_1", "Item_2"],
+ output="Item_result",
+ )
+ )
- # Rule init:
- habapp_rules.common.logic.And(config)
- """
+ # Rule init:
+ habapp_rules.common.logic.And(config)
+ """
- def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None:
- """Callback, which is called if one of the input items had a state event.
+ def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None: # noqa: ARG002
+ """Callback, which is called if one of the input items had a state event.
- :param event: item event of the updated item
- """
- output_state = self._positive_state if all(item.value == self._positive_state for item in self._config.items.inputs) else self._negative_state
- self._set_output_state(output_state)
+ Args:
+ event: item event of the updated item
+ """
+ output_state = self._positive_state if all(item.value == self._positive_state for item in self._config.items.inputs) else self._negative_state
+ self._set_output_state(output_state)
class Or(_BinaryLogicBase):
- """Logical OR function.
+ """Logical OR function.
- # Config:
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Item_1", "Item_2"],
- output="Item_result",
- )
- )
+ # Config:
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(
+ items=habapp_rules.common.config.logic.BinaryLogicItems(
+ inputs=["Item_1", "Item_2"],
+ output="Item_result",
+ )
+ )
- # Rule init:
- habapp_rules.common.logic.Or(config)
- """
+ # Rule init:
+ habapp_rules.common.logic.Or(config)
+ """
- def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None:
- """Callback, which is called if one of the input items had a state event.
+ def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None: # noqa: ARG002
+ """Callback, which is called if one of the input items had a state event.
- :param event: item event of the updated item
- """
- output_state = self._positive_state if any(item.value == self._positive_state for item in self._config.items.inputs) else self._negative_state
- self._set_output_state(output_state)
+ Args:
+ event: item event of the updated item
+ """
+ output_state = self._positive_state if any(item.value == self._positive_state for item in self._config.items.inputs) else self._negative_state
+ self._set_output_state(output_state)
class _NumericLogicBase(HABApp.Rule):
- """Base class for numeric logical functions."""
+ """Base class for numeric logical functions."""
+
+ def __init__(self, config: habapp_rules.common.config.logic.NumericLogicConfig) -> None:
+ """Init a logical function.
+
+ Args:
+ config: Config for logical function
- def __init__(self, config: habapp_rules.common.config.logic.NumericLogicConfig) -> None:
- """Init a logical function.
+ Raises:
+ TypeError: if unsupported item-type is given for output_name
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
- :param config: Config for logical function
- :raises TypeError: if unsupported item-type is given for output_name
- """
- HABApp.Rule.__init__(self)
- self._config = config
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
+ for item in self._config.items.inputs:
+ item.listen_event(self._cb_input_event, HABApp.openhab.events.ItemStateChangedEventFilter())
- for item in self._config.items.inputs:
- item.listen_event(self._cb_input_event, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._cb_input_event(None)
+ input_names = [item.name for item in self._config.items.inputs]
+ self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input items = {input_names}")
- self._cb_input_event(None)
- input_names = [item.name for item in self._config.items.inputs]
- self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input items = {input_names}")
+ def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None: # noqa: ARG002
+ """Callback, which is called if one of the input items had a state event.
- def _cb_input_event(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | None) -> None:
- """Callback, which is called if one of the input items had a state event.
+ Args:
+ event: item event of the updated item
+ """
+ filtered_items = habapp_rules.core.helper.filter_updated_items(self._config.items.inputs, self._config.parameter.ignore_old_values_time)
+ value = self._apply_numeric_logic([item.value for item in filtered_items if item is not None])
- :param event: item event of the updated item
- """
- filtered_items = habapp_rules.core.helper.filter_updated_items(self._config.items.inputs, self._config.parameter.ignore_old_values_time)
- value = self._apply_numeric_logic([item.value for item in filtered_items if item is not None])
+ if value is None:
+ return
- if value is None:
- return
+ self._set_output_state(value)
- self._set_output_state(value)
+ @staticmethod
+ @abc.abstractmethod
+ def _apply_numeric_logic(input_values: list[float]) -> float:
+ """Apply numeric logic.
- @abc.abstractmethod
- def _apply_numeric_logic(self, input_values: list[float]) -> float:
- """Apply numeric logic
+ Args:
+ input_values: input values
- :param input_values: input values
- :return: value which fulfills the filter type
- """
+ Returns:
+ value which fulfills the filter type
+ """
- def _set_output_state(self, output_state: float) -> None:
- """Set state to the output element
+ def _set_output_state(self, output_state: float) -> None:
+ """Set state to the output element.
- :param output_state: state which will be set
- """
- habapp_rules.core.helper.send_if_different(self._config.items.output, output_state)
+ Args:
+ output_state: state which will be set
+ """
+ habapp_rules.core.helper.send_if_different(self._config.items.output, output_state)
class Min(_NumericLogicBase):
- """Logical Min function with filter for old / not updated items.
+ """Logical Min function with filter for old / not updated items.
- # Config:
- config = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Item_1", "Item_2"],
- output="Item_result",
- ),
- parameter=habapp_rules.common.config.logic.NumericLogicParameter(
- ignore_old_values_time=600
- ),
- )
+ # Config:
+ config = habapp_rules.common.config.logic.NumericLogicConfig(
+ items=habapp_rules.common.config.logic.NumericLogicItems(
+ inputs=["Item_1", "Item_2"],
+ output="Item_result",
+ ),
+ parameter=habapp_rules.common.config.logic.NumericLogicParameter(
+ ignore_old_values_time=600
+ ),
+ )
- # Rule init:
- habapp_rules.common.logic.Min(config)
- """
+ # Rule init:
+ habapp_rules.common.logic.Min(config)
+ """
- def _apply_numeric_logic(self, input_values: list[float]) -> float:
- """Apply numeric logic
+ @staticmethod
+ def _apply_numeric_logic(input_values: list[float]) -> float:
+ """Apply numeric logic.
- :param input_values: input values
- :return: min value of the given values
- """
- return HABApp.util.functions.min(input_values)
+ Args:
+ input_values: input values
+
+ Returns:
+ min value of the given values
+ """
+ return HABApp.util.functions.min(input_values)
class Max(_NumericLogicBase):
- """Logical Max function with filter for old / not updated items.
+ """Logical Max function with filter for old / not updated items.
+
+ # Config:
+ config = habapp_rules.common.config.logic.NumericLogicConfig(
+ items=habapp_rules.common.config.logic.NumericLogicItems(
+ inputs=["Item_1", "Item_2"],
+ output="Item_result",
+ ),
+ parameter=habapp_rules.common.config.logic.NumericLogicParameter(
+ ignore_old_values_time=600
+ ),
+ )
- # Config:
- config = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Item_1", "Item_2"],
- output="Item_result",
- ),
- parameter=habapp_rules.common.config.logic.NumericLogicParameter(
- ignore_old_values_time=600
- ),
- )
+ # Rule init:
+ habapp_rules.common.logic.Max(config)
+ """
- # Rule init:
- habapp_rules.common.logic.Max(config)
- """
+ @staticmethod
+ def _apply_numeric_logic(input_values: list[float]) -> float:
+ """Apply numeric logic.
- def _apply_numeric_logic(self, input_values: list[float]) -> float:
- """Apply numeric logic
+ Args:
+ input_values: input values
- :param input_values: input values
- :return: max value of the given values
- """
- return HABApp.util.functions.max(input_values)
+ Returns:
+ 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.
+ """Logical Sum function with filter for old / not updated items.
+
+ # Config:
+ config = habapp_rules.common.config.logic.NumericLogicConfig(
+ items=habapp_rules.common.config.logic.NumericLogicItems(
+ inputs=["Item_1", "Item_2"],
+ output="Item_result",
+ ),
+ parameter=habapp_rules.common.config.logic.NumericLogicParameter(
+ ignore_old_values_time=600
+ ),
+ )
+
+ # Rule init:
+ habapp_rules.common.logic.Sum(config)
+ """
- # Config:
- config = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Item_1", "Item_2"],
- output="Item_result",
- ),
- parameter=habapp_rules.common.config.logic.NumericLogicParameter(
- ignore_old_values_time=600
- ),
- )
+ def __init__(self, config: habapp_rules.common.config.logic.NumericLogicConfig) -> None:
+ """Init a logical function.
- # Rule init:
- habapp_rules.common.logic.Sum(config)
- """
+ Args:
+ config: config for logical sum rule
- def __init__(self, config: habapp_rules.common.config.logic.NumericLogicConfig) -> None:
- """Init a logical function.
+ Raises:
+ TypeError: if unsupported item-type is given for output_name
+ """
+ if isinstance(config.items.output, HABApp.openhab.items.DimmerItem):
+ msg = f"Dimmer items can not be used for Sum function! Given output_name: {config.items.output}"
+ raise TypeError(msg)
- :param config: config for logical sum rule
- :raises TypeError: if unsupported item-type is given for output_name
- """
- if isinstance(config.items.output, HABApp.openhab.items.DimmerItem):
- raise TypeError(f"Dimmer items can not be used for Sum function! Given output_name: {config.items.output}")
+ _NumericLogicBase.__init__(self, config)
- _NumericLogicBase.__init__(self, config)
+ @staticmethod
+ def _apply_numeric_logic(input_values: list[float]) -> float:
+ """Apply numeric logic.
- def _apply_numeric_logic(self, input_values: list[float]) -> float:
- """Apply numeric logic
+ Args:
+ input_values: input values
- :param input_values: input values
- :return: min value of the given values
- """
- return sum(val for val in input_values if val is not None)
+ Returns:
+ min value of the given values
+ """
+ return sum(val for val in input_values if val is not None)
class InvertValue(HABApp.Rule):
- """Rule to update another item if the value of an item changed.
+ """Rule to update another item if the value of an item changed.
- # Config:
- config = habapp_rules.common.config.logic.InvertValueConfig(
- items=habapp_rules.common.config.logic.InvertValueItems(
- input="Item_1",
- output="Item_2",
- )
- )
+ # Config:
+ config = habapp_rules.common.config.logic.InvertValueConfig(
+ items=habapp_rules.common.config.logic.InvertValueItems(
+ input="Item_1",
+ output="Item_2",
+ )
+ )
- # Rule init:
- habapp_rules.common.logic.InvertValue(config)
- """
+ # Rule init:
+ habapp_rules.common.logic.InvertValue(config)
+ """
- def __init__(self, config: habapp_rules.common.config.logic.InvertValueConfig) -> None:
- """Init rule.
+ def __init__(self, config: habapp_rules.common.config.logic.InvertValueConfig) -> None:
+ """Init rule.
- :param config: Config for invert value rule
- """
- HABApp.Rule.__init__(self)
- self._config = config
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
+ Args:
+ config: Config for invert value rule
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, f"{self.__class__.__name__}_{self._config.items.output.name}")
- self._config.items.input.listen_event(self._cb_input_value, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._cb_input_value(HABApp.openhab.events.ItemStateChangedEvent(self._config.items.input.name, self._config.items.input.value, None))
- self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input item = '{self._config.items.input.name}'")
+ self._config.items.input.listen_event(self._cb_input_value, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._cb_input_value(HABApp.openhab.events.ItemStateChangedEvent(self._config.items.input.name, self._config.items.input.value, None))
+ self._instance_logger.debug(f"Init of rule '{self.__class__.__name__}' with was successful. Output item = '{self._config.items.output.name}' | Input item = '{self._config.items.input.name}'")
- def _cb_input_value(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Set output, when input value changed
+ def _cb_input_value(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Set output, when input value changed.
- :param event: event, which triggered this callback
- """
- if event.value is None:
- return
+ Args:
+ event: event, which triggered this callback
+ """
+ if event.value is None:
+ return
- output_value = -1 * event.value
+ output_value = -1 * event.value
- if self._config.parameter.only_negative and output_value > 0:
- output_value = 0
- elif self._config.parameter.only_positive and output_value < 0:
- output_value = 0
+ if (self._config.parameter.only_negative and output_value > 0) or (self._config.parameter.only_positive and output_value < 0):
+ output_value = 0
- self._config.items.output.oh_send_command(output_value)
+ self._config.items.output.oh_send_command(output_value)
diff --git a/habapp_rules/core/exceptions.py b/habapp_rules/core/exceptions.py
index 4c04314..bd51986 100644
--- a/habapp_rules/core/exceptions.py
+++ b/habapp_rules/core/exceptions.py
@@ -1,9 +1,9 @@
"""Exceptions for HabAppRules."""
-class HabAppRulesException(Exception):
- """Exception which is raised by this package."""
+class HabAppRulesError(Exception):
+ """Exception which is raised by this package."""
-class HabAppRulesConfigurationException(HabAppRulesException):
- """Exception which is raised if wrong configuration is given"""
+class HabAppRulesConfigurationError(HabAppRulesError):
+ """Exception which is raised if wrong configuration is given."""
diff --git a/habapp_rules/core/helper.py b/habapp_rules/core/helper.py
index 68c00eb..44a953c 100644
--- a/habapp_rules/core/helper.py
+++ b/habapp_rules/core/helper.py
@@ -1,4 +1,5 @@
"""Common helper functions for all rules."""
+
import logging
import time
@@ -12,56 +13,65 @@
def create_additional_item(name: str, item_type: str, label: str | None = None, groups: list[str] | None = None) -> HABApp.openhab.items.OpenhabItem:
- """Create additional item if it does not already exist
+ """Create additional item if it does not already exist.
+
+ Args:
+ name: Name of item
+ item_type: Type of item (e.g. String)
+ label: Label of the item
+ groups: in which groups is the item
- :param name: Name of item
- :param item_type: Type of item (e.g. String)
- :param label: Label of the item
- :param groups: in which groups is the item
- :return: returns the created item
- :raises habapp_rules.core.exceptions.HabAppRulesException: if item could not be created
- """
- if not name.startswith("H_"):
- LOGGER.warning(f"Item '{name}' does not start with 'H_'. All automatically created items must start with 'H_'. habapp_rules will add 'H_' automatically.")
- name = f"H_{name}"
+ Returns:
+ returns the created item
- if not HABApp.openhab.interface_sync.item_exists(name):
- if not label:
- label = f"{name.removeprefix('H_').replace('_', ' ')}"
- if not HABApp.openhab.interface_sync.create_item(item_type=item_type, name=name, label=label, groups=groups):
- raise habapp_rules.core.exceptions.HabAppRulesException(f"Could not create item '{name}'")
- time.sleep(0.05)
- return HABApp.openhab.items.OpenhabItem.get_item(name)
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesError: if item could not be created
+ """
+ if not name.startswith("H_"):
+ LOGGER.warning(f"Item '{name}' does not start with 'H_'. All automatically created items must start with 'H_'. habapp_rules will add 'H_' automatically.")
+ name = f"H_{name}"
+ if not HABApp.openhab.interface_sync.item_exists(name):
+ if not label:
+ label = f"{name.removeprefix('H_').replace('_', ' ')}"
+ if not HABApp.openhab.interface_sync.create_item(item_type=item_type, name=name, label=label, groups=groups):
+ msg = f"Could not create item '{name}'"
+ raise habapp_rules.core.exceptions.HabAppRulesError(msg)
+ time.sleep(0.05)
+ return HABApp.openhab.items.OpenhabItem.get_item(name)
-def send_if_different(item: str | HABApp.openhab.items.OpenhabItem, value: str | float | int) -> None:
- """Send command if the target value is different to the current value.
- :param item: name of OpenHab item
- :param value: value to write to OpenHAB item
- """
- if isinstance(item, str):
- item = HABApp.openhab.items.OpenhabItem.get_item(item)
+def send_if_different(item: str | HABApp.openhab.items.OpenhabItem, value: str | float) -> None:
+ """Send command if the target value is different to the current value.
- if item.value != value:
- item.oh_send_command(value)
+ Args:
+ item: name of OpenHab item
+ value: value to write to OpenHAB item
+ """
+ if isinstance(item, str):
+ item = HABApp.openhab.items.OpenhabItem.get_item(item)
+
+ if item.value != value:
+ item.oh_send_command(value)
def filter_updated_items(input_items: list[HABApp.openhab.items.OpenhabItem], filter_time: int | None = None) -> list[HABApp.openhab.items.OpenhabItem]:
- """Get input items depending on their last update time and _ignore_old_values_time
+ """Get input items depending on their last update time and _ignore_old_values_time.
+
+ Args:
+ input_items: all items which should be checked for last update time
+ filter_time: threshold for last update time
- :param input_items: all items which should be checked for last update time
- :param filter_time: threshold for last update time
- :return: full list if _ignore_old_values is not set, otherwise all items where updated in time.
- """
- if filter_time is None:
- return input_items
+ Returns:
+ full list if _ignore_old_values is not set, otherwise all items where updated in time.
+ """
+ if filter_time is None:
+ return input_items
- current_time = time.time()
- filtered_items = [item for item in input_items if current_time - item.last_update.timestamp() <= filter_time]
+ filtered_items = [item for item in input_items if item.last_update.newer_than(filter_time)]
- if len(input_items) != len(filtered_items):
- ignored_item_names = [item.name for item in input_items if current_time - item.last_update.timestamp() > filter_time]
- LOGGER.warning(f"The following items where not updated during the last {filter_time}s and will be ignored: {ignored_item_names}")
+ if len(input_items) != len(filtered_items):
+ ignored_item_names = [item.name for item in input_items if item.last_update.older_than(filter_time)]
+ LOGGER.warning(f"The following items where not updated during the last {filter_time}s and will be ignored: {ignored_item_names}")
- return filtered_items
+ return filtered_items
diff --git a/habapp_rules/core/logger.py b/habapp_rules/core/logger.py
index 0075668..b4f4de2 100644
--- a/habapp_rules/core/logger.py
+++ b/habapp_rules/core/logger.py
@@ -1,59 +1,63 @@
"""Setup logger."""
+
import collections.abc
import logging
-import os
import typing
import HABApp.config.config
import HABApp.config.logging
-import habapp_rules.__version__
+import habapp_rules
LOG_PATH = HABApp.config.config.CONFIG.directories.logging.absolute()
def setup_logger() -> None:
- """Setup the logger"""
- log_formatter = logging.Formatter("%(asctime)s.%(msecs)03d | %(threadName)20s | %(levelname)8s | %(name)s:%(lineno)d | %(message)s", datefmt="%Y-%m-%d | %H:%M:%S")
- habapp_rules_logger = logging.getLogger("habapp_rules")
- habapp_rules_logger.setLevel(logging.DEBUG)
+ """Setup the logger."""
+ log_formatter = logging.Formatter("%(asctime)s.%(msecs)03d | %(threadName)20s | %(levelname)8s | %(name)s:%(lineno)d | %(message)s", datefmt="%Y-%m-%d | %H:%M:%S")
+ habapp_rules_logger = logging.getLogger("habapp_rules")
+ habapp_rules_logger.setLevel(logging.DEBUG)
- console_handler = logging.StreamHandler()
- console_handler.setFormatter(log_formatter)
- console_handler.setLevel(logging.DEBUG)
- habapp_rules_logger.addHandler(console_handler)
+ console_handler = logging.StreamHandler()
+ console_handler.setFormatter(log_formatter)
+ console_handler.setLevel(logging.DEBUG)
+ habapp_rules_logger.addHandler(console_handler)
- if not LOG_PATH.is_dir():
- os.makedirs(LOG_PATH)
+ if not LOG_PATH.is_dir():
+ LOG_PATH.mkdir(parents=True)
- file_handler = HABApp.config.logging.MidnightRotatingFileHandler(LOG_PATH / "habapp_rules.log", encoding="utf-8", maxBytes=1_048_576, backupCount=5)
- file_handler.setFormatter(log_formatter)
- file_handler.setLevel(logging.DEBUG)
- habapp_rules_logger.addHandler(file_handler)
+ file_handler = HABApp.config.logging.MidnightRotatingFileHandler(LOG_PATH / "habapp_rules.log", encoding="utf-8", maxBytes=1_048_576, backupCount=5)
+ file_handler.setFormatter(log_formatter)
+ file_handler.setLevel(logging.DEBUG)
+ habapp_rules_logger.addHandler(file_handler)
class InstanceLogger(logging.LoggerAdapter):
- """Logging adapter to add the instance name to the log message."""
+ """Logging adapter to add the instance name to the log message."""
+
+ def __init__(self, logger: logging.Logger, instance_name: str) -> None:
+ """Instantiate a logging adapter for multiple instances.
- def __init__(self, logger: logging.Logger, instance_name: str) -> None:
- """Instantiate a logging adapter for multiple instances.
+ Args:
+ logger: the underlying logger e.g. module logger
+ instance_name: the name of the instance
+ """
+ self._instance_name = instance_name
+ logging.LoggerAdapter.__init__(self, logger)
- :param logger: the underlying logger e.g. module logger
- :param instance_name: the name of the instance
- """
- self._instance_name = instance_name
- logging.LoggerAdapter.__init__(self, logger)
+ def process(self, msg: str, kwargs: collections.abc.MutableMapping[str, typing.Any]) -> tuple[str, collections.abc.MutableMapping[str, typing.Any]]:
+ """Add the instance name to log message.
- def process(self, msg: str, kwargs: collections.abc.MutableMapping[str, typing.Any]) -> tuple[str, collections.abc.MutableMapping[str, typing.Any]]:
- """Add the instance name to log message.
+ Args:
+ msg: the log message
+ kwargs: additional keyword arguments.
- :param msg: the log message
- :param kwargs: additional keyword arguments.
- :return: tuple of msg with given keyword arguments
- """
- return f"{self._instance_name} | {msg}", kwargs
+ Returns:
+ tuple of msg with given keyword arguments
+ """
+ return f"{self._instance_name} | {msg}", kwargs
setup_logger()
LOGGER = logging.getLogger(__name__)
-LOGGER.info(f"Start logging of habapp_rules. Version = {habapp_rules.__version__.__version__}")
+LOGGER.info(f"Start logging of habapp_rules. Version = {habapp_rules.__version__}")
diff --git a/habapp_rules/core/pydantic_base.py b/habapp_rules/core/pydantic_base.py
index 302697c..85a84e9 100644
--- a/habapp_rules/core/pydantic_base.py
+++ b/habapp_rules/core/pydantic_base.py
@@ -1,4 +1,5 @@
"""Base classes for pydantic config models."""
+
import types
import typing
@@ -10,118 +11,146 @@
class ItemBase(pydantic.BaseModel):
- """Base class for item config."""
- model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
+ """Base class for item config."""
+
+ model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
+
+ @pydantic.model_validator(mode="before")
+ @classmethod
+ def check_all_fields_oh_items(cls, data: typing.Any) -> typing.Any: # noqa: ANN401
+ """Validate that all fields are OpenHAB items.
+
+ All items must be subclasses of `HABApp.openhab.items.OpenhabItem`.
+ If create_if_not_exists is set, only one type is allowed.
+ For lists, only one type is allowed.
+
+ Args:
+ data: data object given by pydantic
+
+ Returns:
+ data object
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if validation fails
+ """
+ for name, field_info in cls.model_fields.items():
+ field_types = cls._get_type_of_field(name)
+ extra_args = extra if (extra := field_info.json_schema_extra) else {}
- @pydantic.model_validator(mode="before")
- @classmethod
- def check_all_fields_oh_items(cls, data: typing.Any) -> typing.Any:
- """Validate that all fields are OpenHAB items.
+ if isinstance(field_types, types.GenericAlias):
+ # type is list of OpenHAB items
+ field_types = typing.get_args(field_types)[0]
- All items must be subclasses of `HABApp.openhab.items.OpenhabItem`.
- If create_if_not_exists is set, only one type is allowed.
- For lists, only one type is allowed.
+ if isinstance(field_types, types.UnionType):
+ field_types = [arg for arg in typing.get_args(field_types) if arg is not types.NoneType]
- :param data: data object given by pydantic
- :return: data object
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if validation fails
- """
- for name, field_info in cls.model_fields.items():
- field_types = cls._get_type_of_field(name)
- extra_args = extra if (extra := field_info.json_schema_extra) else {}
+ # validate that create_if_not_exists is not set for lists
+ if extra_args.get("create_if_not_exists", False):
+ msg = "create_if_not_exists is not allowed for list fields"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
- if isinstance(field_types, types.GenericAlias):
- # type is list of OpenHAB items
- field_types = typing.get_args(field_types)[0]
+ field_types = field_types if isinstance(field_types, list) else [field_types]
- if isinstance(field_types, types.UnionType):
- field_types = [arg for arg in typing.get_args(field_types) if arg is not types.NoneType]
+ for field_type in field_types:
+ if not issubclass(field_type, HABApp.openhab.items.OpenhabItem):
+ msg = f"Field {field_type} is not an OpenhabItem"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
- # validate that create_if_not_exists is not set for lists
- if extra_args.get("create_if_not_exists", False):
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException("create_if_not_exists is not allowed for list fields")
+ # validate that only one type is given if create_if_not_exists is set
+ if extra_args.get("create_if_not_exists", False) and len(field_types) > 1:
+ msg = "If create_if_not_exists is set, only one type is allowed"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
- field_types = field_types if isinstance(field_types, list) else [field_types]
+ return data
- for field_type in field_types:
- if not issubclass(field_type, HABApp.openhab.items.OpenhabItem):
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"Field {field_type} is not an OpenhabItem")
+ @pydantic.field_validator("*", mode="before")
+ @classmethod
+ def convert_to_oh_item(cls, var: str | HABApp.openhab.items.OpenhabItem, validation_info: pydantic.ValidationInfo) -> HABApp.openhab.items.OpenhabItem | None:
+ """Convert to OpenHAB item.
- # validate that only one type is given if create_if_not_exists is set
- if extra_args.get("create_if_not_exists", False) and len(field_types) > 1:
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException("If create_if_not_exists is set, only one type is allowed")
+ Args:
+ var: variable given by the user
+ validation_info: validation info given by pydantic
- return data
+ Returns:
+ variable converted to OpenHAB item
- @pydantic.field_validator("*", mode="before")
- @classmethod
- def convert_to_oh_item(cls, var: str | HABApp.openhab.items.OpenhabItem, validation_info: pydantic.ValidationInfo) -> HABApp.openhab.items.OpenhabItem | None:
- """Convert to OpenHAB item.
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if type is not supported
+ """
+ extra_args = extra if (extra := cls.model_fields[validation_info.field_name].json_schema_extra) else {}
+ create_if_not_exists = extra_args.get("create_if_not_exists", False)
- :param var: variable given by the user
- :param validation_info: validation info given by pydantic
- :return: variable converted to OpenHAB item
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if type is not supported
- """
+ if create_if_not_exists:
+ item_type = cls._get_type_of_field(validation_info.field_name).__qualname__.removesuffix("Item")
+ return habapp_rules.core.helper.create_additional_item(var, item_type)
- extra_args = extra if (extra := cls.model_fields[validation_info.field_name].json_schema_extra) else {}
- create_if_not_exists = extra_args.get("create_if_not_exists", False)
+ if isinstance(var, list):
+ return [cls._get_oh_item(itm) for itm in var]
- if create_if_not_exists:
- item_type = cls._get_type_of_field(validation_info.field_name).__qualname__.removesuffix("Item")
- return habapp_rules.core.helper.create_additional_item(var, item_type)
+ if issubclass(type(var), HABApp.openhab.items.OpenhabItem) or isinstance(var, str):
+ return cls._get_oh_item(var)
- if isinstance(var, list):
- return [cls._get_oh_item(itm) for itm in var]
+ if var is None:
+ return None
- if issubclass(type(var), HABApp.openhab.items.OpenhabItem) or isinstance(var, str):
- return cls._get_oh_item(var)
+ msg = f"The following var is not supported: {var}"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
- if var is None:
- return None
+ @staticmethod
+ def _get_oh_item(item: str | HABApp.openhab.items.OpenhabItem) -> HABApp.openhab.items.OpenhabItem:
+ """Get OpenHAB item from string or OpenHAB item.
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"The following var is not supported: {var}")
+ Args:
+ item: name of OpenHAB item or OpenHAB item
- @staticmethod
- def _get_oh_item(item: str | HABApp.openhab.items.OpenhabItem) -> HABApp.openhab.items.OpenhabItem:
- """get OpenHAB item from string or OpenHAB item
+ Returns:
+ OpenHAB item
- :param item: name of OpenHAB item or OpenHAB item
- :return: OpenHAB item
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if type is not supported
- """
- return item if isinstance(item, HABApp.openhab.items.OpenhabItem) else HABApp.openhab.items.OpenhabItem.get_item(item)
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if type is not supported
+ """
+ return item if isinstance(item, HABApp.openhab.items.OpenhabItem) else HABApp.openhab.items.OpenhabItem.get_item(item)
- @classmethod
- def _get_type_of_field(cls, field_name: str) -> type | list[type]:
- """Get type of field
+ @classmethod
+ def _get_type_of_field(cls, field_name: str) -> type | list[type]:
+ """Get type of field.
- :param field_name: name of field
- :return: type of field, NoneType will be ignored
- """
- field_type = cls.model_fields[field_name].annotation
- if isinstance(field_type, types.UnionType):
- field_type = [arg for arg in typing.get_args(field_type) if arg is not types.NoneType]
- return field_type
+ Args:
+ field_name: name of field
+
+ Returns:
+ type of field, NoneType will be ignored
+ """
+ field_type = cls.model_fields[field_name].annotation
+ if isinstance(field_type, types.UnionType):
+ field_type = [arg for arg in typing.get_args(field_type) if arg is not types.NoneType]
+ return field_type
class ParameterBase(pydantic.BaseModel):
- """Base class for parameter config."""
- model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
+ """Base class for parameter config."""
+
+ model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
class ConfigBase(pydantic.BaseModel):
- """Base class for config objects."""
- items: ItemBase | None
- parameter: ParameterBase | None
-
- def __init__(self, **data: typing.Any) -> None:
- """Initialize the model.
-
- :param data: data object given by pydantic
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if validation fails
- """
- try:
- super().__init__(**data)
- except pydantic.ValidationError as exc:
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"Failed to validate model: {exc.errors()}")
+ """Base class for config objects."""
+
+ items: ItemBase | None
+ parameter: ParameterBase | None
+
+ def __init__(self, **data: typing.Any) -> None: # noqa: ANN401
+ """Initialize the model.
+
+ Args:
+ data: data object given by pydantic
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if validation fails
+ """
+ try:
+ super().__init__(**data)
+ except pydantic.ValidationError as exc:
+ msg = f"Failed to validate model: {exc.errors()}"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg) from exc
diff --git a/habapp_rules/core/state_machine_rule.py b/habapp_rules/core/state_machine_rule.py
index f7e7d49..72bfbbf 100644
--- a/habapp_rules/core/state_machine_rule.py
+++ b/habapp_rules/core/state_machine_rule.py
@@ -1,76 +1,85 @@
"""Base class for Rule with State Machine."""
+
import threading
+import typing
import HABApp
import HABApp.openhab.connection.handler.func_sync
import transitions.extensions.states
-
@transitions.extensions.states.add_state_features(transitions.extensions.states.Timeout)
class StateMachineWithTimeout(transitions.Machine):
- """State machine class with timeout"""
+ """State machine class with timeout."""
@transitions.extensions.states.add_state_features(transitions.extensions.states.Timeout)
class HierarchicalStateMachineWithTimeout(transitions.extensions.HierarchicalMachine):
- """Hierarchical state machine class with timeout"""
+ """Hierarchical state machine class with timeout."""
class StateMachineRule(HABApp.Rule):
- """Base class for creating rules with a state machine."""
- states: list[dict] = []
- trans: list[dict] = []
- state: str
-
- def __init__(self, state_item: HABApp.openhab.items.StringItem) -> None:
- """Init rule with state machine.
-
- :param state_item: name of the item to hold the state
- """
- self.state_machine: transitions.Machine | None = None
- HABApp.Rule.__init__(self)
-
- self._item_state = state_item
-
- def get_initial_log_message(self) -> str:
- """Get log message which can be logged at the init of a rule with a state machine.
-
- :return: log message
- """
- return f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful. Initial state = '{self.state}' | State item = '{self._item_state.name}'"
-
- def _get_initial_state(self, default_value: str = "initial") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._item_state.value and self._item_state.value in [item.get("name", None) for item in self.states if isinstance(item, dict)]:
- return self._item_state.value
- return default_value
-
- def _set_initial_state(self) -> None:
- """Set initial state.
- if the ``initial_state`` parameter of the state machine constructor is used the timeouts will not be started for the initial state.
- """
- self._set_state(self._get_initial_state())
-
- def _set_state(self, state_name: str) -> None:
- """Set given state
-
- :param state_name: name of state
- """
- eval(f"self.to_{state_name}()") # pylint: disable=eval-used
-
- 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.get_state(self.state).runner.values():
- if isinstance(itm, threading.Timer) and itm.is_alive():
- itm.cancel()
+ """Base class for creating rules with a state machine."""
+
+ states: typing.ClassVar[list[dict]] = []
+ trans: typing.ClassVar[list[dict]] = []
+ state: str
+
+ def __init__(self, state_item: HABApp.openhab.items.StringItem) -> None:
+ """Init rule with state machine.
+
+ Args:
+ state_item: name of the item to hold the state
+ """
+ self.state_machine: transitions.Machine | None = None
+ HABApp.Rule.__init__(self)
+
+ self._item_state = state_item
+
+ def get_initial_log_message(self) -> str:
+ """Get log message which can be logged at the init of a rule with a state machine.
+
+ Returns:
+ log message
+ """
+ return f"Init of rule '{self.__class__.__name__}' with name '{self.rule_name}' was successful. Initial state = '{self.state}' | State item = '{self._item_state.name}'"
+
+ def _get_initial_state(self, default_value: str = "initial") -> str:
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._item_state.value and self._item_state.value in [item.get("name", None) for item in self.states if isinstance(item, dict)]:
+ return self._item_state.value
+ return default_value
+
+ def _set_initial_state(self) -> None:
+ """Set initial state.
+
+ if the ``initial_state`` parameter of the state machine constructor is used the timeouts will not be started for the initial state.
+ """
+ self._set_state(self._get_initial_state())
+
+ def _set_state(self, state_name: str) -> None: # noqa: PLR6301
+ """Set given state.
+
+ Args:
+ state_name: name of state
+ """
+ eval(f"self.to_{state_name}()") # noqa: S307
+
+ 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.get_state(self.state).runner.values():
+ if isinstance(itm, threading.Timer) and itm.is_alive():
+ itm.cancel()
diff --git a/habapp_rules/core/timeout_list.py b/habapp_rules/core/timeout_list.py
index 07c1d76..0509c17 100644
--- a/habapp_rules/core/timeout_list.py
+++ b/habapp_rules/core/timeout_list.py
@@ -1,105 +1,135 @@
-"""Test for timeout_list"""
+"""Test for timeout_list."""
+
import dataclasses
import time
-import typing
@dataclasses.dataclass
class ValueWithTimeout:
- """Define item for TimeoutList"""
- value: typing.Any
- timeout: float
- add_timestamp: float
+ """Define item for TimeoutList."""
+
+ value: object
+ timeout: float
+ add_timestamp: float
class TimeoutList:
- """List like class, where every item has a timeout, which will remove it from the list."""
+ """List like class, where every item has a timeout, which will remove it from the list."""
+
+ def __init__(self) -> None:
+ """Init class."""
+ self.__items: list[ValueWithTimeout] = []
+
+ def __repr__(self) -> str:
+ """Get representation of all list items (without timeout).
+
+ Returns:
+ all list elements which are currently in the list
+ """
+ self.__remove_old_items()
+ return str([itm.value for itm in self.__items])
+
+ def __bool__(self) -> bool:
+ """Check if list has items.
+
+ Returns:
+ true if items in list
+ """
+ self.__remove_old_items()
+ return bool(self.__items)
+
+ def __contains__(self, item: object) -> bool:
+ """Check if an item is in the list.
+
+ Args:
+ item: item which should be checked
+
+ Returns:
+ true if item is in the list
+ """
+ self.__remove_old_items()
+ return item in [itm.value for itm in self.__items]
- def __init__(self) -> None:
- """Init class"""
- self.__items: list[ValueWithTimeout] = []
+ def __getitem__(self, index: int) -> object:
+ """Get item from list by index.
- def __repr__(self) -> str:
- """Get representation of all list items (without timeout)
+ Args:
+ index: index of item position in list.
- :return: all list elements which are currently in the list
- """
- self.__remove_old_items()
- return str([itm.value for itm in self.__items])
+ Returns:
+ item from list
+ """
+ self.__remove_old_items()
+ return self.__items[index].value
- def __bool__(self) -> bool:
- """Check if list has items
+ def __eq__(self, other: object) -> bool:
+ """Check if equal.
- :return: true if items in list
- """
- self.__remove_old_items()
- return bool(self.__items)
+ Args:
+ other: other item
- def __contains__(self, item: typing.Any) -> bool:
- """Check if an item is in the list
+ Returns:
+ true if equal
+ """
+ if isinstance(other, TimeoutList):
+ return repr(self) == repr(other)
- :param item: item which should be checked
- :return: true if item is in the list
- """
- self.__remove_old_items()
- return item in [itm.value for itm in self.__items]
+ if isinstance(other, list):
+ self.__remove_old_items()
+ return [itm.value for itm in self.__items] == other
- def __getitem__(self, index: int) -> typing.Any:
- """Get item from list by index
+ return False
- :param index: index of item position in list.
- :return: item from list
- """
- self.__remove_old_items()
- return self.__items[index].value
+ def __hash__(self) -> int:
+ """Get hash of the TimeoutList object.
- def __eq__(self, other: typing.Any) -> bool:
- """Check if equal.
+ Returns:
+ hash value
+ """
+ return hash(tuple(itm.value for itm in self.__items))
- :param other: other item
- :return: true if equal
- """
- if isinstance(other, TimeoutList):
- return repr(self) == repr(other)
+ def __remove_old_items(self) -> None:
+ """Remove items from list, which are timed-out."""
+ current_time = time.time()
+ self.__items = [itm for itm in self.__items if current_time - itm.add_timestamp < itm.timeout]
- if isinstance(other, list):
- self.__remove_old_items()
- return [itm.value for itm in self.__items] == other
+ def append(self, item: object, timeout: float) -> None:
+ """Add item to list.
- return False
+ Args:
+ item: item which should be added to the list
+ timeout: timeout, after which the item is not valid anymore
+ """
+ self.__items.append(ValueWithTimeout(item, timeout, time.time()))
- def __remove_old_items(self) -> None:
- """Remove items from list, which are timed-out."""
- current_time = time.time()
- self.__items = [itm for itm in self.__items if current_time - itm.add_timestamp < itm.timeout]
+ def remove(self, item: object) -> None:
+ """Remove item from list. If there are duplicates. The first element will be removed.
- def append(self, item: typing.Any, timeout: float) -> None:
- """Add item to list
+ Args:
+ item: item which should be deleted
- :param item: item which should be added to the list
- :param timeout: timeout, after which the item is not valid anymore
- """
- self.__items.append(ValueWithTimeout(item, timeout, time.time()))
+ Raises:
+ ValueError: if item not in list
+ """
+ item_to_remove = next((itm for itm in self.__items if itm.value == item), None)
- def remove(self, item: typing.Any) -> None:
- """Remove item from list. If there are duplicates. The first element will be removed.
+ if not item_to_remove:
+ msg = f"{self.__class__.__name__}.remove(x): x not in list"
+ raise ValueError(msg)
- :param item: item which should be deleted
- :raises ValueError: if item not in list
- """
- item_to_remove = next((itm for itm in self.__items if itm.value == item), None)
+ self.__items.remove(item_to_remove)
- if not item_to_remove:
- raise ValueError(f"{self.__class__.__name__}.remove(x): x not in list")
+ def pop(self, element_index: int) -> object:
+ """Pop item from list.
- self.__items.remove(item_to_remove)
+ Args:
+ element_index: list index of element which should be deleted
- def pop(self, element_index: int) -> typing.Any:
- """Pop item from list
+ Returns:
+ item which was removed
- :param element_index: list index of element which should be deleted
- :return: item which was removed
- :raises IndexError: if index is out of range
- :raises TypeError: if index can not be interpreted as integer
- """
- return self.__items.pop(element_index).value
+ Raises:
+ IndexError: if index is out of range
+ TypeError: if index can not be interpreted as integer
+ """
+ return self.__items.pop(element_index).value
diff --git a/habapp_rules/core/type_of_day.py b/habapp_rules/core/type_of_day.py
index 0405905..5a11f3e 100644
--- a/habapp_rules/core/type_of_day.py
+++ b/habapp_rules/core/type_of_day.py
@@ -1,26 +1,35 @@
"""Get type of day."""
+
import datetime
import holidays
+from habapp_rules import TIMEZONE
+
_BY_HOLIDAYS = holidays.country_holidays("DE", "BY")
def is_weekend(offset_days: int = 0) -> bool:
- """Check if current (or offset day) is a weekend day
+ """Check if current (or offset day) is a weekend day.
- :param offset_days: 0 means today, 1 tomorrow, -1 yesterday
- :return: True if desired day is a weekend day
- """
- day_to_check = datetime.datetime.today() + datetime.timedelta(days=offset_days)
- return day_to_check.isoweekday() in {6, 7}
+ Args:
+ offset_days: 0 means today, 1 tomorrow, -1 yesterday
+
+ Returns:
+ True if desired day is a weekend day
+ """
+ day_to_check = datetime.datetime.now(tz=TIMEZONE) + datetime.timedelta(days=offset_days)
+ return day_to_check.isoweekday() in {6, 7}
def is_holiday(offset_days: int = 0) -> bool:
- """Check if current (or offset day) is holiday
+ """Check if current (or offset day) is holiday.
+
+ Args:
+ offset_days: 0 means today, 1 tomorrow, -1 yesterday
- :param offset_days: 0 means today, 1 tomorrow, -1 yesterday
- :return: True if desired day is holiday
- """
- day_to_check = datetime.datetime.today() + datetime.timedelta(days=offset_days)
- return day_to_check in _BY_HOLIDAYS
+ Returns:
+ True if desired day is holiday
+ """
+ day_to_check = datetime.datetime.now(tz=TIMEZONE) + datetime.timedelta(days=offset_days)
+ return day_to_check in _BY_HOLIDAYS
diff --git a/habapp_rules/core/version.py b/habapp_rules/core/version.py
index c15b0f5..caa1230 100644
--- a/habapp_rules/core/version.py
+++ b/habapp_rules/core/version.py
@@ -1,24 +1,25 @@
"""Set version rules."""
+
import logging
import HABApp
-import habapp_rules.__version__
+import habapp_rules
import habapp_rules.core.helper
LOGGER = logging.getLogger(__name__)
class SetVersions(HABApp.Rule):
- """Update HABApp and habapp_rules version to OpenHAB items."""
+ """Update HABApp and habapp_rules version to OpenHAB items."""
- def __init__(self) -> None:
- """init rule."""
- HABApp.Rule.__init__(self)
- LOGGER.info("Update versions of OpenHAB items")
+ def __init__(self) -> None:
+ """Init rule."""
+ HABApp.Rule.__init__(self)
+ LOGGER.info("Update versions of OpenHAB items")
- item_version_habapp = habapp_rules.core.helper.create_additional_item("H_habapp_version", "String", "HABApp version")
- item_version_habapp_rules = habapp_rules.core.helper.create_additional_item("H_habapp_rules_version", "String", "habapp_rules version")
+ item_version_habapp = habapp_rules.core.helper.create_additional_item("H_habapp_version", "String", "HABApp version")
+ item_version_habapp_rules = habapp_rules.core.helper.create_additional_item("H_habapp_rules_version", "String", "habapp_rules version")
- item_version_habapp.oh_send_command(HABApp.__version__)
- item_version_habapp_rules.oh_send_command(habapp_rules.__version__.__version__)
+ item_version_habapp.oh_send_command(HABApp.__version__)
+ item_version_habapp_rules.oh_send_command(habapp_rules.__version__)
diff --git a/habapp_rules/energy/config/monthly_report.py b/habapp_rules/energy/config/monthly_report.py
index bf07c3b..3f4eb30 100644
--- a/habapp_rules/energy/config/monthly_report.py
+++ b/habapp_rules/energy/config/monthly_report.py
@@ -1,4 +1,5 @@
"""Config models for monthly energy report."""
+
import HABApp
import multi_notifier.connectors.connector_mail
import pydantic
@@ -7,54 +8,65 @@
class EnergyShare(pydantic.BaseModel):
- """Dataclass for defining energy share objects."""
- model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
-
- energy_item: HABApp.openhab.items.NumberItem
- chart_name: str
- monthly_power: float = 0.0
-
- def __init__(self, energy_item: str | HABApp.openhab.items.NumberItem, chart_name: str, monthly_power: float = 0.0):
- """Init energy share object without keywords.
-
- :param energy_item: name or item of energy
- :param chart_name: name which will be shown in the chart
- :param monthly_power: monthly power of this energy share. This will be set by the energy share rule.
- """
- super().__init__(energy_item=energy_item, chart_name=chart_name, monthly_power=monthly_power)
-
- @pydantic.field_validator("energy_item", mode="before")
- @classmethod
- def check_oh_item(cls, data: str | HABApp.openhab.items.NumberItem) -> HABApp.openhab.items.NumberItem:
- """Check if given item is an OpenHAB item or try to get it from OpenHAB.
-
- :param data: configuration for energy item
- :return: energy item
- :raises ValueError: if item could not be found
- """
- if isinstance(data, HABApp.openhab.items.NumberItem):
- return data
- try:
- return HABApp.openhab.items.NumberItem.get_item(data)
- except HABApp.core.errors.ItemNotFoundException:
- raise ValueError(f"Could not find any item for given name '{data}'")
+ """Dataclass for defining energy share objects."""
+
+ model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
+
+ energy_item: HABApp.openhab.items.NumberItem
+ chart_name: str
+ monthly_power: float = 0.0
+
+ def __init__(self, energy_item: str | HABApp.openhab.items.NumberItem, chart_name: str, monthly_power: float = 0.0) -> None:
+ """Init energy share object without keywords.
+
+ Args:
+ energy_item: name or item of energy
+ chart_name: name which will be shown in the chart
+ monthly_power: monthly power of this energy share. This will be set by the energy share rule.
+ """
+ super().__init__(energy_item=energy_item, chart_name=chart_name, monthly_power=monthly_power)
+
+ @pydantic.field_validator("energy_item", mode="before")
+ @classmethod
+ def check_oh_item(cls, data: str | HABApp.openhab.items.NumberItem) -> HABApp.openhab.items.NumberItem:
+ """Check if given item is an OpenHAB item or try to get it from OpenHAB.
+
+ Args:
+ data: configuration for energy item
+
+ Returns:
+ energy item
+
+ Raises:
+ ValueError: if item could not be found
+ """
+ if isinstance(data, HABApp.openhab.items.NumberItem):
+ return data
+ try:
+ return HABApp.openhab.items.NumberItem.get_item(data)
+ except HABApp.core.errors.ItemNotFoundException as exc:
+ msg = f"Could not find any item for given name '{data}'"
+ raise ValueError(msg) from exc
class MonthlyReportItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for monthly report."""
- energy_sum: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which holds the total energy consumption")
+ """Items for monthly report."""
+
+ energy_sum: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which holds the total energy consumption")
class MonthlyReportParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for monthly report."""
- known_energy_shares: list[EnergyShare] = pydantic.Field([], description="list of EnergyShare objects which hold the known energy shares. E.g. energy for lights or ventilation")
- persistence_group_name: str | None = pydantic.Field(None, description="OpenHAB group name which holds all items which are persisted. If the group name is given it will be checked if all energy items are in the group")
- config_mail: multi_notifier.connectors.connector_mail.MailConfig = pydantic.Field(..., description="config for sending mails")
- recipients: list[str] = pydantic.Field(..., description="list of recipients who get the mail")
- debug: bool = pydantic.Field(False, description="if debug mode is active")
+ """Parameter for monthly report."""
+
+ known_energy_shares: list[EnergyShare] = pydantic.Field([], description="list of EnergyShare objects which hold the known energy shares. E.g. energy for lights or ventilation")
+ persistence_group_name: str | None = pydantic.Field(None, description="OpenHAB group name which holds all items which are persisted. If the group name is given it will be checked if all energy items are in the group")
+ config_mail: multi_notifier.connectors.connector_mail.MailConfig = pydantic.Field(..., description="config for sending mails")
+ recipients: list[str] = pydantic.Field(..., description="list of recipients who get the mail")
+ debug: bool = pydantic.Field(default=False, description="if debug mode is active")
class MonthlyReportConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for monthly report."""
- items: MonthlyReportItems = pydantic.Field(..., description="Items for monthly report")
- parameter: MonthlyReportParameter = pydantic.Field(..., description="Parameter for monthly report")
+ """Config for monthly report."""
+
+ items: MonthlyReportItems = pydantic.Field(..., description="Items for monthly report")
+ parameter: MonthlyReportParameter = pydantic.Field(..., description="Parameter for monthly report")
diff --git a/habapp_rules/energy/donut_chart.py b/habapp_rules/energy/donut_chart.py
index f59230c..b29d502 100644
--- a/habapp_rules/energy/donut_chart.py
+++ b/habapp_rules/energy/donut_chart.py
@@ -1,39 +1,47 @@
"""Module to create donut charts."""
+
import collections.abc
import pathlib
-import matplotlib.pyplot
+import matplotlib.pyplot as plt
def _auto_percent_format(values: list[float]) -> collections.abc.Callable:
- """Get labels for representing the absolute value.
+ """Get labels for representing the absolute value.
+
+ Args:
+ values: list of all values
+
+ Returns:
+ function which returns the formatted string if called
+ """
- :param values: list of all values
- :return: function which returns the formatted string if called
- """
+ def my_format(pct: float) -> str:
+ """Get formatted value.
- def my_format(pct: float) -> str:
- """get formatted value.
+ Args:
+ pct: percent value
- :param pct: percent value
- :return: formatted value
- """
- total = sum(values)
- return f"{(pct * total / 100.0):.1f} kWh"
+ Returns:
+ formatted value
+ """
+ total = sum(values)
+ return f"{(pct * total / 100.0):.1f} kWh"
- return my_format
+ return my_format
def create_chart(labels: list[str], values: list[float], chart_path: pathlib.Path) -> None:
- """Create the donut chart.
-
- :param labels: labels for the donut chart
- :param values: values of the donut chart
- :param chart_path: target path for the chart
- """
- _, ax = matplotlib.pyplot.subplots()
- _, texts, _ = ax.pie(values, labels=labels, autopct=_auto_percent_format(values), pctdistance=0.7, textprops={"fontsize": 10})
- for text in texts:
- text.set_backgroundcolor("white")
-
- matplotlib.pyplot.savefig(str(chart_path), bbox_inches="tight", transparent=True)
+ """Create the donut chart.
+
+ Args:
+ labels: labels for the donut chart
+ values: values of the donut chart
+ chart_path: target path for the chart
+ """
+ _, ax = plt.subplots()
+ _, texts, _ = ax.pie(values, labels=labels, autopct=_auto_percent_format(values), pctdistance=0.7, textprops={"fontsize": 10})
+ for text in texts:
+ text.set_backgroundcolor("white")
+
+ plt.savefig(str(chart_path), bbox_inches="tight", transparent=True)
diff --git a/habapp_rules/energy/monthly_report.py b/habapp_rules/energy/monthly_report.py
index 2282e91..a42adff 100644
--- a/habapp_rules/energy/monthly_report.py
+++ b/habapp_rules/energy/monthly_report.py
@@ -1,199 +1,190 @@
"""Module for sending the monthly energy consumption."""
+
import datetime
import logging
import pathlib
import tempfile
+import dateutil.relativedelta
import HABApp
import HABApp.core.internals
-import dateutil.relativedelta
import jinja2
import multi_notifier.connectors.connector_mail
-import habapp_rules.__version__
+import habapp_rules
import habapp_rules.core.exceptions
import habapp_rules.core.logger
import habapp_rules.energy.config.monthly_report
import habapp_rules.energy.donut_chart
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
-MONTH_MAPPING = {
- 1: "Januar",
- 2: "Februar",
- 3: "März",
- 4: "April",
- 5: "Mai",
- 6: "Juni",
- 7: "Juli",
- 8: "August",
- 9: "September",
- 10: "Oktober",
- 11: "November",
- 12: "Dezember"
-}
+MONTH_MAPPING = {1: "Januar", 2: "Februar", 3: "März", 4: "April", 5: "Mai", 6: "Juni", 7: "Juli", 8: "August", 9: "September", 10: "Oktober", 11: "November", 12: "Dezember"}
def _get_previous_month_name() -> str:
- """Get name of the previous month
-
- :return: name of current month
-
- if other languages are required, the global dict must be replaced
- """
- today = datetime.date.today()
- last_month = today.replace(day=1) - datetime.timedelta(days=1)
-
- return MONTH_MAPPING[last_month.month]
+ """Get name of the previous month.
+ if other languages are required, the global dict must be replaced
-def _get_next_trigger() -> datetime.datetime:
- """Get next trigger time (always first day of month at midnight)
+ Returns:
+ name of current month
+ """
+ today = datetime.datetime.now(tz=TIMEZONE)
+ last_month = today.replace(day=1) - datetime.timedelta(days=1)
- :return: next trigger time
- """
- return datetime.datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) + dateutil.relativedelta.relativedelta(months=1)
+ return MONTH_MAPPING[last_month.month]
class MonthlyReport(HABApp.Rule):
- """Rule for sending the monthly energy consumption.
-
- # Config
- config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
- items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(
- energy_sum="Total Energy"
- ),
- parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
- known_energy_shares=[
- habapp_rules.energy.config.monthly_report.EnergyShare("Dishwasher_Energy", "Dishwasher"),
- habapp_rules.energy.config.monthly_report.EnergyShare("Light", "Light")
- ],
- config_mail=multi_notifier.connectors.connector_mail.MailConfig(
- user="sender@test.de",
- password="fancy_password",
- smtp_host="smtp.test.de",
- smtp_port=587
- ),
- recipients=["test@test.de"],
- )
- )
-
- # Rule init
- habapp_rules.energy.monthly_report.MonthlyReport("Total_Energy", known_energy_share, "Group_RRD4J", config_mail, "test@test.de")
- """
-
- def __init__(self, config: habapp_rules.energy.config.monthly_report.MonthlyReportConfig) -> None:
- """Initialize the rule.
-
- :param config: config for the monthly energy report rule
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if config is not valid
- """
- self._config = config
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.energy_sum.name)
- self._mail = multi_notifier.connectors.connector_mail.Mail(config.parameter.config_mail)
-
- self._mail = multi_notifier.connectors.connector_mail.Mail(config.parameter.config_mail)
-
- if config.parameter.persistence_group_name is not None:
- # check if all energy items are in the given persistence group
- items_to_check = [config.items.energy_sum] + [share.energy_item for share in config.parameter.known_energy_shares]
- not_in_persistence_group = [item.name for item in items_to_check if config.parameter.persistence_group_name not in item.groups]
- if not_in_persistence_group:
- raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"The following OpenHAB items are not in the persistence group '{config.parameter.persistence_group_name}': {not_in_persistence_group}")
-
- self.run.at(next_trigger_time := _get_next_trigger(), self._cb_send_energy)
- if config.parameter.debug:
- self._instance_logger.warning("Debug mode is active!")
- self.run.soon(self._cb_send_energy)
- self._instance_logger.info(f"Successfully initiated monthly consumption rule for {config.items.energy_sum.name}. Triggered first execution to {next_trigger_time.isoformat()}")
-
- def _get_historic_value(self, item: HABApp.openhab.items.NumberItem, start_time: datetime.datetime) -> float:
- """Get historic value of given Number item
-
- :param item: item instance
- :param start_time: start time to search for the interested value
- :return: historic value of the item
- """
- historic = item.get_persistence_data(start_time=start_time, end_time=start_time + datetime.timedelta(hours=1)).data
- if not historic:
- self._instance_logger.warning(f"Could not get value of item '{item.name}' of time = {start_time}")
- return 0
-
- return next(iter(historic.values()))
-
- # pylint: disable=wrong-spelling-in-docstring
- def _create_html(self, energy_sum_month: float) -> str:
- """Create html which will be sent by the mail
-
- :param energy_sum_month: sum value for the current month
- :return: html with replaced values
-
- The template was created by https://app.bootstrapemail.com/editor/documents with the following input:
-
-
-
-
-
-
-
-
-
-
-
Strom Verbrauch
-
von Februar
-
-
-
Aktueller Zählerstand: 7000 kWh.
-
Hier die Details:
-
-
-
-
-
Generated with habapp_rules version = 20.0.3
-
-
-
-
-
- """
- with (pathlib.Path(__file__).parent / "monthly_report_template.html").open(encoding="utf-8") as template_file:
- html_template = template_file.read()
-
- return jinja2.Template(html_template).render(
- month=_get_previous_month_name(),
- energy_now=f"{self._config.items.energy_sum.value:.1f}",
- energy_last_month=f"{energy_sum_month:.1f}",
- habapp_version=habapp_rules.__version__.__version__,
- chart="{{ chart }}" # this is needed to not replace the chart from the mail-template
- )
-
- def _cb_send_energy(self) -> None:
- """Send the mail with the energy consumption of the last month"""
- self._instance_logger.debug("Send energy consumption was triggered.")
- # get values
- now = datetime.datetime.now()
- last_month = now - dateutil.relativedelta.relativedelta(months=1)
-
- energy_sum_month = self._config.items.energy_sum.value - self._get_historic_value(self._config.items.energy_sum, last_month)
- for share in self._config.parameter.known_energy_shares:
- share.monthly_power = share.energy_item.value - self._get_historic_value(share.energy_item, last_month)
-
- energy_unknown = energy_sum_month - sum(share.monthly_power for share in self._config.parameter.known_energy_shares)
-
- with tempfile.TemporaryDirectory() as temp_dir_name:
- # create plot
- labels = [share.chart_name for share in self._config.parameter.known_energy_shares] + ["Rest"]
- values = [share.monthly_power for share in self._config.parameter.known_energy_shares] + [energy_unknown]
- chart_path = pathlib.Path(temp_dir_name) / "chart.png"
- habapp_rules.energy.donut_chart.create_chart(labels, values, chart_path)
-
- # get html
- html = self._create_html(energy_sum_month)
-
- # send mail
- self._mail.send_message(self._config.parameter.recipients, html, f"Stromverbrauch {_get_previous_month_name()}", images={"chart": str(chart_path)})
-
- self.run.at(next_trigger_time := _get_next_trigger(), self._cb_send_energy)
- self._instance_logger.info(f"Successfully sent energy consumption mail to {self._config.parameter.recipients}. Scheduled the next trigger time to {next_trigger_time.isoformat()}")
+ """Rule for sending the monthly energy consumption.
+
+ # Config
+ config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
+ items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(
+ energy_sum="Total Energy"
+ ),
+ parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
+ known_energy_shares=[
+ habapp_rules.energy.config.monthly_report.EnergyShare("Dishwasher_Energy", "Dishwasher"),
+ habapp_rules.energy.config.monthly_report.EnergyShare("Light", "Light")
+ ],
+ config_mail=multi_notifier.connectors.connector_mail.MailConfig(
+ user="sender@test.de",
+ password="fancy_password",
+ smtp_host="smtp.test.de",
+ smtp_port=587
+ ),
+ recipients=["test@test.de"],
+ )
+ )
+
+ # Rule init
+ habapp_rules.energy.monthly_report.MonthlyReport("Total_Energy", known_energy_share, "Group_RRD4J", config_mail, "test@test.de")
+ """
+
+ def __init__(self, config: habapp_rules.energy.config.monthly_report.MonthlyReportConfig) -> None:
+ """Initialize the rule.
+
+ Args:
+ config: config for the monthly energy report rule
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationError: if config is not valid
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.energy_sum.name)
+ self._mail = multi_notifier.connectors.connector_mail.Mail(config.parameter.config_mail)
+
+ self._mail = multi_notifier.connectors.connector_mail.Mail(config.parameter.config_mail)
+
+ if config.parameter.persistence_group_name is not None:
+ # check if all energy items are in the given persistence group
+ items_to_check = [config.items.energy_sum] + [share.energy_item for share in config.parameter.known_energy_shares]
+ not_in_persistence_group = [item.name for item in items_to_check if config.parameter.persistence_group_name not in item.groups]
+ if not_in_persistence_group:
+ msg = f"The following OpenHAB items are not in the persistence group '{config.parameter.persistence_group_name}': {not_in_persistence_group}"
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationError(msg)
+
+ self.run.at(self.run.trigger.time("00:00:00").only_on(self.run.filter.days(1)), self._cb_send_energy)
+
+ if config.parameter.debug:
+ self._instance_logger.warning("Debug mode is active!")
+ self.run.soon(self._cb_send_energy)
+ self._instance_logger.info(f"Successfully initiated monthly consumption rule for {config.items.energy_sum.name}.")
+
+ def _get_historic_value(self, item: HABApp.openhab.items.NumberItem, start_time: datetime.datetime) -> float:
+ """Get historic value of given Number item.
+
+ Args:
+ item: item instance
+ start_time: start time to search for the interested value
+
+ Returns:
+ historic value of the item
+ """
+ historic = item.get_persistence_data(start_time=start_time, end_time=start_time + datetime.timedelta(hours=1)).data
+ if not historic:
+ self._instance_logger.warning(f"Could not get value of item '{item.name}' of time = {start_time}")
+ return 0
+
+ return next(iter(historic.values()))
+
+ def _create_html(self, energy_sum_month: float) -> str:
+ """Create html which will be sent by the mail.
+
+ The template was created by https://app.bootstrapemail.com/editor/documents with the following input:
+
+
+
+
+
+
+
+
+
+
+
Strom Verbrauch
+
von Februar
+
+
+
Aktueller Zählerstand: 7000 kWh.
+
Hier die Details:
+
+
+
+
+
Generated with habapp_rules version = 20.0.3
+
+
+
+
+
+
+ Args:
+ energy_sum_month: sum value for the current month
+
+ Returns:
+ html with replaced values
+ """
+ with (pathlib.Path(__file__).parent / "monthly_report_template.html").open(encoding="utf-8") as template_file:
+ html_template = template_file.read()
+
+ return jinja2.Template(html_template).render(
+ month=_get_previous_month_name(),
+ energy_now=f"{self._config.items.energy_sum.value:.1f}",
+ energy_last_month=f"{energy_sum_month:.1f}",
+ habapp_version=habapp_rules.__version__,
+ chart="{{ chart }}", # this is needed to not replace the chart from the mail-template
+ )
+
+ def _cb_send_energy(self) -> None:
+ """Send the mail with the energy consumption of the last month."""
+ self._instance_logger.debug("Send energy consumption was triggered.")
+ # get values
+ now = datetime.datetime.now(tz=TIMEZONE)
+ last_month = now - dateutil.relativedelta.relativedelta(months=1)
+
+ energy_sum_month = self._config.items.energy_sum.value - self._get_historic_value(self._config.items.energy_sum, last_month)
+ for share in self._config.parameter.known_energy_shares:
+ share.monthly_power = share.energy_item.value - self._get_historic_value(share.energy_item, last_month)
+
+ energy_unknown = energy_sum_month - sum(share.monthly_power for share in self._config.parameter.known_energy_shares)
+
+ with tempfile.TemporaryDirectory() as temp_dir_name:
+ # create plot
+ labels = [share.chart_name for share in self._config.parameter.known_energy_shares] + ["Rest"]
+ values = [share.monthly_power for share in self._config.parameter.known_energy_shares] + [energy_unknown]
+ chart_path = pathlib.Path(temp_dir_name) / "chart.png"
+ habapp_rules.energy.donut_chart.create_chart(labels, values, chart_path)
+
+ # get html
+ html = self._create_html(energy_sum_month)
+
+ # send mail
+ self._mail.send_message(self._config.parameter.recipients, html, f"Stromverbrauch {_get_previous_month_name()}", images={"chart": str(chart_path)})
+
+ self._instance_logger.info(f"Successfully sent energy consumption mail to {self._config.parameter.recipients}.")
diff --git a/habapp_rules/sensors/astro.py b/habapp_rules/sensors/astro.py
index 83106eb..713c689 100644
--- a/habapp_rules/sensors/astro.py
+++ b/habapp_rules/sensors/astro.py
@@ -1,4 +1,5 @@
"""Rules for astro actions."""
+
import abc
import logging
@@ -11,108 +12,114 @@
class _SetNightDayBase(HABApp.Rule):
- """Base class for set night / day."""
+ """Base class for set night / day."""
- def __init__(self, item_target: HABApp.openhab.items.SwitchItem, item_elevation: HABApp.openhab.items.NumberItem, elevation_threshold: float) -> None:
- """Init Rule.
+ def __init__(self, item_target: HABApp.openhab.items.SwitchItem, item_elevation: HABApp.openhab.items.NumberItem, elevation_threshold: float) -> None:
+ """Init Rule.
- :param item_target: OpenHab item which should be set depending on the sun elevation value
- :param item_elevation: OpenHAB item of sun elevation (NumberItem)
- :param elevation_threshold: Threshold value for elevation.
- """
- HABApp.Rule.__init__(self)
+ Args:
+ item_target: OpenHab item which should be set depending on the sun elevation value
+ item_elevation: OpenHAB item of sun elevation (NumberItem)
+ elevation_threshold: Threshold value for elevation.
+ """
+ HABApp.Rule.__init__(self)
- self._item_target = item_target
- self._item_elevation = item_elevation
- self._elevation_threshold = elevation_threshold
+ self._item_target = item_target
+ self._item_elevation = item_elevation
+ self._elevation_threshold = elevation_threshold
- self._item_elevation.listen_event(self._set_night, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._item_elevation.listen_event(self._set_night, HABApp.openhab.events.ItemStateChangedEventFilter())
- self.run.soon(self._set_night)
+ self.run.soon(self._set_night)
- def _set_night(self, _: HABApp.openhab.events.ItemStateChangedEvent | None = None) -> None:
- """Callback which sets the state to the night item."""
- if self._item_elevation.value is None:
- return
- habapp_rules.core.helper.send_if_different(self._item_target, self._get_target_value())
+ def _set_night(self, _: HABApp.openhab.events.ItemStateChangedEvent | None = None) -> None:
+ """Callback which sets the state to the night item."""
+ if self._item_elevation.value is None:
+ return
+ habapp_rules.core.helper.send_if_different(self._item_target, self._get_target_value())
- @abc.abstractmethod
- def _get_target_value(self) -> str:
- """Get target value which should be set.
+ @abc.abstractmethod
+ def _get_target_value(self) -> str:
+ """Get target value which should be set.
- :return: target value (ON / OFF)
- """
+ Returns:
+ target value (ON / OFF)
+ """
class SetDay(_SetNightDayBase):
- """Rule to set / unset day item at dusk / dawn.
+ """Rule to set / unset day item at dusk / dawn.
- # Items:
- Switch day "Day"
- Number elevation "Sun elevation [%s]" {channel="astro...}
+ # Items:
+ Switch day "Day"
+ Number elevation "Sun elevation [%s]" {channel="astro...}
- # Config:
- config = habapp_rules.sensors.config.astro.SetDayConfig(
- items=habapp_rules.sensors.config.astro.SetDayItems(
- day="day",
- elevation="elevation"
- ),
- parameter=habapp_rules.sensors.config.astro.SetDayParameter(
- elevation_threshold=5
- )
- )
+ # Config:
+ config = habapp_rules.sensors.config.astro.SetDayConfig(
+ items=habapp_rules.sensors.config.astro.SetDayItems(
+ day="day",
+ elevation="elevation"
+ ),
+ parameter=habapp_rules.sensors.config.astro.SetDayParameter(
+ elevation_threshold=5
+ )
+ )
- # Rule init:
- habapp_rules.sensors.astro.SetNight(config)
- """
+ # Rule init:
+ habapp_rules.sensors.astro.SetNight(config)
+ """
- def __init__(self, config: habapp_rules.sensors.config.astro.SetDayConfig) -> None:
- """Init Rule.
+ def __init__(self, config: habapp_rules.sensors.config.astro.SetDayConfig) -> None:
+ """Init Rule.
- :param config: Config for set day rule
- """
- _SetNightDayBase.__init__(self, config.items.day, config.items.elevation, config.parameter.elevation_threshold)
+ Args:
+ config: Config for set day rule
+ """
+ _SetNightDayBase.__init__(self, config.items.day, config.items.elevation, config.parameter.elevation_threshold)
- def _get_target_value(self) -> str:
- """Get target value which should be set.
+ def _get_target_value(self) -> str:
+ """Get target value which should be set.
- :return: target value (ON / OFF)
- """
- return "ON" if self._item_elevation.value > self._elevation_threshold else "OFF"
+ Returns:
+ target value (ON / OFF)
+ """
+ return "ON" if self._item_elevation.value > self._elevation_threshold else "OFF"
class SetNight(_SetNightDayBase):
- """Rule to set / unset night item at dusk / dawn.
-
- # Items:
- Switch night_for_shading "Night for shading"
- Number elevation "Sun elevation [%s]" {channel="astro...}
-
- # Config:
- config = habapp_rules.sensors.config.astro.SetNightConfig(
- items=habapp_rules.sensors.config.astro.SetNightItems(
- night="night_for_shading",
- elevation="elevation"
- ),
- parameter=habapp_rules.sensors.config.astro.SetNightParameter(
- elevation_threshold=5
- )
- )
-
- # Rule init:
- habapp_rules.sensors.astro.SetNight(config)
- """
-
- def __init__(self, config: habapp_rules.sensors.config.astro.SetNightConfig) -> None:
- """Init Rule.
-
- :param config: Config for setting night depending on sun elevation
- """
- _SetNightDayBase.__init__(self, config.items.night, config.items.elevation, config.parameter.elevation_threshold)
-
- def _get_target_value(self) -> str:
- """Get target value which should be set.
-
- :return: target value (ON / OFF)
- """
- return "ON" if self._item_elevation.value < self._elevation_threshold else "OFF"
+ """Rule to set / unset night item at dusk / dawn.
+
+ # Items:
+ Switch night_for_shading "Night for shading"
+ Number elevation "Sun elevation [%s]" {channel="astro...}
+
+ # Config:
+ config = habapp_rules.sensors.config.astro.SetNightConfig(
+ items=habapp_rules.sensors.config.astro.SetNightItems(
+ night="night_for_shading",
+ elevation="elevation"
+ ),
+ parameter=habapp_rules.sensors.config.astro.SetNightParameter(
+ elevation_threshold=5
+ )
+ )
+
+ # Rule init:
+ habapp_rules.sensors.astro.SetNight(config)
+ """
+
+ def __init__(self, config: habapp_rules.sensors.config.astro.SetNightConfig) -> None:
+ """Init Rule.
+
+ Args:
+ config: Config for setting night depending on sun elevation
+ """
+ _SetNightDayBase.__init__(self, config.items.night, config.items.elevation, config.parameter.elevation_threshold)
+
+ def _get_target_value(self) -> str:
+ """Get target value which should be set.
+
+ Returns:
+ target value (ON / OFF)
+ """
+ return "ON" if self._item_elevation.value < self._elevation_threshold else "OFF"
diff --git a/habapp_rules/sensors/config/astro.py b/habapp_rules/sensors/config/astro.py
index 19c6546..e8ae9bf 100644
--- a/habapp_rules/sensors/config/astro.py
+++ b/habapp_rules/sensors/config/astro.py
@@ -1,4 +1,5 @@
"""Config models for astro rules."""
+
import HABApp.openhab.items
import pydantic
@@ -6,37 +7,44 @@
class _NightDayItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for night day."""
- elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Elevation of the sun")
+ """Items for night day."""
+
+ elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="Elevation of the sun")
class SetDayItems(_NightDayItems):
- """Items for setting day."""
- day: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item which should be set to ON after dawn and OFF after dusk")
+ """Items for setting day."""
+
+ day: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item which should be set to ON after dawn and OFF after dusk")
class SetNightItems(_NightDayItems):
- """Items for setting night."""
- night: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item which should be set to ON after dusk and OFF after dawn")
+ """Items for setting night."""
+
+ night: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="Item which should be set to ON after dusk and OFF after dawn")
class SetDayParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for setting day."""
- elevation_threshold: float = pydantic.Field(0.0, description="Threshold value for elevation. If the sun elevation is greater than the threshold, the day item will be set to ON")
+ """Parameter for setting day."""
+
+ elevation_threshold: float = pydantic.Field(0.0, description="Threshold value for elevation. If the sun elevation is greater than the threshold, the day item will be set to ON")
class SetNightParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for setting night."""
- elevation_threshold: float = pydantic.Field(-8.0, description="Threshold value for elevation. If the sun elevation is greater than the threshold, the night item will be set to ON")
+ """Parameter for setting night."""
+
+ elevation_threshold: float = pydantic.Field(-8.0, description="Threshold value for elevation. If the sun elevation is greater than the threshold, the night item will be set to ON")
class SetDayConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for setting day."""
- items: SetDayItems = pydantic.Field(..., description="Items for setting day")
- parameter: SetDayParameter = pydantic.Field(SetDayParameter(), description="Parameter for setting day")
+ """Config for setting day."""
+
+ items: SetDayItems = pydantic.Field(..., description="Items for setting day")
+ parameter: SetDayParameter = pydantic.Field(SetDayParameter(), description="Parameter for setting day")
class SetNightConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for setting night."""
- items: SetNightItems = pydantic.Field(..., description="Items for setting night")
- parameter: SetNightParameter = pydantic.Field(SetNightParameter(), description="Parameter for setting night")
+ """Config for setting night."""
+
+ items: SetNightItems = pydantic.Field(..., description="Items for setting night")
+ parameter: SetNightParameter = pydantic.Field(SetNightParameter(), description="Parameter for setting night")
diff --git a/habapp_rules/sensors/config/current_switch.py b/habapp_rules/sensors/config/current_switch.py
index 20fe077..11ddb10 100644
--- a/habapp_rules/sensors/config/current_switch.py
+++ b/habapp_rules/sensors/config/current_switch.py
@@ -5,22 +5,23 @@
import habapp_rules.core.pydantic_base
+
class CurrentSwitchItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for current switch the rule."""
+ """Items for current switch the rule."""
- current: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which measures the current")
- switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which should be switched on, if the current is above")
+ current: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which measures the current")
+ switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which should be switched on, if the current is above")
class CurrentSwitchParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for current switch the rules."""
+ """Parameter for current switch the rules."""
- threshold: float = pydantic.Field(0.2, description="threshold for switching on")
- extended_time: float = pydantic.Field(0, description="extended time in seconds, if current is below threshold")
+ threshold: float = pydantic.Field(0.2, description="threshold for switching on")
+ extended_time: float = pydantic.Field(0, description="extended time in seconds, if current is below threshold")
class CurrentSwitchConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config models for current switch the rule."""
+ """Config models for current switch the rule."""
- items: CurrentSwitchItems = pydantic.Field(..., description="items for current switch rules")
- parameter: CurrentSwitchParameter = pydantic.Field(CurrentSwitchParameter(), description="parameter for current switch rules")
+ items: CurrentSwitchItems = pydantic.Field(..., description="items for current switch rules")
+ parameter: CurrentSwitchParameter = pydantic.Field(CurrentSwitchParameter(), description="parameter for current switch rules")
diff --git a/habapp_rules/sensors/config/dwd.py b/habapp_rules/sensors/config/dwd.py
index 67e3795..64364f0 100644
--- a/habapp_rules/sensors/config/dwd.py
+++ b/habapp_rules/sensors/config/dwd.py
@@ -1,42 +1,48 @@
"""Config rules for DWD rules."""
-import typing
import HABApp
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
class WindAlarmItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for DWD wind alarm rule."""
+ """Items for DWD wind alarm rule."""
- wind_alarm: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item for wind alarm, which will be set to ON if wind alarm is active")
- manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item to disable all automatic functions")
- hand_timeout: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="item to set the hand timeout")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item for storing the current state")
+ wind_alarm: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item for wind alarm, which will be set to ON if wind alarm is active")
+ manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item to disable all automatic functions")
+ hand_timeout: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="item to set the hand timeout")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item for storing the current state")
class WindAlarmParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for DWD wind alarm rule."""
- hand_timeout: int | None = pydantic.Field(None, description="hand timeout in seconds or 0 for no timeout")
- dwd_item_prefix: str = pydantic.Field("I26_99_warning_", description="prefix of dwd warning names")
- number_dwd_objects: int = pydantic.Field(3, description="number of dwd objects")
- threshold_wind_speed: int = pydantic.Field(70, description="threshold for wind speed -> wind alarm will only be active if greater or equal")
- threshold_severity: int = pydantic.Field(2, description="threshold for severity -> wind alarm will only be active if greater or equal")
+ """Parameter for DWD wind alarm rule."""
+
+ hand_timeout: int | None = pydantic.Field(None, description="hand timeout in seconds or 0 for no timeout")
+ dwd_item_prefix: str = pydantic.Field("I26_99_warning_", description="prefix of dwd warning names")
+ number_dwd_objects: int = pydantic.Field(3, description="number of dwd objects")
+ threshold_wind_speed: int = pydantic.Field(70, description="threshold for wind speed -> wind alarm will only be active if greater or equal")
+ threshold_severity: int = pydantic.Field(2, description="threshold for severity -> wind alarm will only be active if greater or equal")
class WindAlarmConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for DWD wind alarm rule."""
- items: WindAlarmItems = pydantic.Field(..., description="items for DWD wind alarm rule")
- parameter: WindAlarmParameter = pydantic.Field(WindAlarmParameter(), description="parameters for DWD wind alarm rule")
-
- @pydantic.model_validator(mode="after")
- def check_hand_timeout(self) -> typing.Self:
- """Validate hand timeout.
-
- :return: validated config model
- :raises ValueError: if both 'items.hand_timeout' and 'parameter.hand_timeout' are set
- """
- if not (self.items.hand_timeout is None) ^ (self.parameter.hand_timeout is None): # XNOR
- raise ValueError("Either 'items.wind_alarm' or 'parameter.hand_timeout' must be set")
- return self
+ """Config for DWD wind alarm rule."""
+
+ items: WindAlarmItems = pydantic.Field(..., description="items for DWD wind alarm rule")
+ parameter: WindAlarmParameter = pydantic.Field(WindAlarmParameter(), description="parameters for DWD wind alarm rule")
+
+ @pydantic.model_validator(mode="after")
+ def check_hand_timeout(self) -> typing_extensions.Self:
+ """Validate hand timeout.
+
+ Returns:
+ validated config model
+
+ Raises:
+ ValueError: if both 'items.hand_timeout' and 'parameter.hand_timeout' are set
+ """
+ if not (self.items.hand_timeout is None) ^ (self.parameter.hand_timeout is None): # XNOR
+ msg = "Either 'items.wind_alarm' or 'parameter.hand_timeout' must be set"
+ raise ValueError(msg)
+ return self
diff --git a/habapp_rules/sensors/config/humidity.py b/habapp_rules/sensors/config/humidity.py
index 4a4d016..05194f3 100644
--- a/habapp_rules/sensors/config/humidity.py
+++ b/habapp_rules/sensors/config/humidity.py
@@ -1,4 +1,5 @@
"""Config models for humidity rules."""
+
import HABApp
import pydantic
@@ -6,19 +7,22 @@
class HumiditySwitchItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for humidity switch."""
- humidity: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which holds the measured humidity")
- output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which will be switched on if high humidity is detected")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the state")
+ """Items for humidity switch."""
+
+ humidity: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="item which holds the measured humidity")
+ output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item which will be switched on if high humidity is detected")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the state")
class HumiditySwitchParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for humidity switch."""
- absolute_threshold: float = pydantic.Field(65, description="threshold for high humidity")
- extended_time: int = pydantic.Field(10 * 60, description="extended time in seconds, if humidity is below threshold")
+ """Parameter for humidity switch."""
+
+ absolute_threshold: float = pydantic.Field(65, description="threshold for high humidity")
+ extended_time: int = pydantic.Field(10 * 60, description="extended time in seconds, if humidity is below threshold")
class HumiditySwitchConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for humidity switch."""
- items: HumiditySwitchItems = pydantic.Field(..., description="items for humidity switch")
- parameter: HumiditySwitchParameter = pydantic.Field(HumiditySwitchParameter(), description="parameter for humidity switch")
+ """Config for humidity switch."""
+
+ items: HumiditySwitchItems = pydantic.Field(..., description="items for humidity switch")
+ parameter: HumiditySwitchParameter = pydantic.Field(HumiditySwitchParameter(), description="parameter for humidity switch")
diff --git a/habapp_rules/sensors/config/motion.py b/habapp_rules/sensors/config/motion.py
index aa5c2d1..538ac5b 100644
--- a/habapp_rules/sensors/config/motion.py
+++ b/habapp_rules/sensors/config/motion.py
@@ -1,4 +1,5 @@
"""Config models for motion rules."""
+
import HABApp
import pydantic
@@ -6,24 +7,27 @@
class MotionItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for motion."""
- motion_raw: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="unfiltered motion item")
- motion_filtered: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="filtered motion item")
- brightness: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="brightness item")
- brightness_threshold: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="brightness threshold item")
- lock: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock item")
- sleep_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleep state item")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
+ """Items for motion."""
+
+ motion_raw: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="unfiltered motion item")
+ motion_filtered: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="filtered motion item")
+ brightness: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="brightness item")
+ brightness_threshold: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="brightness threshold item")
+ lock: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock item")
+ sleep_state: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="sleep state item")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
class MotionParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for motion."""
- extended_motion_time: int = pydantic.Field(5, description="extended motion time in seconds")
- brightness_threshold: float | None = pydantic.Field(None, description="brightness threshold value")
- post_sleep_lock_time: int = pydantic.Field(10, description="post sleep lock time in seconds")
+ """Parameter for motion."""
+
+ extended_motion_time: int = pydantic.Field(5, description="extended motion time in seconds")
+ brightness_threshold: float | None = pydantic.Field(None, description="brightness threshold value")
+ post_sleep_lock_time: int = pydantic.Field(10, description="post sleep lock time in seconds")
class MotionConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for motion."""
- items: MotionItems = pydantic.Field(..., description="items for motion")
- parameter: MotionParameter = pydantic.Field(MotionParameter(), description="parameter for motion")
+ """Config for motion."""
+
+ items: MotionItems = pydantic.Field(..., description="items for motion")
+ parameter: MotionParameter = pydantic.Field(MotionParameter(), description="parameter for motion")
diff --git a/habapp_rules/sensors/config/sun.py b/habapp_rules/sensors/config/sun.py
index 648f2c0..df3095a 100644
--- a/habapp_rules/sensors/config/sun.py
+++ b/habapp_rules/sensors/config/sun.py
@@ -1,9 +1,10 @@
"""Config models for sun rules."""
+
import logging
-import typing
import HABApp
import pydantic
+import typing_extensions
import habapp_rules.core.pydantic_base
@@ -11,146 +12,169 @@
class _ItemsBase(habapp_rules.core.pydantic_base.ItemBase):
- """Base class for items for sun sensor."""
- output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="output item")
- threshold: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="threshold item")
+ """Base class for items for sun sensor."""
+
+ output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="output item")
+ threshold: HABApp.openhab.items.NumberItem | None = pydantic.Field(None, description="threshold item")
class BrightnessItems(_ItemsBase):
- """Items for sun sensor which uses brightness items as input."""
- brightness: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="brightness item")
+ """Items for sun sensor which uses brightness items as input."""
+
+ brightness: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="brightness item")
class TemperatureDifferenceItems(_ItemsBase):
- """Items for sun sensor which uses temperature items as input."""
- temperatures: list[HABApp.openhab.items.NumberItem] = pydantic.Field(..., description="temperature items")
+ """Items for sun sensor which uses temperature items as input."""
+
+ temperatures: list[HABApp.openhab.items.NumberItem] = pydantic.Field(..., description="temperature items")
- @pydantic.model_validator(mode="after")
- def validate_temperature_items(self) -> typing.Self:
- """Validate that at least two temperature items are given.
+ @pydantic.model_validator(mode="after")
+ def validate_temperature_items(self) -> typing_extensions.Self:
+ """Validate that at least two temperature items are given.
- :return: validated model
- :raises ValueError: if less than two temperature items are given
- """
- if len(self.temperatures) < 2:
- raise ValueError("At least two temperature items are required!")
- return self
+ Returns:
+ validated model
+
+ Raises:
+ ValueError: if less than two temperature items are given
+ """
+ if len(self.temperatures) < 2: # noqa: PLR2004
+ msg = "At least two temperature items are required!"
+ raise ValueError(msg)
+ return self
class BrightnessParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for sun sensor which uses brightness items as input."""
- threshold: float | None = pydantic.Field(None, description="threshold value")
- hysteresis: float = pydantic.Field(0.0, description="hysteresis value")
- filter_tau: int = pydantic.Field(30 * 60, description="filter constant for the exponential filter. Default is set to 30 minutes")
- filter_instant_increase: bool = pydantic.Field(True, description="if set to True, increase of input values will not be filtered")
- filter_instant_decrease: bool = pydantic.Field(False, description="if set to True, decrease of input values will not be filtered")
- filtered_signal_groups: list[str] = pydantic.Field([], description="group names where the filtered signal will be added")
+ """Parameter for sun sensor which uses brightness items as input."""
+
+ threshold: float | None = pydantic.Field(None, description="threshold value")
+ hysteresis: float = pydantic.Field(0.0, description="hysteresis value")
+ filter_tau: int = pydantic.Field(30 * 60, description="filter constant for the exponential filter. Default is set to 30 minutes")
+ filter_instant_increase: bool = pydantic.Field(default=True, description="if set to True, increase of input values will not be filtered")
+ filter_instant_decrease: bool = pydantic.Field(default=False, description="if set to True, decrease of input values will not be filtered")
+ filtered_signal_groups: list[str] = pydantic.Field([], description="group names where the filtered signal will be added")
class TemperatureDifferenceParameter(BrightnessParameter):
- """Parameter for sun sensor which uses temperature items as input."""
- ignore_old_values_time: int | None = pydantic.Field(None, description="ignores values which are older than the given time in seconds. If None, all values will be taken")
+ """Parameter for sun sensor which uses temperature items as input."""
+
+ ignore_old_values_time: int | None = pydantic.Field(None, description="ignores values which are older than the given time in seconds. If None, all values will be taken")
class _ConfigBase(habapp_rules.core.pydantic_base.ConfigBase):
- """Base config model for sun sensor."""
- items: BrightnessItems | TemperatureDifferenceItems = pydantic.Field(..., description="items for sun sensor")
- parameter: BrightnessParameter | TemperatureDifferenceParameter = pydantic.Field(..., description="parameter for sun sensor")
+ """Base config model for sun sensor."""
+
+ items: BrightnessItems | TemperatureDifferenceItems = pydantic.Field(..., description="items for sun sensor")
+ parameter: BrightnessParameter | TemperatureDifferenceParameter = pydantic.Field(..., description="parameter for sun sensor")
- @pydantic.model_validator(mode="after")
- def validate_threshold(self) -> typing.Self:
- """Validate threshold.
+ @pydantic.model_validator(mode="after")
+ def validate_threshold(self) -> typing_extensions.Self:
+ """Validate threshold.
- :return: validated model
- :raises ValueError: if threshold and parameter are not set
- """
- if (self.items.threshold is None) == (self.parameter.threshold is None):
- raise ValueError("The threshold must be set ether with the parameter or with the item, both are not allowed")
- return self
+ Returns:
+ validated model
- @property
- def threshold(self) -> float:
- """Get threshold."""
- if self.parameter.threshold:
- return self.parameter.threshold
+ Raises:
+ ValueError: if threshold and parameter are not set
+ """
+ if (self.items.threshold is None) == (self.parameter.threshold is None):
+ msg = "The threshold must be set ether with the parameter or with the item, both are not allowed"
+ raise ValueError(msg)
+ return self
- if self.items.threshold.value is None:
- LOGGER.warning("Threshold item has no value set. Setting threshold to infinity")
- return float("inf")
+ @property
+ def threshold(self) -> float:
+ """Get threshold."""
+ if self.parameter.threshold:
+ return self.parameter.threshold
- return self.items.threshold.value
+ if self.items.threshold.value is None:
+ LOGGER.warning("Threshold item has no value set. Setting threshold to infinity")
+ return float("inf")
+
+ return self.items.threshold.value
class BrightnessConfig(_ConfigBase):
- """Config model for sun sensor which uses brightness as input."""
- items: BrightnessItems = pydantic.Field(..., description="items for sun sensor which uses brightness as input")
- parameter: BrightnessParameter = pydantic.Field(BrightnessParameter(), description="parameter for sun sensor which uses brightness as input")
+ """Config model for sun sensor which uses brightness as input."""
+
+ items: BrightnessItems = pydantic.Field(..., description="items for sun sensor which uses brightness as input")
+ parameter: BrightnessParameter = pydantic.Field(BrightnessParameter(), description="parameter for sun sensor which uses brightness as input")
class TemperatureDifferenceConfig(_ConfigBase):
- """Config model for sun sensor which uses temperature items as input."""
- items: TemperatureDifferenceItems = pydantic.Field(..., description="items for sun sensor which uses temperature items as input")
- parameter: TemperatureDifferenceParameter = pydantic.Field(TemperatureDifferenceParameter(), description="parameter for sun sensor which uses temperature items as input")
+ """Config model for sun sensor which uses temperature items as input."""
+
+ items: TemperatureDifferenceItems = pydantic.Field(..., description="items for sun sensor which uses temperature items as input")
+ parameter: TemperatureDifferenceParameter = pydantic.Field(TemperatureDifferenceParameter(), description="parameter for sun sensor which uses temperature items as input")
+
-############################ SunPositionFilter ###############################
+# SunPositionFilter ###############################
class SunPositionWindow(pydantic.BaseModel):
- """Class for defining min / max values for azimuth and elevation."""
- azimuth_min: float = pydantic.Field(..., description="Starting value for azimuth", ge=0.0, le=360.0)
- azimuth_max: float = pydantic.Field(..., description="End value for azimuth", ge=0.0, le=360.0)
- elevation_min: float = pydantic.Field(0.0, description="Starting value for elevation", ge=-90.0, le=90.0)
- elevation_max: float = pydantic.Field(90.0, description="End value for elevation", ge=-90.0, le=90.0)
-
- def __init__(self, azimuth_min: float, azimuth_max: float, elevation_min: float = 0.0, elevation_max: float = 90.0) -> None:
- """Init of class for defining min / max values for azimuth and elevation.
-
- :param azimuth_min: minimum azimuth value
- :param azimuth_max: maximum azimuth value
- :param elevation_min: minimum elevation value
- :param elevation_max: maximum elevation value
- """
- super().__init__(azimuth_min=azimuth_min, azimuth_max=azimuth_max, elevation_min=elevation_min, elevation_max=elevation_max)
-
- @pydantic.model_validator(mode="after")
- def validate_model(self) -> typing.Self:
- """Validate values.
-
- :return: validated model
- """
- if self.azimuth_min > self.azimuth_max:
- LOGGER.warning(f"azimuth_min should be smaller than azimuth_max -> min / max will be swapped. Given values: azimuth_min = {self.azimuth_min} | azimuth_max = {self.azimuth_max}")
- min_orig = self.azimuth_min
- max_orig = self.azimuth_max
- self.azimuth_min = max_orig
- self.azimuth_max = min_orig
-
- if self.elevation_min > self.elevation_max:
- LOGGER.warning(f"elevation_min should be smaller than elevation_max -> min / max will be swapped. Given values: elevation_min = {self.elevation_min} | elevation_max = {self.elevation_max}")
- min_orig = self.elevation_min
- max_orig = self.elevation_max
- self.elevation_min = max_orig
- self.elevation_max = min_orig
- return self
+ """Class for defining min / max values for azimuth and elevation."""
+
+ azimuth_min: float = pydantic.Field(..., description="Starting value for azimuth", ge=0.0, le=360.0)
+ azimuth_max: float = pydantic.Field(..., description="End value for azimuth", ge=0.0, le=360.0)
+ elevation_min: float = pydantic.Field(0.0, description="Starting value for elevation", ge=-90.0, le=90.0)
+ elevation_max: float = pydantic.Field(90.0, description="End value for elevation", ge=-90.0, le=90.0)
+
+ def __init__(self, azimuth_min: float, azimuth_max: float, elevation_min: float = 0.0, elevation_max: float = 90.0) -> None:
+ """Init of class for defining min / max values for azimuth and elevation.
+
+ Args:
+ azimuth_min: minimum azimuth value
+ azimuth_max: maximum azimuth value
+ elevation_min: minimum elevation value
+ elevation_max: maximum elevation value
+ """
+ super().__init__(azimuth_min=azimuth_min, azimuth_max=azimuth_max, elevation_min=elevation_min, elevation_max=elevation_max)
+
+ @pydantic.model_validator(mode="after")
+ def validate_model(self) -> typing_extensions.Self:
+ """Validate values.
+
+ Returns:
+ validated model
+ """
+ if self.azimuth_min > self.azimuth_max:
+ LOGGER.warning(f"azimuth_min should be smaller than azimuth_max -> min / max will be swapped. Given values: azimuth_min = {self.azimuth_min} | azimuth_max = {self.azimuth_max}")
+ min_orig = self.azimuth_min
+ max_orig = self.azimuth_max
+ self.azimuth_min = max_orig
+ self.azimuth_max = min_orig
+
+ if self.elevation_min > self.elevation_max:
+ LOGGER.warning(f"elevation_min should be smaller than elevation_max -> min / max will be swapped. Given values: elevation_min = {self.elevation_min} | elevation_max = {self.elevation_max}")
+ min_orig = self.elevation_min
+ max_orig = self.elevation_max
+ self.elevation_min = max_orig
+ self.elevation_max = min_orig
+ return self
class SunPositionItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for sun position filter."""
- azimuth: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun azimuth item")
- elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun elevation item")
- input: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="input item (sun protection required)")
- output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="output item (sun protection required and sun in the configured azimuth / elevation window)")
+ """Items for sun position filter."""
+
+ azimuth: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun azimuth item")
+ elevation: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="sun elevation item")
+ input: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="input item (sun protection required)")
+ output: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="output item (sun protection required and sun in the configured azimuth / elevation window)")
class SunPositionParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for sun position filter."""
- sun_position_window: SunPositionWindow | list[SunPositionWindow] = pydantic.Field(..., description="sun position window, where the sun hits the target")
+ """Parameter for sun position filter."""
+
+ sun_position_window: SunPositionWindow | list[SunPositionWindow] = pydantic.Field(..., description="sun position window, where the sun hits the target")
- @property
- def sun_position_windows(self) -> list[SunPositionWindow]:
- """Get sun position windows."""
- return self.sun_position_window if isinstance(self.sun_position_window, list) else [self.sun_position_window]
+ @property
+ def sun_position_windows(self) -> list[SunPositionWindow]:
+ """Get sun position windows."""
+ return self.sun_position_window if isinstance(self.sun_position_window, list) else [self.sun_position_window]
class SunPositionConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config model for sun position filter."""
- items: SunPositionItems = pydantic.Field(..., description="items for sun position filter")
- parameter: SunPositionParameter = pydantic.Field(..., description="parameter for sun position filter")
+ """Config model for sun position filter."""
+
+ items: SunPositionItems = pydantic.Field(..., description="items for sun position filter")
+ parameter: SunPositionParameter = pydantic.Field(..., description="parameter for sun position filter")
diff --git a/habapp_rules/sensors/current_switch.py b/habapp_rules/sensors/current_switch.py
index 9cb93f8..286bf64 100644
--- a/habapp_rules/sensors/current_switch.py
+++ b/habapp_rules/sensors/current_switch.py
@@ -1,69 +1,78 @@
"""current switch rules."""
+
+from typing import TYPE_CHECKING
+
import HABApp
-from eascheduler.jobs.job_countdown import CountdownJob
import habapp_rules.core.helper
import habapp_rules.sensors.config.current_switch
+if TYPE_CHECKING:
+ from eascheduler.jobs.job_countdown import CountdownJob # pragma: no cover
+
class CurrentSwitch(HABApp.Rule):
- """Rules class to manage basic light states.
-
- # Items:
- Number Current "Current"
- Switch Something_is_ON "Something is ON"
-
- # Config:
- config = habapp_rules.sensors.config.current_switch.CurrentSwitchConfig(
- items = habapp_rules.actors.config.light.CurrentSwitchItems(
- current="Current",
- switch="Something_is_ON"
- )
- )
-
- # Rule init:
- habapp_rules.actors.power.CurrentSwitch(config)
- """
-
- def __init__(self, config: habapp_rules.sensors.config.current_switch.CurrentSwitchConfig) -> None:
- """Init current switch rule.
-
- :param config: config for current switch rule
- """
- HABApp.Rule.__init__(self)
- self._config = config
- self._extended_countdown: CountdownJob | None = self.run.countdown(self._config.parameter.extended_time, habapp_rules.core.helper.send_if_different, item=self._config.items.switch, value="OFF") \
- if self._config.parameter.extended_time else None
-
- self._check_current_and_set_switch(self._config.items.current.value)
- self._config.items.current.listen_event(self._cb_current_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- def _check_current_and_set_switch(self, current: float | None) -> None:
- """Check if current is above the threshold and set switch.
-
- :param current: current value which should be checked
- """
- if current is None:
- return
-
- current_above_threshold = current > self._config.parameter.threshold
-
- if self._config.parameter.extended_time:
- if current_above_threshold:
- self._extended_countdown.stop()
- habapp_rules.core.helper.send_if_different(self._config.items.switch, "ON")
-
- elif not current_above_threshold and self._config.items.switch.is_on():
- # start or reset the countdown
- self._extended_countdown.reset()
-
- else:
- # extended time is not active
- habapp_rules.core.helper.send_if_different(self._config.items.switch, "ON" if current_above_threshold else "OFF")
-
- def _cb_current_changed(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is called if the current value changed.
-
- :param event: event, which triggered this callback
- """
- self._check_current_and_set_switch(event.value)
+ """Rules class to manage basic light states.
+
+ # Items:
+ Number Current "Current"
+ Switch Something_is_ON "Something is ON"
+
+ # Config:
+ config = habapp_rules.sensors.config.current_switch.CurrentSwitchConfig(
+ items = habapp_rules.actors.config.light.CurrentSwitchItems(
+ current="Current",
+ switch="Something_is_ON"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.actors.power.CurrentSwitch(config)
+ """
+
+ def __init__(self, config: habapp_rules.sensors.config.current_switch.CurrentSwitchConfig) -> None:
+ """Init current switch rule.
+
+ Args:
+ config: config for current switch rule
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+ self._extended_countdown: CountdownJob | None = (
+ self.run.countdown(self._config.parameter.extended_time, habapp_rules.core.helper.send_if_different, item=self._config.items.switch, value="OFF") if self._config.parameter.extended_time else None
+ )
+
+ self._check_current_and_set_switch(self._config.items.current.value)
+ self._config.items.current.listen_event(self._cb_current_changed, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ def _check_current_and_set_switch(self, current: float | None) -> None:
+ """Check if current is above the threshold and set switch.
+
+ Args:
+ current: current value which should be checked
+ """
+ if current is None:
+ return
+
+ current_above_threshold = current > self._config.parameter.threshold
+
+ if self._config.parameter.extended_time:
+ if current_above_threshold:
+ self._extended_countdown.stop()
+ habapp_rules.core.helper.send_if_different(self._config.items.switch, "ON")
+
+ elif not current_above_threshold and self._config.items.switch.is_on():
+ # start or reset the countdown
+ self._extended_countdown.reset()
+
+ else:
+ # extended time is not active
+ habapp_rules.core.helper.send_if_different(self._config.items.switch, "ON" if current_above_threshold else "OFF")
+
+ def _cb_current_changed(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if the current value changed.
+
+ Args:
+ event: event, which triggered this callback
+ """
+ self._check_current_and_set_switch(event.value)
diff --git a/habapp_rules/sensors/dwd.py b/habapp_rules/sensors/dwd.py
index 496396d..0ba3c29 100644
--- a/habapp_rules/sensors/dwd.py
+++ b/habapp_rules/sensors/dwd.py
@@ -1,8 +1,10 @@
"""DWD rules."""
+
import dataclasses
import datetime
import logging
import re
+import typing
import HABApp
@@ -16,226 +18,223 @@
@dataclasses.dataclass
class DwdItems:
- """Dataclass for DWD items needed by DwdWindAlarm."""
-
- description: HABApp.openhab.items.StringItem
- warn_type: HABApp.openhab.items.StringItem
- severity: HABApp.openhab.items.StringItem
- start_time: HABApp.openhab.items.DatetimeItem
- end_time: HABApp.openhab.items.DatetimeItem
-
- SEVERITY_MAPPING = {
- "NULL": 0,
- "Minor": 1,
- "Moderate": 2,
- "Severe": 3,
- "Extreme": 4
- }
-
- @classmethod
- def from_prefix(cls, prefix: str) -> "DwdItems":
- """Init DwdItems from prefix
-
- :param prefix: common prefix of all DWD items
- :return: Dataclass with all needed DWD items
- """
- description = HABApp.openhab.items.StringItem.get_item(f"{prefix}_description")
- warn_type = HABApp.openhab.items.StringItem.get_item(f"{prefix}_type")
- severity = HABApp.openhab.items.StringItem.get_item(f"{prefix}_severity")
- start_time = HABApp.openhab.items.DatetimeItem.get_item(f"{prefix}_start_time")
- end_time = HABApp.openhab.items.DatetimeItem.get_item(f"{prefix}_end_time")
-
- return cls(description, warn_type, severity, start_time, end_time)
-
- @property
- def severity_as_int(self) -> int:
- """Get severity as integer.
-
- :return: severity as integer value
- """
- return self.SEVERITY_MAPPING.get(self.severity.value, 0)
-
-
-# pylint: disable=no-member
+ """Dataclass for DWD items needed by DwdWindAlarm."""
+
+ description: HABApp.openhab.items.StringItem
+ warn_type: HABApp.openhab.items.StringItem
+ severity: HABApp.openhab.items.StringItem
+ start_time: HABApp.openhab.items.DatetimeItem
+ end_time: HABApp.openhab.items.DatetimeItem
+
+ SEVERITY_MAPPING: typing.ClassVar = {"NULL": 0, "Minor": 1, "Moderate": 2, "Severe": 3, "Extreme": 4}
+
+ @classmethod
+ def from_prefix(cls, prefix: str) -> "DwdItems":
+ """Init DwdItems from prefix.
+
+ Args:
+ prefix: common prefix of all DWD items
+
+ Returns:
+ Dataclass with all needed DWD items
+ """
+ description = HABApp.openhab.items.StringItem.get_item(f"{prefix}_description")
+ warn_type = HABApp.openhab.items.StringItem.get_item(f"{prefix}_type")
+ severity = HABApp.openhab.items.StringItem.get_item(f"{prefix}_severity")
+ start_time = HABApp.openhab.items.DatetimeItem.get_item(f"{prefix}_start_time")
+ end_time = HABApp.openhab.items.DatetimeItem.get_item(f"{prefix}_end_time")
+
+ return cls(description, warn_type, severity, start_time, end_time)
+
+ @property
+ def severity_as_int(self) -> int:
+ """Get severity as integer.
+
+ Returns:
+ severity as integer value
+ """
+ return self.SEVERITY_MAPPING.get(self.severity.value, 0)
+
+
class DwdWindAlarm(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Rule for setting wind alarm by DWD warnings.
-
- # Items:
- Switch I26_99_wind_alarm "Wind alarm"
- Switch I26_99_wind_alarm_manual "Wind alarm manual"
- String I26_99_wind_alarm_state "Wind alarm state"
-
- String I26_99_warning_1_severity "Severity" {channel="dwdunwetter:dwdwarnings:ingolstadt:severity1"}
- String I26_99_warning_1_description "Description [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:description1"}
- DateTime I26_99_warning_1_start_time "valid from [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:onset1"}
- DateTime I26_99_warning_1_end_time "valid till [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:expires1"}
- String I26_99_warning_1_type "Type [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:event1"}
-
- String I26_99_warning_2_severity "Severity" {channel="dwdunwetter:dwdwarnings:ingolstadt:severity2"}
- String I26_99_warning_2_description "Description [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:description2"}
- DateTime I26_99_warning_2_start_time "valid from [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:onset2"}
- DateTime I26_99_warning_2_end_time "valid till [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:expires2"}
- String I26_99_warning_2_type "Type [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:event2"}
-
- # Config
- config = habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="I26_99_wind_alarm",
- manual="I26_99_wind_alarm_manual",
- state="I26_99_wind_alarm_state"
- ),
- parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
-
- )
- )
-
- # Rule init:
- habapp_rules.sensors.dwd.DwdWindAlarm(config)
- """
- states = [
- {"name": "Manual"},
- {"name": "Hand", "timeout": 20 * 3600, "on_timeout": "_auto_hand_timeout"},
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "On"},
- {"name": "Off"}
- ]}
- ]
-
- trans = [
- {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
- {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
- {"trigger": "hand", "source": "Auto", "dest": "Hand"},
-
- {"trigger": "wind_alarm_start", "source": "Auto_Off", "dest": "Auto_On"},
- {"trigger": "wind_alarm_end", "source": "Auto_On", "dest": "Auto_Off"},
- ]
-
- def __init__(self, config: habapp_rules.sensors.config.dwd.WindAlarmConfig) -> None:
- """Init of DWD wind alarm object.
-
- :param config: config for DWD wind alarm rule
- :raises TypeError: if type of hand_timeout is not supported
- """
- self._config = config
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.wind_alarm.name)
-
- self._items_dwd = [DwdItems.from_prefix(f"{self._config.parameter.dwd_item_prefix}{idx + 1}") for idx in range(self._config.parameter.number_dwd_objects)]
-
- # init state machine
- self._previous_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state") # this function is missing!
-
- self._wind_alarm_observer = habapp_rules.actors.state_observer.StateObserverSwitch(self._config.items.wind_alarm.name, self._cb_hand, self._cb_hand)
-
- self._set_timeouts()
- self._set_initial_state()
-
- # callbacks
- self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.hand_timeout is not None:
- self._config.items.hand_timeout.listen_event(self._cb_hand_timeout, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self.run.every(None, 300, self._cb_cyclic_check)
-
- def _get_initial_state(self, default_value: str = "") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.manual.is_on():
- return "Manual"
- if self._wind_alarm_active():
- return "Auto_On"
- return "Auto_Off"
-
- def _get_hand_timeout(self) -> int:
- """Get value of hand timeout.
-
- :return: hand timeout in seconds (0 is no timeout)
- """
- if self._config.items.hand_timeout is not None:
- if (item_value := self._config.items.hand_timeout.value) is None:
- self._instance_logger.warning("The value of the hand timeout item is None. Will use 24 hours as default!")
- return 24 * 3600
- return item_value
- return self._config.parameter.hand_timeout
-
- def _set_timeouts(self) -> None:
- """Set timeouts."""
- self.state_machine.get_state("Hand").timeout = self._get_hand_timeout()
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item and other states.
-
- This should method should be set to "after_state_change" of the state machine.
- """
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- if self.state == "Auto_On" and self._wind_alarm_observer.value is not True:
- self._wind_alarm_observer.send_command("ON")
- elif self.state == "Auto_Off" and self._wind_alarm_observer.value is not False:
- self._wind_alarm_observer.send_command("OFF")
-
- self._previous_state = self.state
-
- def on_enter_Auto_Init(self) -> None: # pylint: disable=invalid-name
- """Is called on entering of init state"""
- self._set_initial_state()
-
- def _cb_hand(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None:
- """Callback, which is triggered by the state observer if a manual change was detected.
-
- :param event: original trigger event
- """
- self.hand()
-
- def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the manual switch has a state change event.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.manual_on()
- else:
- self.manual_off()
-
- def _cb_hand_timeout(self, _) -> None:
- """Callback which is triggered if the timeout item changed."""
- self._set_timeouts()
-
- def _wind_alarm_active(self) -> bool:
- """Check if wind alarm is active
-
- :return: True if active, False if not
- """
- for dwd_items in self._items_dwd:
- if dwd_items.warn_type.value in {"BÖEN", "WIND", "STURM", "GEWITTER"}:
- speed_values = [int(value) for value in re.findall(r"\b(\d+)\s*km/h\b", dwd_items.description.value)]
- if not speed_values:
- continue
-
- if max(speed_values) >= self._config.parameter.threshold_wind_speed and dwd_items.severity_as_int >= self._config.parameter.threshold_severity:
- if dwd_items.start_time.value < datetime.datetime.now() < dwd_items.end_time.value:
- return True
- return False
-
- def _cb_cyclic_check(self) -> None:
- """Callback to check if wind alarm is active. This should be called cyclic every few minutes."""
- if self.state not in {"Auto_On", "Auto_Off"}:
- return
-
- if self._wind_alarm_active() and self.state != "Auto_On":
- self.wind_alarm_start()
-
- elif not self._wind_alarm_active() and self.state != "Auto_Off":
- self.wind_alarm_end()
+ """Rule for setting wind alarm by DWD warnings.
+
+ # Items:
+ Switch I26_99_wind_alarm "Wind alarm"
+ Switch I26_99_wind_alarm_manual "Wind alarm manual"
+ String I26_99_wind_alarm_state "Wind alarm state"
+
+ String I26_99_warning_1_severity "Severity" {channel="dwdunwetter:dwdwarnings:ingolstadt:severity1"}
+ String I26_99_warning_1_description "Description [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:description1"}
+ DateTime I26_99_warning_1_start_time "valid from [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:onset1"}
+ DateTime I26_99_warning_1_end_time "valid till [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:expires1"}
+ String I26_99_warning_1_type "Type [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:event1"}
+
+ String I26_99_warning_2_severity "Severity" {channel="dwdunwetter:dwdwarnings:ingolstadt:severity2"}
+ String I26_99_warning_2_description "Description [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:description2"}
+ DateTime I26_99_warning_2_start_time "valid from [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:onset2"}
+ DateTime I26_99_warning_2_end_time "valid till [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:expires2"}
+ String I26_99_warning_2_type "Type [%s]" {channel="dwdunwetter:dwdwarnings:ingolstadt:event2"}
+
+ # Config
+ config = habapp_rules.sensors.config.dwd.WindAlarmConfig(
+ items=habapp_rules.sensors.config.dwd.WindAlarmItems(
+ wind_alarm="I26_99_wind_alarm",
+ manual="I26_99_wind_alarm_manual",
+ state="I26_99_wind_alarm_state"
+ ),
+ parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
+
+ )
+ )
+
+ # Rule init:
+ habapp_rules.sensors.dwd.DwdWindAlarm(config)
+ """
+
+ states: typing.ClassVar = [{"name": "Manual"}, {"name": "Hand", "timeout": 20 * 3600, "on_timeout": "_auto_hand_timeout"}, {"name": "Auto", "initial": "Init", "children": [{"name": "Init"}, {"name": "On"}, {"name": "Off"}]}]
+
+ trans: typing.ClassVar = [
+ {"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
+ {"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
+ {"trigger": "hand", "source": "Auto", "dest": "Hand"},
+ {"trigger": "wind_alarm_start", "source": "Auto_Off", "dest": "Auto_On"},
+ {"trigger": "wind_alarm_end", "source": "Auto_On", "dest": "Auto_Off"},
+ ]
+
+ def __init__(self, config: habapp_rules.sensors.config.dwd.WindAlarmConfig) -> None:
+ """Init of DWD wind alarm object.
+
+ Args:
+ config: config for DWD wind alarm rule
+
+ Raises:
+ TypeError: if type of hand_timeout is not supported
+ """
+ self._config = config
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.wind_alarm.name)
+
+ self._items_dwd = [DwdItems.from_prefix(f"{self._config.parameter.dwd_item_prefix}{idx + 1}") for idx in range(self._config.parameter.number_dwd_objects)]
+
+ # init state machine
+ self._previous_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
+ model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state"
+ ) # this function is missing!
+
+ self._wind_alarm_observer = habapp_rules.actors.state_observer.StateObserverSwitch(self._config.items.wind_alarm.name, self._cb_hand, self._cb_hand)
+
+ self._set_timeouts()
+ self._set_initial_state()
+
+ # callbacks
+ self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.hand_timeout is not None:
+ self._config.items.hand_timeout.listen_event(self._cb_hand_timeout, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self.run.at(self.run.trigger.interval(None, 300), self._cb_cyclic_check)
+
+ def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.manual.is_on():
+ return "Manual"
+ if self._wind_alarm_active():
+ return "Auto_On"
+ return "Auto_Off"
+
+ def _get_hand_timeout(self) -> int:
+ """Get value of hand timeout.
+
+ Returns:
+ hand timeout in seconds (0 is no timeout)
+ """
+ if self._config.items.hand_timeout is not None:
+ if (item_value := self._config.items.hand_timeout.value) is None:
+ self._instance_logger.warning("The value of the hand timeout item is None. Will use 24 hours as default!")
+ return 24 * 3600
+ return item_value
+ return self._config.parameter.hand_timeout
+
+ def _set_timeouts(self) -> None:
+ """Set timeouts."""
+ self.state_machine.get_state("Hand").timeout = self._get_hand_timeout()
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item and other states.
+
+ This should method should be set to "after_state_change" of the state machine.
+ """
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ if self.state == "Auto_On" and self._wind_alarm_observer.value is not True:
+ self._wind_alarm_observer.send_command("ON")
+ elif self.state == "Auto_Off" and self._wind_alarm_observer.value is not False:
+ self._wind_alarm_observer.send_command("OFF")
+
+ self._previous_state = self.state
+
+ def on_enter_Auto_Init(self) -> None: # noqa: N802
+ """Is called on entering of init state."""
+ self._set_initial_state()
+
+ def _cb_hand(self, event: HABApp.openhab.events.ItemStateUpdatedEvent | HABApp.openhab.events.ItemCommandEvent) -> None: # noqa: ARG002
+ """Callback, which is triggered by the state observer if a manual change was detected.
+
+ Args:
+ event: original trigger event
+ """
+ self.hand()
+
+ def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the manual switch has a state change event.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.manual_on()
+ else:
+ self.manual_off()
+
+ def _cb_hand_timeout(self, _: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if the timeout item changed."""
+ self._set_timeouts()
+
+ def _wind_alarm_active(self) -> bool:
+ """Check if wind alarm is active.
+
+ Returns:
+ True if active, False if not
+ """
+ for dwd_items in self._items_dwd:
+ if dwd_items.warn_type.value in {"BÖEN", "WIND", "STURM", "GEWITTER"}:
+ speed_values = [int(value) for value in re.findall(r"\b(\d+)\s*km/h\b", dwd_items.description.value)]
+ if not speed_values:
+ continue
+
+ if (
+ max(speed_values) >= self._config.parameter.threshold_wind_speed and dwd_items.severity_as_int >= self._config.parameter.threshold_severity and dwd_items.start_time.value < datetime.datetime.now() < dwd_items.end_time.value # noqa: DTZ005
+ ):
+ return True
+ return False
+
+ def _cb_cyclic_check(self) -> None:
+ """Callback to check if wind alarm is active. This should be called cyclic every few minutes."""
+ if self.state not in {"Auto_On", "Auto_Off"}:
+ return
+
+ if self._wind_alarm_active() and self.state != "Auto_On":
+ self.wind_alarm_start()
+
+ elif not self._wind_alarm_active() and self.state != "Auto_Off":
+ self.wind_alarm_end()
diff --git a/habapp_rules/sensors/humidity.py b/habapp_rules/sensors/humidity.py
index 4a41656..8c7a7a0 100644
--- a/habapp_rules/sensors/humidity.py
+++ b/habapp_rules/sensors/humidity.py
@@ -1,5 +1,7 @@
"""Rule for evaluating a humidity sensor."""
+
import logging
+import typing
import HABApp
@@ -11,89 +13,95 @@
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member
class HumiditySwitch(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Rule for setting humidity switch if high humidity or a high humidity change is detected."""
-
- states = [
- {"name": "off"},
- {"name": "on", "initial": "HighHumidity", "children": [
- {"name": "HighHumidity"},
- {"name": "Extended", "timeout": 99, "on_timeout": "on_extended_timeout"},
- ]}
- ]
-
- trans = [
- {"trigger": "high_humidity_start", "source": "off", "dest": "on"},
- {"trigger": "high_humidity_start", "source": "on_Extended", "dest": "on_HighHumidity"},
- {"trigger": "high_humidity_end", "source": "on_HighHumidity", "dest": "on_Extended"},
- {"trigger": "on_extended_timeout", "source": "on_Extended", "dest": "off"},
- ]
-
- def __init__(self, config: habapp_rules.sensors.config.humidity.HumiditySwitchConfig) -> None:
- """Init humidity rule.
-
- :param config: config for humidity switch rule
- """
- self._config = config
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.humidity.name)
-
- # init state machine
- self._previous_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- self.state_machine.get_state("on_Extended").timeout = self._config.parameter.extended_time
-
- # register callbacks
- self._config.items.humidity.listen_event(self._cb_humidity, HABApp.openhab.events.ItemStateUpdatedEventFilter())
-
- def _update_openhab_state(self) -> None:
- """Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine."""
- super()._update_openhab_state()
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
-
- self._set_output()
- self._previous_state = self.state
-
- def _set_output(self) -> None:
- """Set output."""
- target_state = "ON" if self.state in {"on_HighHumidity", "on_Extended"} else "OFF"
- habapp_rules.core.helper.send_if_different(self._config.items.output, target_state)
-
- def _get_initial_state(self, default_value: str = "initial") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- return "on" if self._check_high_humidity() else "off"
-
- def _check_high_humidity(self, humidity_value: float | None = None) -> bool:
- """Check if humidity is above threshold.
-
- :param humidity_value: humidity value, which should be checked. If None, the value of the humidity item will be used
- :return: True if humidity is above threshold
- """
- if humidity_value is None:
- if self._config.items.humidity.value is None:
- return False
- humidity_value = self._config.items.humidity.value
-
- return humidity_value >= self._config.parameter.absolute_threshold
-
- def _cb_humidity(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
- """Callback, which is triggered if the humidity was updated.
-
- :param event: trigger event
- """
- if self._check_high_humidity(event.value):
- self.high_humidity_start()
- else:
- self.high_humidity_end()
+ """Rule for setting humidity switch if high humidity or a high humidity change is detected."""
+
+ states: typing.ClassVar = [
+ {"name": "off"},
+ {
+ "name": "on",
+ "initial": "HighHumidity",
+ "children": [
+ {"name": "HighHumidity"},
+ {"name": "Extended", "timeout": 99, "on_timeout": "on_extended_timeout"},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "high_humidity_start", "source": "off", "dest": "on"},
+ {"trigger": "high_humidity_start", "source": "on_Extended", "dest": "on_HighHumidity"},
+ {"trigger": "high_humidity_end", "source": "on_HighHumidity", "dest": "on_Extended"},
+ {"trigger": "on_extended_timeout", "source": "on_Extended", "dest": "off"},
+ ]
+
+ def __init__(self, config: habapp_rules.sensors.config.humidity.HumiditySwitchConfig) -> None:
+ """Init humidity rule.
+
+ Args:
+ config: config for humidity switch rule
+ """
+ self._config = config
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.humidity.name)
+
+ # init state machine
+ self._previous_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ self.state_machine.get_state("on_Extended").timeout = self._config.parameter.extended_time
+
+ # register callbacks
+ self._config.items.humidity.listen_event(self._cb_humidity, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine."""
+ super()._update_openhab_state()
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+
+ self._set_output()
+ self._previous_state = self.state
+
+ def _set_output(self) -> None:
+ """Set output."""
+ target_state = "ON" if self.state in {"on_HighHumidity", "on_Extended"} else "OFF"
+ habapp_rules.core.helper.send_if_different(self._config.items.output, target_state)
+
+ def _get_initial_state(self, default_value: str = "initial") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ return "on" if self._check_high_humidity() else "off"
+
+ def _check_high_humidity(self, humidity_value: float | None = None) -> bool:
+ """Check if humidity is above threshold.
+
+ Args:
+ humidity_value: humidity value, which should be checked. If None, the value of the humidity item will be used
+
+ Returns:
+ if humidity is above threshold
+ """
+ if humidity_value is None:
+ if self._config.items.humidity.value is None:
+ return False
+ humidity_value = self._config.items.humidity.value
+
+ return humidity_value >= self._config.parameter.absolute_threshold
+
+ def _cb_humidity(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
+ """Callback, which is triggered if the humidity was updated.
+
+ Args:
+ event: trigger event
+ """
+ if self._check_high_humidity(event.value):
+ self.high_humidity_start()
+ else:
+ self.high_humidity_end()
diff --git a/habapp_rules/sensors/motion.py b/habapp_rules/sensors/motion.py
index aed1d34..2614d0f 100644
--- a/habapp_rules/sensors/motion.py
+++ b/habapp_rules/sensors/motion.py
@@ -1,5 +1,7 @@
"""Rules for managing motion sensors."""
+
import logging
+import typing
import HABApp
@@ -14,12 +16,11 @@
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member, too-many-instance-attributes
class Motion(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Class for filtering motion sensors.
+ """Class for filtering motion sensors.
- # MQTT-things:
- Thing topic Motion "Motion Sensor"{
+ # MQTT-things:
+ Thing topic Motion "Motion Sensor"{
Channels:
Type switch : motion "Motion" [stateTopic="zigbee2mqtt/Motion/occupancy", on="true", off="false"]
Type number : brightness "Brightness" [stateTopic="zigbee2mqtt/Motion/illuminance_lux"]
@@ -27,245 +28,261 @@ class Motion(habapp_rules.core.state_machine_rule.StateMachineRule):
# Items:
Switch Motion_raw "Motion raw" {channel="mqtt:topic:broker:Motion:motion"}
- Switch Motion_filtered "Motion filtered"
- Number Motion_Brightness "Brightness" {channel="mqtt:topic:broker:Motion:brightness"}
- String I999_00_Sleeping_state "Sleeping state"
-
- # Config
- config = habapp_rules.sensors.config.motion.MotionConfig(
- items=habapp_rules.sensors.config.motion.MotionItems(
- motion_raw="Motion_raw",
- motion_filtered="Motion_filtered",
- brightness="Motion_Brightness",
- sleep_state="I999_00_Sleeping_state"
- ),
- parameter=habapp_rules.sensors.config.motion.MotionParameter(
- brightness_threshold=100,
- ),
- )
-
- # Rule init:
- habapp_rules.sensors.motion.Motion(config)
- """
- states = [
- {"name": "Locked"},
- {"name": "SleepLocked"},
- {"name": "PostSleepLocked", "timeout": 99, "on_timeout": "timeout_post_sleep_locked"},
- {"name": "Unlocked", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "Wait"},
- {"name": "Motion"},
- {"name": "MotionExtended", "timeout": 99, "on_timeout": "timeout_motion_extended"},
- {"name": "TooBright"},
- ]}
- ]
-
- trans = [
- # lock
- {"trigger": "lock_on", "source": ["Unlocked", "SleepLocked", "PostSleepLocked"], "dest": "Locked"},
- {"trigger": "lock_off", "source": "Locked", "dest": "Unlocked", "unless": "_sleep_active"},
- {"trigger": "lock_off", "source": "Locked", "dest": "SleepLocked", "conditions": "_sleep_active"},
-
- # sleep
- {"trigger": "sleep_started", "source": ["Unlocked", "PostSleepLocked"], "dest": "SleepLocked"},
- {"trigger": "sleep_end", "source": "SleepLocked", "dest": "Unlocked", "unless": "_post_sleep_lock_configured"},
- {"trigger": "sleep_end", "source": "SleepLocked", "dest": "PostSleepLocked", "conditions": "_post_sleep_lock_configured"},
- {"trigger": "timeout_post_sleep_locked", "source": "PostSleepLocked", "dest": "Unlocked", "unless": "_raw_motion_active"},
- {"trigger": "motion_off", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
- {"trigger": "motion_on", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
-
- # motion
- {"trigger": "motion_on", "source": "Unlocked_Wait", "dest": "Unlocked_Motion"},
- {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_MotionExtended", "conditions": "_motion_extended_configured"},
- {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_Wait", "unless": "_motion_extended_configured"},
- {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Wait", "unless": "_brightness_over_threshold"},
- {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_TooBright", "conditions": "_brightness_over_threshold"},
- {"trigger": "motion_on", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Motion"},
-
- # brightness
- {"trigger": "brightness_over_threshold", "source": "Unlocked_Wait", "dest": "Unlocked_TooBright"},
- {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Wait", "unless": "_raw_motion_active"},
- {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Motion", "conditions": "_raw_motion_active"}
- ]
-
- # pylint: disable=too-many-arguments
- def __init__(self, config: habapp_rules.sensors.config.motion.MotionConfig) -> None:
- """Init of motion filter.
-
- :param config: config for the motion filter
- :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not valid
- """
- self._config = config
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.motion_raw.name)
-
- self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(threshold := self._get_brightness_threshold(), threshold * 0.1 if threshold else 5) if self._config.items.brightness is not None else None
-
- # init state machine
- self._previous_state = None
- self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- self.state_machine.get_state("PostSleepLocked").timeout = self._config.parameter.post_sleep_lock_time
- self.state_machine.get_state("Unlocked_MotionExtended").timeout = self._config.parameter.extended_motion_time
-
- # register callbacks
- self._config.items.motion_raw.listen_event(self._cb_motion_raw, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.brightness is not None:
- self._config.items.brightness.listen_event(self._cb_brightness, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.brightness_threshold is not None:
- self._config.items.brightness_threshold.listen_event(self._cb_threshold_change, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.lock is not None:
- self._config.items.lock.listen_event(self._cb_lock, HABApp.openhab.events.ItemStateChangedEventFilter())
- if self._config.items.sleep_state is not None:
- self._config.items.sleep_state.listen_event(self._cb_sleep, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._instance_logger.debug(super().get_initial_log_message())
-
- def _get_initial_state(self, default_value: str = "initial") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: if OpenHAB item has a state it will return it, otherwise return the given default value
- """
- if self._config.items.lock is not None and self._config.items.lock.is_on():
- return "Locked"
- if self._config.items.sleep_state is not None and self._config.items.sleep_state.value == habapp_rules.system.SleepState.SLEEPING.value:
- return "SleepLocked"
- if self._config.items.brightness is not None and self._brightness_over_threshold():
- return "Unlocked_TooBright"
- if self._config.items.motion_raw.is_on():
- return "Unlocked_Motion"
- return "Unlocked_Wait"
-
- def _update_openhab_state(self):
- """Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine."""
- if self.state != self._previous_state:
- super()._update_openhab_state()
- self.__send_filtered_motion()
-
- self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
- self._previous_state = self.state
-
- def __send_filtered_motion(self) -> None:
- """Send filtered motion state to OpenHAB item."""
- target_state = "ON" if self.state in {"Unlocked_Motion", "Unlocked_MotionExtended"} else "OFF"
- if target_state != self._config.items.motion_filtered.value:
- self._config.items.motion_filtered.oh_send_command(target_state)
-
- def _raw_motion_active(self) -> bool:
- """Check if raw motion is active
-
- :return: True if active, else False
- """
- return bool(self._config.items.motion_raw)
-
- def _brightness_over_threshold(self) -> bool:
- """Check if brightness is over threshold
-
- :return: True if active, else False
- """
- return self._hysteresis_switch.get_output(self._config.items.brightness.value)
-
- def _motion_extended_configured(self) -> bool:
- """Check if extended motion is configured
-
- :return: True if active, else False
- """
- return self._config.parameter.extended_motion_time > 0
-
- def _post_sleep_lock_configured(self) -> bool:
- """Check if post sleep lock is configured
-
- :return: True if active, else False
- """
- return self._config.parameter.post_sleep_lock_time > 0
-
- def _sleep_active(self) -> bool:
- """Check if sleeping is active
-
- :return: True if sleeping is active, else False
- """
- return self._config.items.sleep_state.value == habapp_rules.system.SleepState.SLEEPING.value
-
- def _get_brightness_threshold(self) -> float:
- """Get the current brightness threshold value.
-
- :return: brightness threshold
- :raises habapp_rules.core.exceptions.HabAppRulesException: if brightness value not given by item or value
- """
- if self._config.parameter.brightness_threshold:
- return self._config.parameter.brightness_threshold
- if self._config.items.brightness_threshold is not None:
- return value if (value := self._config.items.brightness_threshold.value) else float("inf")
- raise habapp_rules.core.exceptions.HabAppRulesException(f"Can not get brightness threshold. Brightness value or item is not given. value: {self._config.parameter.brightness_threshold} | item: {self._config.items.brightness_threshold}")
-
- # pylint: disable=invalid-name
- def on_enter_Unlocked_Init(self):
- """Callback, which is called on enter of Unlocked_Init state"""
- if self._config.items.brightness is not None and self._brightness_over_threshold():
- self.to_Unlocked_TooBright()
- elif self._config.items.motion_raw.is_on():
- self.to_Unlocked_Motion()
- else:
- self.to_Unlocked_Wait()
-
- def _check_brightness(self, value: float | None = None) -> None:
- """Check if brightness is higher than the threshold and trigger the class methods.
-
- :param value: Value to check. None if last value should be used
- """
- if self._hysteresis_switch.get_output(value):
- self.brightness_over_threshold()
- else:
- self.brightness_below_threshold()
-
- def _cb_threshold_change(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the brightness threshold state changed.
-
- :param event: trigger event
- """
- self._hysteresis_switch.set_threshold_on(event.value)
- self._check_brightness()
-
- def _cb_motion_raw(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the raw motion state changed.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.motion_on()
- else:
- self.motion_off()
-
- def _cb_brightness(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the brightness state changed.
-
- :param event: trigger event
- """
- self._check_brightness(event.value)
-
- def _cb_lock(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the lock state changed.
-
- :param event: trigger event
- """
- if event.value == "ON":
- self.lock_on()
- else:
- self.lock_off()
-
- def _cb_sleep(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the sleep state changed.
-
- :param event: trigger event
- """
- if event.value == habapp_rules.system.SleepState.SLEEPING.value:
- self.sleep_started()
- if event.value == habapp_rules.system.SleepState.AWAKE.value:
- self.sleep_end()
+ Switch Motion_filtered "Motion filtered"
+ Number Motion_Brightness "Brightness" {channel="mqtt:topic:broker:Motion:brightness"}
+ String I999_00_Sleeping_state "Sleeping state"
+
+ # Config
+ config = habapp_rules.sensors.config.motion.MotionConfig(
+ items=habapp_rules.sensors.config.motion.MotionItems(
+ motion_raw="Motion_raw",
+ motion_filtered="Motion_filtered",
+ brightness="Motion_Brightness",
+ sleep_state="I999_00_Sleeping_state"
+ ),
+ parameter=habapp_rules.sensors.config.motion.MotionParameter(
+ brightness_threshold=100,
+ ),
+ )
+
+ # Rule init:
+ habapp_rules.sensors.motion.Motion(config)
+ """
+
+ states: typing.ClassVar = [
+ {"name": "Locked"},
+ {"name": "SleepLocked"},
+ {"name": "PostSleepLocked", "timeout": 99, "on_timeout": "timeout_post_sleep_locked"},
+ {
+ "name": "Unlocked",
+ "initial": "Init",
+ "children": [
+ {"name": "Init"},
+ {"name": "Wait"},
+ {"name": "Motion"},
+ {"name": "MotionExtended", "timeout": 99, "on_timeout": "timeout_motion_extended"},
+ {"name": "TooBright"},
+ ],
+ },
+ ]
+
+ trans: typing.ClassVar = [
+ # lock
+ {"trigger": "lock_on", "source": ["Unlocked", "SleepLocked", "PostSleepLocked"], "dest": "Locked"},
+ {"trigger": "lock_off", "source": "Locked", "dest": "Unlocked", "unless": "_sleep_active"},
+ {"trigger": "lock_off", "source": "Locked", "dest": "SleepLocked", "conditions": "_sleep_active"},
+ # sleep
+ {"trigger": "sleep_started", "source": ["Unlocked", "PostSleepLocked"], "dest": "SleepLocked"},
+ {"trigger": "sleep_end", "source": "SleepLocked", "dest": "Unlocked", "unless": "_post_sleep_lock_configured"},
+ {"trigger": "sleep_end", "source": "SleepLocked", "dest": "PostSleepLocked", "conditions": "_post_sleep_lock_configured"},
+ {"trigger": "timeout_post_sleep_locked", "source": "PostSleepLocked", "dest": "Unlocked", "unless": "_raw_motion_active"},
+ {"trigger": "motion_off", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
+ {"trigger": "motion_on", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
+ # motion
+ {"trigger": "motion_on", "source": "Unlocked_Wait", "dest": "Unlocked_Motion"},
+ {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_MotionExtended", "conditions": "_motion_extended_configured"},
+ {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_Wait", "unless": "_motion_extended_configured"},
+ {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Wait", "unless": "_brightness_over_threshold"},
+ {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_TooBright", "conditions": "_brightness_over_threshold"},
+ {"trigger": "motion_on", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Motion"},
+ # brightness
+ {"trigger": "brightness_over_threshold", "source": "Unlocked_Wait", "dest": "Unlocked_TooBright"},
+ {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Wait", "unless": "_raw_motion_active"},
+ {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Motion", "conditions": "_raw_motion_active"},
+ ]
+
+ def __init__(self, config: habapp_rules.sensors.config.motion.MotionConfig) -> None:
+ """Init of motion filter.
+
+ Args:
+ config: config for the motion filter
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesConfigurationException: if configuration is not valid
+ """
+ self._config = config
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, self._config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self._config.items.motion_raw.name)
+
+ self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(threshold := self._get_brightness_threshold(), threshold * 0.1 if threshold else 5) if self._config.items.brightness is not None else None
+
+ # init state machine
+ self._previous_state = None
+ self.state_machine = habapp_rules.core.state_machine_rule.HierarchicalStateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ self.state_machine.get_state("PostSleepLocked").timeout = self._config.parameter.post_sleep_lock_time
+ self.state_machine.get_state("Unlocked_MotionExtended").timeout = self._config.parameter.extended_motion_time
+
+ # register callbacks
+ self._config.items.motion_raw.listen_event(self._cb_motion_raw, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.brightness is not None:
+ self._config.items.brightness.listen_event(self._cb_brightness, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.brightness_threshold is not None:
+ self._config.items.brightness_threshold.listen_event(self._cb_threshold_change, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.lock is not None:
+ self._config.items.lock.listen_event(self._cb_lock, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if self._config.items.sleep_state is not None:
+ self._config.items.sleep_state.listen_event(self._cb_sleep, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._instance_logger.debug(super().get_initial_log_message())
+
+ def _get_initial_state(self, default_value: str = "initial") -> str: # noqa: ARG002
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ if OpenHAB item has a state it will return it, otherwise return the given default value
+ """
+ if self._config.items.lock is not None and self._config.items.lock.is_on():
+ return "Locked"
+ if self._config.items.sleep_state is not None and self._config.items.sleep_state.value == habapp_rules.system.SleepState.SLEEPING.value:
+ return "SleepLocked"
+ if self._config.items.brightness is not None and self._brightness_over_threshold():
+ return "Unlocked_TooBright"
+ if self._config.items.motion_raw.is_on():
+ return "Unlocked_Motion"
+ return "Unlocked_Wait"
+
+ def _update_openhab_state(self) -> None:
+ """Update OpenHAB state item. This should method should be set to "after_state_change" of the state machine."""
+ if self.state != self._previous_state:
+ super()._update_openhab_state()
+ self.__send_filtered_motion()
+
+ self._instance_logger.debug(f"State change: {self._previous_state} -> {self.state}")
+ self._previous_state = self.state
+
+ def __send_filtered_motion(self) -> None:
+ """Send filtered motion state to OpenHAB item."""
+ target_state = "ON" if self.state in {"Unlocked_Motion", "Unlocked_MotionExtended"} else "OFF"
+ if target_state != self._config.items.motion_filtered.value:
+ self._config.items.motion_filtered.oh_send_command(target_state)
+
+ def _raw_motion_active(self) -> bool:
+ """Check if raw motion is active.
+
+ Returns:
+ True if active, else False
+ """
+ return bool(self._config.items.motion_raw)
+
+ def _brightness_over_threshold(self) -> bool:
+ """Check if brightness is over threshold.
+
+ Returns:
+ True if active, else False
+ """
+ return self._hysteresis_switch.get_output(self._config.items.brightness.value)
+
+ def _motion_extended_configured(self) -> bool:
+ """Check if extended motion is configured.
+
+ Returns:
+ True if active, else False
+ """
+ return self._config.parameter.extended_motion_time > 0
+
+ def _post_sleep_lock_configured(self) -> bool:
+ """Check if post sleep lock is configured.
+
+ Returns:
+ rue if active, else False
+ """
+ return self._config.parameter.post_sleep_lock_time > 0
+
+ def _sleep_active(self) -> bool:
+ """Check if sleeping is active.
+
+ Returns:
+ True if sleeping is active, else False
+ """
+ return self._config.items.sleep_state.value == habapp_rules.system.SleepState.SLEEPING.value
+
+ def _get_brightness_threshold(self) -> float:
+ """Get the current brightness threshold value.
+
+ Returns:
+ brightness threshold
+
+ Raises:
+ habapp_rules.core.exceptions.HabAppRulesError: if brightness value not given by item or value
+ """
+ if self._config.parameter.brightness_threshold:
+ return self._config.parameter.brightness_threshold
+ if self._config.items.brightness_threshold is not None:
+ return value if (value := self._config.items.brightness_threshold.value) else float("inf")
+ msg = f"Can not get brightness threshold. Brightness value or item is not given. value: {self._config.parameter.brightness_threshold} | item: {self._config.items.brightness_threshold}"
+ raise habapp_rules.core.exceptions.HabAppRulesError(msg)
+
+ def on_enter_Unlocked_Init(self) -> None: # noqa: N802
+ """Callback, which is called on enter of Unlocked_Init state."""
+ if self._config.items.brightness is not None and self._brightness_over_threshold():
+ self.to_Unlocked_TooBright()
+ elif self._config.items.motion_raw.is_on():
+ self.to_Unlocked_Motion()
+ else:
+ self.to_Unlocked_Wait()
+
+ def _check_brightness(self, value: float | None = None) -> None:
+ """Check if brightness is higher than the threshold and trigger the class methods.
+
+ Args:
+ value: Value to check. None if last value should be used
+ """
+ if self._hysteresis_switch.get_output(value):
+ self.brightness_over_threshold()
+ else:
+ self.brightness_below_threshold()
+
+ def _cb_threshold_change(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the brightness threshold state changed.
+
+ Args:
+ event: trigger event
+ """
+ self._hysteresis_switch.set_threshold_on(event.value)
+ self._check_brightness()
+
+ def _cb_motion_raw(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the raw motion state changed.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.motion_on()
+ else:
+ self.motion_off()
+
+ def _cb_brightness(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the brightness state changed.
+
+ Args:
+ event: trigger event
+ """
+ self._check_brightness(event.value)
+
+ def _cb_lock(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the lock state changed.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == "ON":
+ self.lock_on()
+ else:
+ self.lock_off()
+
+ def _cb_sleep(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the sleep state changed.
+
+ Args:
+ event: trigger event
+ """
+ if event.value == habapp_rules.system.SleepState.SLEEPING.value:
+ self.sleep_started()
+ if event.value == habapp_rules.system.SleepState.AWAKE.value:
+ self.sleep_end()
diff --git a/habapp_rules/sensors/sun.py b/habapp_rules/sensors/sun.py
index fea271b..8dd80b0 100644
--- a/habapp_rules/sensors/sun.py
+++ b/habapp_rules/sensors/sun.py
@@ -1,4 +1,5 @@
"""Rules to handle sun sensors."""
+
import logging
import HABApp
@@ -15,221 +16,225 @@
class _SensorBase(HABApp.Rule):
- """Base class for sun sensors."""
-
- def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig | habapp_rules.sensors.config.sun.TemperatureDifferenceConfig, item_input: HABApp.openhab.items.NumberItem) -> None:
- """Init of base class for sun sensors.
-
- :param config: config for sun sensor
- :param item_input: item for input value (brightness or temperature difference)
- """
- self._config = config
-
- # init HABApp Rule
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)
-
- # init exponential filter
- name_input_exponential_filtered = f"H_{item_input.name.removeprefix('H_')}_filtered"
- habapp_rules.core.helper.create_additional_item(name_input_exponential_filtered, "Number", name_input_exponential_filtered.replace("_", " "), config.parameter.filtered_signal_groups)
- item_input_filtered = HABApp.openhab.items.NumberItem.get_item(name_input_exponential_filtered)
-
- exponential_filter_config = habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw=item_input,
- filtered=item_input_filtered),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=config.parameter.filter_tau,
- instant_increase=config.parameter.filter_instant_increase,
- instant_decrease=config.parameter.filter_instant_decrease
- )
- )
- habapp_rules.common.filter.ExponentialFilter(exponential_filter_config)
-
- # attributes
- self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(config.threshold, config.parameter.hysteresis, False)
-
- # callbacks
- item_input_filtered.listen_event(self._cb_input_filtered, HABApp.openhab.events.ItemStateChangedEventFilter())
- if config.items.threshold is not None:
- config.items.threshold.listen_event(self._cb_threshold, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- def _send_output(self, new_value: str) -> None:
- """Send output if different.
-
- :param new_value: new value which should be sent
- """
- if new_value != self._config.items.output.value:
- self._config.items.output.oh_send_command(new_value)
- self._instance_logger.debug(f"Set output '{self._config.items.output.name}' to {new_value}")
-
- def _cb_input_filtered(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the filtered input value changed.
-
- :param event: trigger event
- """
- value = self._hysteresis_switch.get_output(event.value)
- self._send_output(value)
-
- def _cb_threshold(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is triggered if the threshold changed.
-
- :param event: trigger event
- """
- self._hysteresis_switch.set_threshold_on(event.value)
+ """Base class for sun sensors."""
+ def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig | habapp_rules.sensors.config.sun.TemperatureDifferenceConfig, item_input: HABApp.openhab.items.NumberItem) -> None:
+ """Init of base class for sun sensors.
-class SensorBrightness(_SensorBase):
- """Rules class to set sun protection depending on brightness level.
+ Args:
+ config: config for sun sensor
+ item_input: item for input value (brightness or temperature difference)
+ """
+ self._config = config
- # Items:
- Number brightness "Current brightness [%d lux]" {channel="..."}
- Number brightness_threshold "Brightness threshold [%d lux]"
- Switch sun_protection_brightness "Sun protection brightness [%s] {channel="..."}
+ # init HABApp Rule
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)
- # Config:
- config = habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="brightness",
- output="sun_protection_brightness",
- brightness_threshold="brightness_threshold"
- )
- )
+ # init exponential filter
+ name_input_exponential_filtered = f"H_{item_input.name.removeprefix('H_')}_filtered"
+ habapp_rules.core.helper.create_additional_item(name_input_exponential_filtered, "Number", name_input_exponential_filtered.replace("_", " "), config.parameter.filtered_signal_groups)
+ item_input_filtered = HABApp.openhab.items.NumberItem.get_item(name_input_exponential_filtered)
- # Rule init:
- habapp_rules.sensors.sun.SensorBrightness(config)
- """
+ exponential_filter_config = habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw=item_input, filtered=item_input_filtered),
+ parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=config.parameter.filter_tau, instant_increase=config.parameter.filter_instant_increase, instant_decrease=config.parameter.filter_instant_decrease),
+ )
+ habapp_rules.common.filter.ExponentialFilter(exponential_filter_config)
- def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig) -> None:
- """Init of sun sensor which takes a brightness value
+ # attributes
+ self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(config.threshold, config.parameter.hysteresis, return_bool=False)
- :param config: config for the sun sensor which is using brightness
- """
- _SensorBase.__init__(self, config, config.items.brightness)
+ # callbacks
+ item_input_filtered.listen_event(self._cb_input_filtered, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if config.items.threshold is not None:
+ config.items.threshold.listen_event(self._cb_threshold, HABApp.openhab.events.ItemStateChangedEventFilter())
+ def _send_output(self, new_value: str) -> None:
+ """Send output if different.
-class SensorTemperatureDifference(_SensorBase):
- """Rules class to set sun protection depending on temperature difference. E.g. temperature in the sun / temperature in the shadow.
+ Args:
+ new_value: new value which should be sent
+ """
+ if new_value != self._config.items.output.value:
+ self._config.items.output.oh_send_command(new_value)
+ self._instance_logger.debug(f"Set output '{self._config.items.output.name}' to {new_value}")
+
+ def _cb_input_filtered(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the filtered input value changed.
- # Items:
- Number temperature_sun "Temperature sun [%.1f °C]" {channel="..."}
- Number temperature_shadow "Temperature shadow [%.1f °C]" {channel="..."}
- Number temperature_threshold "Temperature threshold [%.1f °C]"
- Switch sun_protection_temperature "Sun protection temperature [%s] {channel="..."}
+ Args:
+ event: trigger event
+ """
+ value = self._hysteresis_switch.get_output(event.value)
+ self._send_output(value)
- # Config:
- config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
- items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(
- temperatures=["temperature_sun", "temperature_shadow"],
- output="sun_protection_temperature",
- threshold="temperature_threshold"
- )
- )
+ def _cb_threshold(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is triggered if the threshold changed.
- # Rule init:
- habapp_rules.sensors.sun.SensorTempDiff(config)
- """
+ Args:
+ event: trigger event
+ """
+ self._hysteresis_switch.set_threshold_on(event.value)
- def __init__(self, config: habapp_rules.sensors.config.sun.TemperatureDifferenceConfig) -> None:
- """Init of sun sensor which takes a two or more temperature values (one in the sun and one in the shadow)
- :param config: config for the sun sensor which is using temperature items
- """
- self._config = config
- name_temperature_diff = f"H_Temperature_diff_for_{config.items.output.name}"
- habapp_rules.core.helper.create_additional_item(name_temperature_diff, "Number", name_temperature_diff.replace("_", " "), config.parameter.filtered_signal_groups)
- self._item_temp_diff = HABApp.openhab.items.NumberItem.get_item(name_temperature_diff)
+class SensorBrightness(_SensorBase):
+ """Rules class to set sun protection depending on brightness level.
+
+ # Items:
+ Number brightness "Current brightness [%d lux]" {channel="..."}
+ Number brightness_threshold "Brightness threshold [%d lux]"
+ Switch sun_protection_brightness "Sun protection brightness [%s] {channel="..."}
- _SensorBase.__init__(self, config, self._item_temp_diff)
+ # Config:
+ config = habapp_rules.sensors.config.sun.BrightnessConfig(
+ items=habapp_rules.sensors.config.sun.BrightnessItems(
+ brightness="brightness",
+ output="sun_protection_brightness",
+ brightness_threshold="brightness_threshold"
+ )
+ )
- # callbacks
- for temperature_item in self._config.items.temperatures:
- temperature_item.listen_event(self._cb_temperature, HABApp.openhab.events.ItemStateChangedEventFilter())
+ # Rule init:
+ habapp_rules.sensors.sun.SensorBrightness(config)
+ """
- # calculate temperature difference
- self._cb_temperature(None)
+ def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig) -> None:
+ """Init of sun sensor which takes a brightness value.
- def _cb_temperature(self, _: HABApp.openhab.events.ItemStateChangedEvent | None):
- """Callback, which is triggered if a temperature value changed."""
- filtered_items = [itm for itm in habapp_rules.core.helper.filter_updated_items(self._config.items.temperatures, self._config.parameter.ignore_old_values_time) if itm.value is not None]
- if len(filtered_items) < 2:
- return
- value_min = min(item.value for item in filtered_items)
- value_max = max(item.value for item in filtered_items)
+ Args:
+ config: config for the sun sensor which is using brightness
+ """
+ _SensorBase.__init__(self, config, config.items.brightness)
- self._item_temp_diff.oh_send_command(value_max - value_min)
+
+class SensorTemperatureDifference(_SensorBase):
+ """Rules class to set sun protection depending on temperature difference. E.g. temperature in the sun / temperature in the shadow.
+
+ # Items:
+ Number temperature_sun "Temperature sun [%.1f °C]" {channel="..."}
+ Number temperature_shadow "Temperature shadow [%.1f °C]" {channel="..."}
+ Number temperature_threshold "Temperature threshold [%.1f °C]"
+ Switch sun_protection_temperature "Sun protection temperature [%s] {channel="..."}
+
+ # Config:
+ config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
+ items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(
+ temperatures=["temperature_sun", "temperature_shadow"],
+ output="sun_protection_temperature",
+ threshold="temperature_threshold"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.sensors.sun.SensorTempDiff(config)
+ """
+
+ def __init__(self, config: habapp_rules.sensors.config.sun.TemperatureDifferenceConfig) -> None:
+ """Init of sun sensor which takes a two or more temperature values (one in the sun and one in the shadow).
+
+ Args:
+ config: config for the sun sensor which is using temperature items
+ """
+ self._config = config
+ name_temperature_diff = f"H_Temperature_diff_for_{config.items.output.name}"
+ habapp_rules.core.helper.create_additional_item(name_temperature_diff, "Number", name_temperature_diff.replace("_", " "), config.parameter.filtered_signal_groups)
+ self._item_temp_diff = HABApp.openhab.items.NumberItem.get_item(name_temperature_diff)
+
+ _SensorBase.__init__(self, config, self._item_temp_diff)
+
+ # callbacks
+ for temperature_item in self._config.items.temperatures:
+ temperature_item.listen_event(self._cb_temperature, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ # calculate temperature difference
+ self._cb_temperature(None)
+
+ def _cb_temperature(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
+ """Callback, which is triggered if a temperature value changed."""
+ filtered_items = [itm for itm in habapp_rules.core.helper.filter_updated_items(self._config.items.temperatures, self._config.parameter.ignore_old_values_time) if itm.value is not None]
+ if len(filtered_items) < 2: # noqa: PLR2004
+ return
+ value_min = min(item.value for item in filtered_items)
+ value_max = max(item.value for item in filtered_items)
+
+ self._item_temp_diff.oh_send_command(value_max - value_min)
class SunPositionFilter(HABApp.Rule):
- """Rules class to filter a switch state depending on the sun position. This can be used to only close the blinds of a window, if the sun hits the window
-
- # Items:
- Number sun_azimuth "Sun Azimuth [%.1f °]" {channel="astro..."}
- Number sun_elevation "Sun Elevation [%.1f °]" {channel="astro..."}
- Switch sun_shining "Sun is shining [%s]
- Switch sun_hits_window "Sun hits window [%s]
-
- # Config:
- config = habapp_rules.sensors.config.sun.SunPositionConfig(
- items=habapp_rules.sensors.config.sun.SunPositionItems(
- azimuth="sun_azimuth",
- elevation="sun_elevation",
- input="sun_shining",
- output="sun_hits_window"
- ),
- parameter=habapp_rules.sensors.config.sun.SunPositionParameter(
- sun_position_window=habapp_rules.sensors.config.sun.SunPositionWindow(40, 120)
- )
- )
-
- # Rule init:
-
- habapp_rules.sensors.sun.SunPositionFilter(config)
- """
-
- def __init__(self, config: habapp_rules.sensors.config.sun.SunPositionConfig) -> None:
- """Init of sun position filter.
-
- :param config: config for the sun position filter
- """
- self._config = config
-
- # init HABApp Rule
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)
-
- # callbacks
- config.items.azimuth.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter()) # listen_event for elevation is not needed because elevation and azimuth is updated together
- config.items.input.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._update_output(None)
-
- def _sun_in_window(self, azimuth: float, elevation: float) -> bool:
- """Check if the sun is in the 'sun window' where it hits the target.
-
- :param azimuth: azimuth of the sun
- :param elevation: elevation of the sun
- :return: True if the sun hits the target, else False
- """
- sun_in_window = False
-
- if any(window.azimuth_min <= azimuth <= window.azimuth_max and window.elevation_min <= elevation <= window.elevation_max for window in self._config.parameter.sun_position_windows):
- sun_in_window = True
-
- return sun_in_window
-
- def _update_output(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
- """Callback, which is triggered if the sun position or input changed."""
- azimuth = self._config.items.azimuth.value
- elevation = self._config.items.elevation.value
-
- if azimuth is None or elevation is None:
- self._instance_logger.warning(f"Azimuth or elevation is None -> will set output to input. azimuth = {azimuth} | elevation = {elevation}")
- filter_output = self._config.items.input.value
- elif self._config.items.input.value in {"OFF", None}:
- filter_output = "OFF"
- else:
- filter_output = "ON" if self._sun_in_window(azimuth, elevation) else "OFF"
-
- if filter_output != self._config.items.output.value:
- self._config.items.output.oh_send_command(filter_output)
+ """Rules class to filter a switch state depending on the sun position. This can be used to only close the blinds of a window, if the sun hits the window.
+
+ # Items:
+ Number sun_azimuth "Sun Azimuth [%.1f °]" {channel="astro..."}
+ Number sun_elevation "Sun Elevation [%.1f °]" {channel="astro..."}
+ Switch sun_shining "Sun is shining [%s]
+ Switch sun_hits_window "Sun hits window [%s]
+
+ # Config:
+ config = habapp_rules.sensors.config.sun.SunPositionConfig(
+ items=habapp_rules.sensors.config.sun.SunPositionItems(
+ azimuth="sun_azimuth",
+ elevation="sun_elevation",
+ input="sun_shining",
+ output="sun_hits_window"
+ ),
+ parameter=habapp_rules.sensors.config.sun.SunPositionParameter(
+ sun_position_window=habapp_rules.sensors.config.sun.SunPositionWindow(40, 120)
+ )
+ )
+
+ # Rule init:
+
+ habapp_rules.sensors.sun.SunPositionFilter(config)
+ """
+
+ def __init__(self, config: habapp_rules.sensors.config.sun.SunPositionConfig) -> None:
+ """Init of sun position filter.
+
+ Args:
+ config: config for the sun position filter
+ """
+ self._config = config
+
+ # init HABApp Rule
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)
+
+ # callbacks
+ config.items.azimuth.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter()) # listen_event for elevation is not needed because elevation and azimuth is updated together
+ config.items.input.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._update_output(None)
+
+ def _sun_in_window(self, azimuth: float, elevation: float) -> bool:
+ """Check if the sun is in the 'sun window' where it hits the target.
+
+ Args:
+ azimuth: azimuth of the sun
+ elevation: elevation of the sun
+
+ Returns:
+ True if the sun hits the target, else False
+ """
+ sun_in_window = False
+
+ if any(window.azimuth_min <= azimuth <= window.azimuth_max and window.elevation_min <= elevation <= window.elevation_max for window in self._config.parameter.sun_position_windows):
+ sun_in_window = True
+
+ return sun_in_window
+
+ def _update_output(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
+ """Callback, which is triggered if the sun position or input changed."""
+ azimuth = self._config.items.azimuth.value
+ elevation = self._config.items.elevation.value
+
+ if azimuth is None or elevation is None:
+ self._instance_logger.warning(f"Azimuth or elevation is None -> will set output to input. azimuth = {azimuth} | elevation = {elevation}")
+ filter_output = self._config.items.input.value
+ elif self._config.items.input.value in {"OFF", None}:
+ filter_output = "OFF"
+ else:
+ filter_output = "ON" if self._sun_in_window(azimuth, elevation) else "OFF"
+
+ if filter_output != self._config.items.output.value:
+ self._config.items.output.oh_send_command(filter_output)
diff --git a/habapp_rules/system/__init__.py b/habapp_rules/system/__init__.py
index 78d6492..73d7197 100644
--- a/habapp_rules/system/__init__.py
+++ b/habapp_rules/system/__init__.py
@@ -1,19 +1,22 @@
"""States of all system state machines."""
+
import enum
class SleepState(enum.Enum):
- """Sleep states."""
- AWAKE = "awake"
- PRE_SLEEPING = "pre_sleeping"
- SLEEPING = "sleeping"
- POST_SLEEPING = "post_sleeping"
- LOCKED = "locked"
+ """Sleep states."""
+
+ AWAKE = "awake"
+ PRE_SLEEPING = "pre_sleeping"
+ SLEEPING = "sleeping"
+ POST_SLEEPING = "post_sleeping"
+ LOCKED = "locked"
class PresenceState(enum.Enum):
- """Presence states."""
- PRESENCE = "presence"
- LEAVING = "leaving"
- ABSENCE = "absence"
- LONG_ABSENCE = "long_absence"
+ """Presence states."""
+
+ PRESENCE = "presence"
+ LEAVING = "leaving"
+ ABSENCE = "absence"
+ LONG_ABSENCE = "long_absence"
diff --git a/habapp_rules/system/config/item_watchdog.py b/habapp_rules/system/config/item_watchdog.py
new file mode 100644
index 0000000..7533796
--- /dev/null
+++ b/habapp_rules/system/config/item_watchdog.py
@@ -0,0 +1,26 @@
+"""Config models for watchdog rules."""
+
+import HABApp.openhab.items
+import pydantic
+
+import habapp_rules.core.pydantic_base
+
+
+class WatchdogItems(habapp_rules.core.pydantic_base.ItemBase):
+ """Items for watchdog rule."""
+
+ observed: HABApp.openhab.items.OpenhabItem = pydantic.Field(..., description="observed item")
+ warning: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="warning item, which will be set to ON if the observed item was not updated in the expected time")
+
+
+class WatchdogParameter(habapp_rules.core.pydantic_base.ParameterBase):
+ """Parameter for watchdog rule."""
+
+ timeout: int = pydantic.Field(3600, description="timeout in seconds")
+
+
+class WatchdogConfig(habapp_rules.core.pydantic_base.ConfigBase):
+ """Config for watchdog rule."""
+
+ items: WatchdogItems = pydantic.Field(..., description="items for watchdog rule")
+ parameter: WatchdogParameter = pydantic.Field(WatchdogParameter(), description="parameters for watchdog rule")
diff --git a/habapp_rules/system/config/notification.py b/habapp_rules/system/config/notification.py
index c37c090..92aa98a 100644
--- a/habapp_rules/system/config/notification.py
+++ b/habapp_rules/system/config/notification.py
@@ -1,4 +1,5 @@
"""Config models for presence rules."""
+
import HABApp
import pydantic
from multi_notifier.connectors.connector_mail import Mail
@@ -8,17 +9,20 @@
class NotificationItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for presence detection."""
- target_item: HABApp.openhab.items.OpenhabItem = pydantic.Field(..., description="Item which state change triggers a notification")
+ """Items for presence detection."""
+
+ target_item: HABApp.openhab.items.OpenhabItem = pydantic.Field(..., description="Item which state change triggers a notification")
class NotificationParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for notification."""
- notify_connector: Mail | Telegram = pydantic.Field(..., description="Notifier which is used for sending notifications")
- recipients: str | list[str] = pydantic.Field(..., description="Recipients which should be notified")
+ """Parameter for notification."""
+
+ notify_connector: Mail | Telegram = pydantic.Field(..., description="Notifier which is used for sending notifications")
+ recipients: str | list[str] = pydantic.Field(..., description="Recipients which should be notified")
class NotificationConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for notification."""
- items: NotificationItems = pydantic.Field(..., description="items for notification")
- parameter: NotificationParameter = pydantic.Field(..., description="parameter for notification")
+ """Config for notification."""
+
+ items: NotificationItems = pydantic.Field(..., description="items for notification")
+ parameter: NotificationParameter = pydantic.Field(..., description="parameter for notification")
diff --git a/habapp_rules/system/config/presence.py b/habapp_rules/system/config/presence.py
index 6d8c730..3757e1e 100644
--- a/habapp_rules/system/config/presence.py
+++ b/habapp_rules/system/config/presence.py
@@ -1,4 +1,5 @@
"""Config models for presence rules."""
+
import HABApp
import pydantic
@@ -6,15 +7,17 @@
class PresenceItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for presence detection."""
- presence: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="presence item")
- leaving: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="leaving item")
- outdoor_doors: list[HABApp.openhab.items.ContactItem] = pydantic.Field([], description="list of door contacts which are used to detect presence if outside door was opened")
- phones: list[HABApp.openhab.items.SwitchItem] = pydantic.Field([], description="list of phone items which are used to detect presence and leaving depending on present phones")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
+ """Items for presence detection."""
+
+ presence: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="presence item")
+ leaving: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="leaving item")
+ outdoor_doors: list[HABApp.openhab.items.ContactItem] = pydantic.Field([], description="list of door contacts which are used to detect presence if outside door was opened")
+ phones: list[HABApp.openhab.items.SwitchItem] = pydantic.Field([], description="list of phone items which are used to detect presence and leaving depending on present phones")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
class PresenceConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for presence detection."""
- items: PresenceItems = pydantic.Field(..., description="items for presence detection")
- parameter: None = None
+ """Config for presence detection."""
+
+ items: PresenceItems = pydantic.Field(..., description="items for presence detection")
+ parameter: None = None
diff --git a/habapp_rules/system/config/sleep.py b/habapp_rules/system/config/sleep.py
index 272d960..e010cfa 100644
--- a/habapp_rules/system/config/sleep.py
+++ b/habapp_rules/system/config/sleep.py
@@ -1,4 +1,5 @@
"""Config models for sleep rules."""
+
import datetime
import HABApp
@@ -8,37 +9,43 @@
class SleepItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for sleep detection."""
- sleep: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep item")
- sleep_request: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep request item")
- lock: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock item")
- lock_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock request item")
- display_text: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="display text item")
- state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
+ """Items for sleep detection."""
+
+ sleep: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep item")
+ sleep_request: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep request item")
+ lock: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock item")
+ lock_request: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="lock request item")
+ display_text: HABApp.openhab.items.StringItem | None = pydantic.Field(None, description="display text item")
+ state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="state item")
class SleepConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for sleep detection."""
- items: SleepItems = pydantic.Field(..., description="items for sleep state")
- parameter: None = None
+ """Config for sleep detection."""
+
+ items: SleepItems = pydantic.Field(..., description="items for sleep state")
+ parameter: None = None
+
+# LINK SLEEP ##############################
-############################## LINK SLEEP ##############################
class LinkSleepItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for sleep detection."""
- sleep_master: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep item of the the master item which will link it's state to the slave items")
- sleep_request_slaves: list[HABApp.openhab.items.SwitchItem] = pydantic.Field(..., description="list of sleep request items of the slaves")
- link_active_feedback: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON if link is active or OFF if link is not active anymore")
+ """Items for sleep detection."""
+
+ sleep_master: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="sleep item of the the master item which will link it's state to the slave items")
+ sleep_request_slaves: list[HABApp.openhab.items.SwitchItem] = pydantic.Field(..., description="list of sleep request items of the slaves")
+ link_active_feedback: HABApp.openhab.items.SwitchItem | None = pydantic.Field(None, description="item which is ON if link is active or OFF if link is not active anymore")
class LinkSleepParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Config for sleep detection."""
- link_time_start: datetime.time = pydantic.Field(datetime.time(0), description="Start time when the linking is active")
- link_time_end: datetime.time = pydantic.Field(datetime.time(23, 59, 59), description="End time when the linking is not active anymore")
+ """Config for sleep detection."""
+
+ link_time_start: datetime.time = pydantic.Field(datetime.time(0), description="Start time when the linking is active")
+ link_time_end: datetime.time = pydantic.Field(datetime.time(23, 59, 59), description="End time when the linking is not active anymore")
class LinkSleepConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for sleep detection."""
- items: LinkSleepItems = pydantic.Field(..., description="items for sleep state")
- parameter: LinkSleepParameter = pydantic.Field(LinkSleepParameter(), description="parameter for link sleep")
+ """Config for sleep detection."""
+
+ items: LinkSleepItems = pydantic.Field(..., description="items for sleep state")
+ parameter: LinkSleepParameter = pydantic.Field(LinkSleepParameter(), description="parameter for link sleep")
diff --git a/habapp_rules/system/config/summer_winter.py b/habapp_rules/system/config/summer_winter.py
index a36c2cb..06b27d5 100644
--- a/habapp_rules/system/config/summer_winter.py
+++ b/habapp_rules/system/config/summer_winter.py
@@ -1,4 +1,5 @@
"""Config models for summer / winter rules."""
+
import HABApp
import pydantic
@@ -6,20 +7,23 @@
class SummerWinterItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for summer/winter detection."""
- outside_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="outside temperature item")
- summer: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="summer item")
- last_check: HABApp.openhab.items.DatetimeItem | None = pydantic.Field(None, description="last check item")
+ """Items for summer/winter detection."""
+
+ outside_temperature: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="outside temperature item")
+ summer: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="summer item")
+ last_check: HABApp.openhab.items.DatetimeItem | None = pydantic.Field(None, description="last check item")
class SummerWinterParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for summer/winter detection."""
- persistence_service: str | None = pydantic.Field(None, description="name of persistence service")
- days: int = pydantic.Field(5, description="number of days in the past which will be used to check if it is summer")
- temperature_threshold: float = pydantic.Field(16, description="threshold weighted temperature for summer")
+ """Parameter for summer/winter detection."""
+
+ persistence_service: str | None = pydantic.Field(None, description="name of persistence service")
+ days: int = pydantic.Field(5, description="number of days in the past which will be used to check if it is summer")
+ temperature_threshold: float = pydantic.Field(16, description="threshold weighted temperature for summer")
class SummerWinterConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for summer/winter detection."""
- items: SummerWinterItems = pydantic.Field(..., description="items for summer/winter state")
- parameter: SummerWinterParameter = pydantic.Field(SummerWinterParameter(), description="parameter for summer/winter")
+ """Config for summer/winter detection."""
+
+ items: SummerWinterItems = pydantic.Field(..., description="items for summer/winter state")
+ parameter: SummerWinterParameter = pydantic.Field(SummerWinterParameter(), description="parameter for summer/winter")
diff --git a/habapp_rules/system/config/watchdog.py b/habapp_rules/system/config/watchdog.py
deleted file mode 100644
index 855abca..0000000
--- a/habapp_rules/system/config/watchdog.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Config models for watchdog rules."""
-import HABApp.openhab.items
-import pydantic
-
-import habapp_rules.core.pydantic_base
-
-
-class WatchdogItems(habapp_rules.core.pydantic_base.ItemBase):
- """Items for watchdog rule."""
- observed: HABApp.openhab.items.OpenhabItem = pydantic.Field(..., description="observed item")
- warning: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="warning item, which will be set to ON if the observed item was not updated in the expected time")
-
-
-class WatchdogParameter(habapp_rules.core.pydantic_base.ParameterBase):
- """Parameter for watchdog rule."""
- timeout: int = pydantic.Field(3600, description="timeout in seconds")
-
-
-class WatchdogConfig(habapp_rules.core.pydantic_base.ConfigBase):
- """Config for watchdog rule."""
- items: WatchdogItems = pydantic.Field(..., description="items for watchdog rule")
- parameter: WatchdogParameter = pydantic.Field(WatchdogParameter(), description="parameters for watchdog rule")
diff --git a/habapp_rules/system/item_watchdog.py b/habapp_rules/system/item_watchdog.py
new file mode 100644
index 0000000..6cea954
--- /dev/null
+++ b/habapp_rules/system/item_watchdog.py
@@ -0,0 +1,47 @@
+"""Watchdog rules."""
+
+import HABApp
+
+import habapp_rules.core.helper
+from habapp_rules.system.config.item_watchdog import WatchdogConfig
+
+
+class ItemWatchdog(HABApp.Rule):
+ """Watchdog rule to check if the observed item was updated in time.
+
+ # Items:
+ Switch Item_To_Observe "Item which should be observed"
+ Switch Warning "Warning, item was not updated in time"
+
+ # Config:
+ config = habapp_rules.system.config.WatchdogConfig(
+ items=habapp_rules.system.config.WatchdogItems(
+ observed="Item_To_Observe",
+ warning="Warning")
+ )
+
+ # Rule init:
+ habapp_rules.system.watchdog.Watchdog(config)
+ """
+
+ def __init__(self, config: WatchdogConfig) -> None:
+ """Init watchdog rule.
+
+ Args:
+ config: Config for watchdog rule
+ """
+ HABApp.Rule.__init__(self)
+ self._config = config
+
+ self._countdown = self.run.countdown(self._config.parameter.timeout, habapp_rules.core.helper.send_if_different, item=self._config.items.warning, value="ON")
+ self._countdown.reset()
+ self._config.items.observed.listen_event(self._cb_observed_state_updated, HABApp.openhab.events.ItemStateUpdatedEventFilter())
+
+ def _cb_observed_state_updated(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None: # noqa: ARG002
+ """Callback which is called if the observed item was updated.
+
+ Args:
+ event: event which triggered this callback
+ """
+ habapp_rules.core.helper.send_if_different(self._config.items.warning, "OFF")
+ self._countdown.reset()
diff --git a/habapp_rules/system/notification.py b/habapp_rules/system/notification.py
index 29796d1..0efcd64 100644
--- a/habapp_rules/system/notification.py
+++ b/habapp_rules/system/notification.py
@@ -1,4 +1,5 @@
"""Rules for notification."""
+
import HABApp
from multi_notifier.connectors.connector_telegram import Telegram
@@ -6,26 +7,28 @@
class SendStateChanged(HABApp.Rule):
- """Rule class to send a telegram if the state of an item changes."""
+ """Rule class to send a telegram if the state of an item changes."""
- def __init__(self, config: habapp_rules.system.config.notification.NotificationConfig) -> None:
- """Init the rule object.
+ def __init__(self, config: habapp_rules.system.config.notification.NotificationConfig) -> None:
+ """Init the rule object.
- :param config: config for notification rule
- """
- self._config = config
- HABApp.Rule.__init__(self)
+ Args:
+ config: config for notification rule
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
- self._config.items.target_item.listen_event(self._send_state_change, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._config.items.target_item.listen_event(self._send_state_change, HABApp.openhab.events.ItemStateChangedEventFilter())
- def _send_state_change(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is called if the state of the item changed.
+ def _send_state_change(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is called if the state of the item changed.
- :param event: event which triggered the callback
- """
- msg = f"{event.name} changed from {event.old_value} to {event.value}"
+ Args:
+ event: event which triggered the callback
+ """
+ msg = f"{event.name} changed from {event.old_value} to {event.value}"
- if isinstance(self._config.parameter.notify_connector, Telegram):
- self._config.parameter.notify_connector.send_message(self._config.parameter.recipients, msg)
- else:
- self._config.parameter.notify_connector.send_message(self._config.parameter.recipients, msg, subject=f"{event.name} changed")
+ if isinstance(self._config.parameter.notify_connector, Telegram):
+ self._config.parameter.notify_connector.send_message(self._config.parameter.recipients, msg)
+ else:
+ self._config.parameter.notify_connector.send_message(self._config.parameter.recipients, msg, subject=f"{event.name} changed")
diff --git a/habapp_rules/system/presence.py b/habapp_rules/system/presence.py
index 69f383c..b988d42 100644
--- a/habapp_rules/system/presence.py
+++ b/habapp_rules/system/presence.py
@@ -1,6 +1,8 @@
"""Rule to detect presence or absence."""
+
import logging
import threading
+import typing
import HABApp.openhab.definitions
import HABApp.openhab.events
@@ -16,184 +18,187 @@
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member
class Presence(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Rule class to manage the presence of a home.
+ """Rule class to manage the presence of a home.
- Hint: If you have some kind of guest-mode, use a guest-available switch as a phone to enable a persistent presence, also if all phones are not at home
+ Hint: If you have some kind of guest-mode, use a guest-available switch as a phone to enable a persistent presence, also if all phones are not at home
- Example OpenHAB configuration:
- # KNX-things:
- Thing device T00_99_OpenHab_Presence "KNX OpenHAB Presence"{
+ Example OpenHAB configuration:
+ # KNX-things:
+ Thing device T00_99_OpenHab_Presence "KNX OpenHAB Presence"{
Type switch-control : presence "Presence" [ ga="0/2/11+0/2/10"]
Type switch-control : leaving "Leaving" [ ga="0/2/21+0/2/20"]
}
# Items:
Switch I01_00_Presence "Presence [%s]" (G00_00_rrd4j) ["Status", "Presence"] {channel="knx:device:bridge:T00_99_OpenHab_Presence:presence"}
- Switch I01_00_Leaving "Leaving [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Presence:leaving"}
-
- # Config:
- config = habapp_rules.system.config.presence.PresenceConfig(
- items=habapp_rules.system.config.presence.PresenceItems(
- presence="I01_00_Presence",
- leaving="I01_00_Leaving"
- )
- )
-
- # Rule init:
- habapp_rules.system.presence.Presence(config)
- """
-
- states = [
- {"name": "presence"},
- {"name": "leaving", "timeout": 5 * 60, "on_timeout": "absence_detected"}, # leaving takes 5 minutes
- {"name": "absence", "timeout": 1.5 * 24 * 3600, "on_timeout": "long_absence_detected"}, # switch to long absence after 1.5 days
- {"name": "long_absence"}
- ]
-
- trans = [
- {"trigger": "presence_detected", "source": ["absence", "long_absence"], "dest": "presence"},
- {"trigger": "leaving_detected", "source": ["presence", "absence", "long_absence"], "dest": "leaving"},
- {"trigger": "abort_leaving", "source": "leaving", "dest": "presence"},
- {"trigger": "absence_detected", "source": ["presence", "leaving"], "dest": "absence"},
- {"trigger": "long_absence_detected", "source": "absence", "dest": "long_absence"},
- ]
-
- def __init__(self, config: habapp_rules.system.config.presence.PresenceConfig) -> None:
- """Init of Presence object.
-
- :param config: config for presence detection
- """
- self._config = config
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.presence.name)
-
- # init state machine
- self.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- # add callbacks
- self._config.items.leaving.listen_event(self._cb_leaving, HABApp.openhab.events.ItemStateChangedEventFilter())
- self._config.items.presence.listen_event(self._cb_presence, HABApp.openhab.events.ItemStateChangedEventFilter())
- for door_item in self._config.items.outdoor_doors:
- door_item.listen_event(self._cb_outside_door, HABApp.core.events.ValueChangeEventFilter())
- for phone_item in self._config.items.phones:
- 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 = PresenceState.PRESENCE.value) -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: return correct state if it could be detected, if not return default value
- """
- phone_items = [phone for phone in self._config.items.phones if phone.value is not None] # phones with valid state (not None)
- if phone_items:
- if any((item.value == "ON" for item in phone_items)):
- return PresenceState.PRESENCE.value
-
- if self._config.items.presence.value == "ON":
- return PresenceState.LEAVING.value
- return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value
-
- if self._config.items.leaving.value == "ON":
- return PresenceState.LEAVING.value
-
- if self._config.items.presence.value == "ON":
- return PresenceState.PRESENCE.value
-
- if self._config.items.presence.value == "OFF":
- return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value
-
- return default_value
-
- def _update_openhab_state(self) -> None:
- """Extend _update_openhab state of base class to also update other OpenHAB items."""
- super()._update_openhab_state()
- self._instance_logger.info(f"Presence state changed to {self.state}")
-
- # update presence item
- target_value = "ON" if self.state in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value} else "OFF"
- habapp_rules.core.helper.send_if_different(self._config.items.presence, target_value)
- habapp_rules.core.helper.send_if_different(self._config.items.leaving, "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 not in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value}:
- self._instance_logger.debug(f"Presence detected by door ({event.name})")
- self.presence_detected()
-
- def _cb_leaving(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is called if leaving item changed state.
-
- :param event: Item state change event of leaving item
- """
- 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 == PresenceState.LEAVING.value:
- self._instance_logger.debug("Abort leaving through leaving switch")
- self.abort_leaving()
-
- def _cb_presence(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is called if presence item changed state.
-
- :param event: Item state change event of presence item
- """
- 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 {PresenceState.PRESENCE.value, PresenceState.LEAVING.value}:
- self._instance_logger.debug("Absence was set manually by presence switch")
- self.absence_detected()
-
- def _cb_phone(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback, which is called if a phone state changed.
-
- :param event: Item state change event of phone item
- """
- active_phones = len([phone for phone in self._config.items.phones if phone.value == "ON"])
- if active_phones == 1 and event.value == "ON":
- # first phone switched to ON
- if self.__phone_absence_timer:
- self.__phone_absence_timer.cancel()
- self.__phone_absence_timer = None
-
- if self.state == PresenceState.LEAVING.value:
- self._instance_logger.debug("Leaving was aborted through first phone which came online")
- self.abort_leaving()
-
- 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()
-
- elif active_phones == 0 and event.value == "OFF":
- # last phone switched to OFF
- self.__phone_absence_timer = threading.Timer(20 * 60, self.__set_leaving_through_phone)
- self.__phone_absence_timer.start()
-
- def __set_leaving_through_phone(self) -> None:
- """Set leaving detected if timeout expired."""
- 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
+ Switch I01_00_Leaving "Leaving [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Presence:leaving"}
+
+ # Config:
+ config = habapp_rules.system.config.presence.PresenceConfig(
+ items=habapp_rules.system.config.presence.PresenceItems(
+ presence="I01_00_Presence",
+ leaving="I01_00_Leaving"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.system.presence.Presence(config)
+ """
+
+ states: typing.ClassVar = [
+ {"name": "presence"},
+ {"name": "leaving", "timeout": 5 * 60, "on_timeout": "absence_detected"}, # leaving takes 5 minutes
+ {"name": "absence", "timeout": 1.5 * 24 * 3600, "on_timeout": "long_absence_detected"}, # switch to long absence after 1.5 days
+ {"name": "long_absence"},
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "presence_detected", "source": ["absence", "long_absence"], "dest": "presence"},
+ {"trigger": "leaving_detected", "source": ["presence", "absence", "long_absence"], "dest": "leaving"},
+ {"trigger": "abort_leaving", "source": "leaving", "dest": "presence"},
+ {"trigger": "absence_detected", "source": ["presence", "leaving"], "dest": "absence"},
+ {"trigger": "long_absence_detected", "source": "absence", "dest": "long_absence"},
+ ]
+
+ def __init__(self, config: habapp_rules.system.config.presence.PresenceConfig) -> None:
+ """Init of Presence object.
+
+ Args:
+ config: config for presence detection
+ """
+ self._config = config
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.presence.name)
+
+ # init state machine
+ self.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ # add callbacks
+ self._config.items.leaving.listen_event(self._cb_leaving, HABApp.openhab.events.ItemStateChangedEventFilter())
+ self._config.items.presence.listen_event(self._cb_presence, HABApp.openhab.events.ItemStateChangedEventFilter())
+ for door_item in self._config.items.outdoor_doors:
+ door_item.listen_event(self._cb_outside_door, HABApp.core.events.ValueChangeEventFilter())
+ for phone_item in self._config.items.phones:
+ 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 = PresenceState.PRESENCE.value) -> str:
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ return correct state if it could be detected, if not return default value
+ """
+ phone_items = [phone for phone in self._config.items.phones if phone.value is not None] # phones with valid state (not None)
+ if phone_items:
+ if any(item.value == "ON" for item in phone_items):
+ return PresenceState.PRESENCE.value
+
+ if self._config.items.presence.value == "ON":
+ return PresenceState.LEAVING.value
+ return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value
+
+ if self._config.items.leaving.value == "ON":
+ return PresenceState.LEAVING.value
+
+ if self._config.items.presence.value == "ON":
+ return PresenceState.PRESENCE.value
+
+ if self._config.items.presence.value == "OFF":
+ return PresenceState.LONG_ABSENCE.value if self._item_state.value == PresenceState.LONG_ABSENCE.value else PresenceState.ABSENCE.value
+
+ return default_value
+
+ def _update_openhab_state(self) -> None:
+ """Extend _update_openhab state of base class to also update other OpenHAB items."""
+ super()._update_openhab_state()
+ self._instance_logger.info(f"Presence state changed to {self.state}")
+
+ # update presence item
+ target_value = "ON" if self.state in {PresenceState.PRESENCE.value, PresenceState.LEAVING.value} else "OFF"
+ habapp_rules.core.helper.send_if_different(self._config.items.presence, target_value)
+ habapp_rules.core.helper.send_if_different(self._config.items.leaving, "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.
+
+ Args:
+ event: state change event of door item
+ """
+ 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()
+
+ def _cb_leaving(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if leaving item changed state.
+
+ Args:
+ event: Item state change event of leaving item
+ """
+ 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 == PresenceState.LEAVING.value:
+ self._instance_logger.debug("Abort leaving through leaving switch")
+ self.abort_leaving()
+
+ def _cb_presence(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if presence item changed state.
+
+ Args:
+ event: Item state change event of presence item
+ """
+ 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 {PresenceState.PRESENCE.value, PresenceState.LEAVING.value}:
+ self._instance_logger.debug("Absence was set manually by presence switch")
+ self.absence_detected()
+
+ def _cb_phone(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if a phone state changed.
+
+ Args:
+ event: Item state change event of phone item
+ """
+ active_phones = len([phone for phone in self._config.items.phones if phone.value == "ON"])
+ if active_phones == 1 and event.value == "ON":
+ # first phone switched to ON
+ if self.__phone_absence_timer:
+ self.__phone_absence_timer.cancel()
+ self.__phone_absence_timer = None
+
+ if self.state == PresenceState.LEAVING.value:
+ self._instance_logger.debug("Leaving was aborted through first phone which came online")
+ self.abort_leaving()
+
+ 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()
+
+ elif active_phones == 0 and event.value == "OFF":
+ # last phone switched to OFF
+ self.__phone_absence_timer = threading.Timer(20 * 60, self.__set_leaving_through_phone)
+ self.__phone_absence_timer.start()
+
+ def __set_leaving_through_phone(self) -> None:
+ """Set leaving detected if timeout expired."""
+ 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:
+ """Stop all threads, started by this rule."""
+ 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
diff --git a/habapp_rules/system/sleep.py b/habapp_rules/system/sleep.py
index 1b8f1e2..f1a43fb 100644
--- a/habapp_rules/system/sleep.py
+++ b/habapp_rules/system/sleep.py
@@ -1,6 +1,8 @@
"""Rule to set/unset sleep state."""
+
import datetime
import logging
+import typing
import HABApp.openhab.definitions
import HABApp.openhab.events
@@ -11,17 +13,17 @@
import habapp_rules.core.logger
import habapp_rules.core.state_machine_rule
import habapp_rules.system.config.sleep
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
-# pylint: disable=no-member
class Sleep(habapp_rules.core.state_machine_rule.StateMachineRule):
- """Rules class to manage sleep state.
+ """Rules class to manage sleep state.
- Example OpenHAB configuration:
- # KNX-things:
- Thing device T00_99_OpenHab_Sleep "KNX OpenHAB Sleep"{
+ Example OpenHAB configuration:
+ # KNX-things:
+ Thing device T00_99_OpenHab_Sleep "KNX OpenHAB Sleep"{
Type switch : sleep "Sleep Request" [ ga="0/2/30"]
Type switch-control : sleep_RM "Sleep RM" [ ga="0/2/31"]
@@ -33,233 +35,241 @@ class Sleep(habapp_rules.core.state_machine_rule.StateMachineRule):
# Items:
Switch I01_02_Sleep "Sleep [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_RM"}
- Switch I01_02_Sleep_req "Sleep request" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep"}
- String I01_02_Sleep_text "Text for display [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_text"}
- Switch I01_02_Sleep_lock "Lock [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_lock_RM"}
- Switch I01_02_Sleep_lock_req "Lock request" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_lock"}
- String I01_02_Sleep_State "State [%s]"
-
- # Config:
- config = habapp_rules.system.config.sleep.SleepConfig(
- items=habapp_rules.system.config.sleep.SleepItems(
- sleep="I01_02_Sleep",
- sleep_req="I01_02_Sleep_req",
- state="I01_02_Sleep_State",
- lock="I01_02_Sleep_lock",
- lock_req="I01_02_Sleep_lock_req",
- display_text="I01_02_Sleep_text"
- )
- )
-
- # Rule init:
- habapp_rules.system.sleep.Sleep(config)
- """
-
- states = [
- {"name": "awake"},
- {"name": "pre_sleeping", "timeout": 3, "on_timeout": "pre_sleeping_timeout"},
- {"name": "sleeping"},
- {"name": "post_sleeping", "timeout": 3, "on_timeout": "post_sleeping_timeout"},
- {"name": "locked"},
- ]
-
- trans = [
- {"trigger": "start_sleeping", "source": "awake", "dest": "pre_sleeping"},
- {"trigger": "pre_sleeping_timeout", "source": "pre_sleeping", "dest": "sleeping"},
- {"trigger": "end_sleeping", "source": "sleeping", "dest": "post_sleeping"},
- {"trigger": "end_sleeping", "source": "pre_sleeping", "dest": "awake", "unless": "lock_request_active"},
- {"trigger": "end_sleeping", "source": "pre_sleeping", "dest": "locked", "conditions": "lock_request_active"},
- {"trigger": "post_sleeping_timeout", "source": "post_sleeping", "dest": "awake", "unless": "lock_request_active"},
- {"trigger": "post_sleeping_timeout", "source": "post_sleeping", "dest": "locked", "conditions": "lock_request_active"},
- {"trigger": "set_lock", "source": "awake", "dest": "locked"},
- {"trigger": "release_lock", "source": "locked", "dest": "awake"}
- ]
-
- def __init__(self, config: habapp_rules.system.config.sleep.SleepConfig) -> None:
- """Init of Sleep object.
-
- :param config: config for sleeping state
- """
- self._config = config
- habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, config.items.state)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.sleep.name)
-
- # init attributes
- self._sleep_request_active = config.items.sleep_request.is_on()
- self._lock_request_active = config.items.lock_request.is_on() if config.items.lock_request is not None else False
-
- # init state machine
- self.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(
- model=self,
- states=self.states,
- transitions=self.trans,
- ignore_invalid_triggers=True,
- after_state_change="_update_openhab_state")
- self._set_initial_state()
-
- self._update_openhab_state()
-
- # add callbacks
- config.items.sleep_request.listen_event(self._cb_sleep_request, HABApp.openhab.events.ItemStateChangedEventFilter())
- if config.items.lock_request is not None:
- config.items.lock_request.listen_event(self._cb_lock_request, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- self._instance_logger.debug(super().get_initial_log_message())
-
- def _get_initial_state(self, default_value: str = "awake") -> str:
- """Get initial state of state machine.
-
- :param default_value: default / initial state
- :return: return correct state if it could be detected, if not return default value
- """
- sleep_req = self._config.items.sleep_request.is_on() if self._config.items.sleep_request.value is not None else None
- lock_req = self._config.items.lock_request.is_on() if self._config.items.lock_request is not None and self._config.items.lock_request.value is not None else None
-
- if sleep_req:
- return "sleeping"
- if lock_req:
- return "locked"
- if sleep_req is False:
- return "awake"
-
- return default_value
-
- @property
- def sleep_request_active(self) -> bool:
- """Check if a sleep request is active
-
- :return: return true if lock request is active
- """
- return self._sleep_request_active
-
- @property
- def lock_request_active(self) -> bool:
- """Check if a lock request is active
-
- :return: return true if lock request is active
- """
- return self._lock_request_active
-
- def _update_openhab_state(self):
- """Extend _update_openhab state of base class to also update other OpenHAB items."""
- super()._update_openhab_state()
-
- # update sleep state
- if self.state in {"pre_sleeping", "sleeping"}:
- habapp_rules.core.helper.send_if_different(self._config.items.sleep, "ON")
- else:
- habapp_rules.core.helper.send_if_different(self._config.items.sleep, "OFF")
-
- # update lock state
- self.__update_lock_state()
-
- # update display text
- if self._config.items.display_text is not None:
- self._config.items.display_text.oh_send_command(self.__get_display_text())
-
- def __get_display_text(self) -> str:
- """Get Text for displays.
-
- :return: display text
- """
- if self.state == "awake":
- return "Schlafen"
- if self.state == "pre_sleeping":
- return "Guten Schlaf"
- if self.state == "sleeping":
- return "Aufstehen"
- if self.state == "post_sleeping":
- return "Guten Morgen"
- if self.state == "locked":
- return "Gesperrt"
- return ""
-
- def __update_lock_state(self):
- """Update the return lock state value of OpenHAB item."""
- if self._config.items.lock is not None:
- if self.state in {"pre_sleeping", "post_sleeping", "locked"}:
- habapp_rules.core.helper.send_if_different(self._config.items.lock, "ON")
- else:
- habapp_rules.core.helper.send_if_different(self._config.items.lock, "OFF")
-
- def _cb_sleep_request(self, event: HABApp.openhab.events.ItemStateChangedEvent):
- """Callback, which is called if sleep request item changed state.
-
- :param event: Item state change event of sleep_request item
- """
- if event.value == "ON" and self.state == "awake":
- self._instance_logger.debug("Start sleeping through sleep switch")
- self._sleep_request_active = True
- self.start_sleeping()
- elif event.value == "ON" and self.state == "locked":
- self._sleep_request_active = False
- self._config.items.sleep_request.oh_send_command("OFF")
- elif event.value == "OFF" and self.state in {"sleeping", "pre_sleeping"}:
- self._instance_logger.debug("End sleeping through sleep switch")
- self._sleep_request_active = True
- self.end_sleeping()
-
- def _cb_lock_request(self, event: HABApp.openhab.events.ItemStateChangedEvent):
- """Callback, which is called if lock request item changed state.
-
- :param event: Item state change event of sleep_request item
- """
- self._lock_request_active = event.value == "ON"
-
- if self.state == "awake" and event.value == "ON":
- self.set_lock()
- elif self.state == "locked" and event.value == "OFF":
- self.release_lock()
- else:
- self.__update_lock_state()
+ Switch I01_02_Sleep_req "Sleep request" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep"}
+ String I01_02_Sleep_text "Text for display [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_text"}
+ Switch I01_02_Sleep_lock "Lock [%s]" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_lock_RM"}
+ Switch I01_02_Sleep_lock_req "Lock request" {channel="knx:device:bridge:T00_99_OpenHab_Sleep:sleep_lock"}
+ String I01_02_Sleep_State "State [%s]"
+
+ # Config:
+ config = habapp_rules.system.config.sleep.SleepConfig(
+ items=habapp_rules.system.config.sleep.SleepItems(
+ sleep="I01_02_Sleep",
+ sleep_req="I01_02_Sleep_req",
+ state="I01_02_Sleep_State",
+ lock="I01_02_Sleep_lock",
+ lock_req="I01_02_Sleep_lock_req",
+ display_text="I01_02_Sleep_text"
+ )
+ )
+
+ # Rule init:
+ habapp_rules.system.sleep.Sleep(config)
+ """
+
+ states: typing.ClassVar = [
+ {"name": "awake"},
+ {"name": "pre_sleeping", "timeout": 3, "on_timeout": "pre_sleeping_timeout"},
+ {"name": "sleeping"},
+ {"name": "post_sleeping", "timeout": 3, "on_timeout": "post_sleeping_timeout"},
+ {"name": "locked"},
+ ]
+
+ trans: typing.ClassVar = [
+ {"trigger": "start_sleeping", "source": "awake", "dest": "pre_sleeping"},
+ {"trigger": "pre_sleeping_timeout", "source": "pre_sleeping", "dest": "sleeping"},
+ {"trigger": "end_sleeping", "source": "sleeping", "dest": "post_sleeping"},
+ {"trigger": "end_sleeping", "source": "pre_sleeping", "dest": "awake", "unless": "lock_request_active"},
+ {"trigger": "end_sleeping", "source": "pre_sleeping", "dest": "locked", "conditions": "lock_request_active"},
+ {"trigger": "post_sleeping_timeout", "source": "post_sleeping", "dest": "awake", "unless": "lock_request_active"},
+ {"trigger": "post_sleeping_timeout", "source": "post_sleeping", "dest": "locked", "conditions": "lock_request_active"},
+ {"trigger": "set_lock", "source": "awake", "dest": "locked"},
+ {"trigger": "release_lock", "source": "locked", "dest": "awake"},
+ ]
+
+ def __init__(self, config: habapp_rules.system.config.sleep.SleepConfig) -> None:
+ """Init of Sleep object.
+
+ Args:
+ config: config for sleeping state
+ """
+ self._config = config
+ habapp_rules.core.state_machine_rule.StateMachineRule.__init__(self, config.items.state)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.sleep.name)
+
+ # init attributes
+ self._sleep_request_active = config.items.sleep_request.is_on()
+ self._lock_request_active = config.items.lock_request.is_on() if config.items.lock_request is not None else False
+
+ # init state machine
+ self.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(model=self, states=self.states, transitions=self.trans, ignore_invalid_triggers=True, after_state_change="_update_openhab_state")
+ self._set_initial_state()
+
+ self._update_openhab_state()
+
+ # add callbacks
+ config.items.sleep_request.listen_event(self._cb_sleep_request, HABApp.openhab.events.ItemStateChangedEventFilter())
+ if config.items.lock_request is not None:
+ config.items.lock_request.listen_event(self._cb_lock_request, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ self._instance_logger.debug(super().get_initial_log_message())
+
+ def _get_initial_state(self, default_value: str = "awake") -> str:
+ """Get initial state of state machine.
+
+ Args:
+ default_value: default / initial state
+
+ Returns:
+ return correct state if it could be detected, if not return default value
+ """
+ sleep_req = self._config.items.sleep_request.is_on() if self._config.items.sleep_request.value is not None else None
+ lock_req = self._config.items.lock_request.is_on() if self._config.items.lock_request is not None and self._config.items.lock_request.value is not None else None
+
+ if sleep_req:
+ return "sleeping"
+ if lock_req:
+ return "locked"
+ if sleep_req is False:
+ return "awake"
+
+ return default_value
+
+ @property
+ def sleep_request_active(self) -> bool:
+ """Check if a sleep request is active.
+
+ Returns:
+ return true if lock request is active
+ """
+ return self._sleep_request_active
+
+ @property
+ def lock_request_active(self) -> bool:
+ """Check if a lock request is active.
+
+ Returns:
+ return true if lock request is active
+ """
+ return self._lock_request_active
+
+ def _update_openhab_state(self) -> None:
+ """Extend _update_openhab state of base class to also update other OpenHAB items."""
+ super()._update_openhab_state()
+
+ # update sleep state
+ if self.state in {"pre_sleeping", "sleeping"}:
+ habapp_rules.core.helper.send_if_different(self._config.items.sleep, "ON")
+ else:
+ habapp_rules.core.helper.send_if_different(self._config.items.sleep, "OFF")
+
+ # update lock state
+ self.__update_lock_state()
+
+ # update display text
+ if self._config.items.display_text is not None:
+ self._config.items.display_text.oh_send_command(self.__get_display_text())
+
+ def __get_display_text(self) -> str:
+ """Get Text for displays.
+
+ Returns:
+ display text
+ """
+ if self.state == "awake":
+ return "Schlafen"
+ if self.state == "pre_sleeping":
+ return "Guten Schlaf"
+ if self.state == "sleeping":
+ return "Aufstehen"
+ if self.state == "post_sleeping":
+ return "Guten Morgen"
+ if self.state == "locked":
+ return "Gesperrt"
+ return ""
+
+ def __update_lock_state(self) -> None:
+ """Update the return lock state value of OpenHAB item."""
+ if self._config.items.lock is not None:
+ if self.state in {"pre_sleeping", "post_sleeping", "locked"}:
+ habapp_rules.core.helper.send_if_different(self._config.items.lock, "ON")
+ else:
+ habapp_rules.core.helper.send_if_different(self._config.items.lock, "OFF")
+
+ def _cb_sleep_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if sleep request item changed state.
+
+ Args:
+ event: Item state change event of sleep_request item
+ """
+ if event.value == "ON" and self.state == "awake":
+ self._instance_logger.debug("Start sleeping through sleep switch")
+ self._sleep_request_active = True
+ self.start_sleeping()
+ elif event.value == "ON" and self.state == "locked":
+ self._sleep_request_active = False
+ self._config.items.sleep_request.oh_send_command("OFF")
+ elif event.value == "OFF" and self.state in {"sleeping", "pre_sleeping"}:
+ self._instance_logger.debug("End sleeping through sleep switch")
+ self._sleep_request_active = True
+ self.end_sleeping()
+
+ def _cb_lock_request(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback, which is called if lock request item changed state.
+
+ Args:
+ event: Item state change event of sleep_request item
+ """
+ self._lock_request_active = event.value == "ON"
+
+ if self.state == "awake" and event.value == "ON":
+ self.set_lock()
+ elif self.state == "locked" and event.value == "OFF":
+ self.release_lock()
+ else:
+ self.__update_lock_state()
class LinkSleep(HABApp.Rule):
- """Link sleep items depending on current time"""
-
- def __init__(self, config: habapp_rules.system.config.sleep.LinkSleepConfig) -> None:
- """Init rule.
-
- :param config: Config for linking sleep rules
- """
- self._config = config
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self.rule_name)
-
- config.items.sleep_master.listen_event(self._cb_master, HABApp.openhab.events.ItemStateChangedEventFilter())
-
- if config.items.link_active_feedback is not None:
- self.run.on_every_day(config.parameter.link_time_start, self._set_link_active_feedback, target_state="ON")
- self.run.on_every_day(config.parameter.link_time_end, self._set_link_active_feedback, target_state="OFF")
- self.run.soon(self._set_link_active_feedback, target_state=self._check_time_in_window())
-
- def _check_time_in_window(self) -> bool:
- """Check if current time is in the active time window
-
- :return: True if current time is in time the active time window
- """
- now = datetime.datetime.now().time()
-
- if self._config.parameter.link_time_start <= self._config.parameter.link_time_end:
- return self._config.parameter.link_time_start <= now <= self._config.parameter.link_time_end
- # cross midnight
- return self._config.parameter.link_time_start <= now or now <= self._config.parameter.link_time_end
-
- def _cb_master(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
- """Callback which is triggered if the state of the master item changes
-
- :param event: state change event
- """
- if not self._check_time_in_window():
- return
-
- self._instance_logger.debug(f"Set request of all linked sleep states of {self._config.items.sleep_master.name}")
- for itm in self._config.items.sleep_request_slaves:
- habapp_rules.core.helper.send_if_different(itm, event.value)
-
- def _set_link_active_feedback(self, target_state: str) -> None:
- """Set feedback for link is active.
-
- :param target_state: Target state which should be set ["ON" / "OFF"]
- """
- self._config.items.link_active_feedback.oh_send_command(target_state)
+ """Link sleep items depending on current time."""
+
+ def __init__(self, config: habapp_rules.system.config.sleep.LinkSleepConfig) -> None:
+ """Init rule.
+
+ Args:
+ config: Config for linking sleep rules
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, self.rule_name)
+
+ config.items.sleep_master.listen_event(self._cb_master, HABApp.openhab.events.ItemStateChangedEventFilter())
+
+ if config.items.link_active_feedback is not None:
+ self.run.at(self.run.trigger.time(config.parameter.link_time_start), self._set_link_active_feedback, target_state="ON")
+ self.run.at(self.run.trigger.time(config.parameter.link_time_end), self._set_link_active_feedback, target_state="OFF")
+ self.run.soon(self._set_link_active_feedback, target_state=self._check_time_in_window())
+
+ def _check_time_in_window(self) -> bool:
+ """Check if current time is in the active time window.
+
+ Returns:
+ True if current time is in time the active time window
+ """
+ now = datetime.datetime.now(tz=TIMEZONE).time()
+
+ if self._config.parameter.link_time_start <= self._config.parameter.link_time_end:
+ return self._config.parameter.link_time_start <= now <= self._config.parameter.link_time_end
+ # cross midnight
+ return self._config.parameter.link_time_start <= now or now <= self._config.parameter.link_time_end
+
+ def _cb_master(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
+ """Callback which is triggered if the state of the master item changes.
+
+ Args:
+ event: state change event
+ """
+ if not self._check_time_in_window():
+ return
+
+ self._instance_logger.debug(f"Set request of all linked sleep states of {self._config.items.sleep_master.name}")
+ for itm in self._config.items.sleep_request_slaves:
+ habapp_rules.core.helper.send_if_different(itm, event.value)
+
+ def _set_link_active_feedback(self, target_state: str) -> None:
+ """Set feedback for link is active.
+
+ Args:
+ target_state: Target state which should be set ["ON" / "OFF"]
+ """
+ self._config.items.link_active_feedback.oh_send_command(target_state)
diff --git a/habapp_rules/system/summer_winter.py b/habapp_rules/system/summer_winter.py
index b72bbbe..347015c 100644
--- a/habapp_rules/system/summer_winter.py
+++ b/habapp_rules/system/summer_winter.py
@@ -1,4 +1,5 @@
"""Rule to detect whether it is summer or winter."""
+
import datetime
import logging
import statistics
@@ -8,96 +9,109 @@
import habapp_rules.common.hysteresis
import habapp_rules.core.logger
import habapp_rules.system.config.summer_winter
+from habapp_rules import TIMEZONE
LOGGER = logging.getLogger(__name__)
-class SummerWinterException(Exception):
- """Custom Exception for SummerWinter"""
+class SummerWinterError(Exception):
+ """Custom Exception for SummerWinter."""
class SummerWinter(HABApp.Rule):
- """Rule check if it is summer or winter."""
-
- def __init__(self, config: habapp_rules.system.config.summer_winter.SummerWinterConfig) -> None:
- """Init rule to update summer/winter item.
-
- :param config: Config for summer/winter detection
- """
- self._config = config
- HABApp.Rule.__init__(self)
- self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.summer.name)
-
- # set class variables
- self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(config.parameter.temperature_threshold, 0.5)
- self.__now = datetime.datetime.now()
-
- # run at init and every day at 23:00
- self.run.soon(self._cb_update_summer)
- self.run.on_every_day(datetime.time(23), self._cb_update_summer)
-
- LOGGER.debug("Init of Summer / Winter successful")
-
- def __get_weighted_mean(self, days_in_past: int) -> float:
- """Get weighted mean temperature.
-
- The weighted mean temperature will be calculated according the following formula: (T07 + T14 + T22 + T22) / 4 where T07 is the temperature at 7:00 (and so on)
- It is possible to get the weighted temperature of today or of some days in the past -> defined by the days_in past attribute. If this method is called before 22:00 there will be an offset of one day.
- :param days_in_past: if days in past is set to 0 it will return the mean of today. 1 will return the offset of yesterday
- :return: the weighted mean according the formula in doc-string
- :raises SummerWinterException: if there is not enough data for at least one evaluated hour.
- """
- day_offset = 0
- if self.__now.hour < 23:
- day_offset = 1
-
- temperature_values = []
- for hour in [7, 14, 22]:
- start_time = datetime.datetime(self.__now.year, self.__now.month, self.__now.day, hour, 0) - datetime.timedelta(days=day_offset + days_in_past)
- end_time = start_time + datetime.timedelta(hours=1)
- persistence_data = self._config.items.outside_temperature.get_persistence_data(persistence=self._config.parameter.persistence_service, start_time=start_time, end_time=end_time)
- if not persistence_data.data:
- raise SummerWinterException(f"No data for {start_time}")
- temperature_values.append(next(iter(persistence_data.data.values())))
- return (sum(temperature_values) + temperature_values[2]) / 4
-
- def __is_summer(self) -> bool:
- """Check if it is summer (or winter).
-
- :return: Returns True if it is summer.
- :raises SummerWinterException: if summer/winter could not be detected
- """
- self.__now = datetime.datetime.now()
- values = []
- for day in range(self._config.parameter.days):
- try:
- values.append(self.__get_weighted_mean(day))
- except SummerWinterException:
- self._instance_logger.warning(f"Could not get mean value of day -{day}")
-
- if not values:
- raise SummerWinterException("Not enough data to detect summer/winter")
-
- is_summer = self._hysteresis_switch.get_output(mean_value := statistics.mean(values))
- self._instance_logger.debug(f"Check Summer/Winter. values = {values} | mean = {mean_value} | summer = {is_summer}")
- return is_summer
-
- def _cb_update_summer(self) -> None:
- """Callback to update the summer item."""
- try:
- is_summer = self.__is_summer()
- except SummerWinterException:
- self._instance_logger.error("Could not get summer / winter")
- return
-
- # get target state of summer
- target_value = "ON" if is_summer else "OFF"
-
- # send state
- if self._config.items.summer.value != target_value:
- self._instance_logger.info(f"Summer changed to {target_value}")
- self._config.items.summer.oh_send_command(target_value)
-
- # update last update item at every call
- if self._config.items.last_check is not None:
- self._config.items.last_check.oh_send_command(datetime.datetime.now())
+ """Rule check if it is summer or winter."""
+
+ def __init__(self, config: habapp_rules.system.config.summer_winter.SummerWinterConfig) -> None:
+ """Init rule to update summer/winter item.
+
+ Args:
+ config: Config for summer/winter detection
+ """
+ self._config = config
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.summer.name)
+
+ # set class variables
+ self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(config.parameter.temperature_threshold, 0.5)
+ self.__now = datetime.datetime.now(tz=TIMEZONE)
+
+ # run at init and every day at 23:00
+ self.run.soon(self._cb_update_summer)
+ self.run.at(self.run.trigger.time("23:00:00"), self._cb_update_summer)
+
+ LOGGER.debug("Init of Summer / Winter successful")
+
+ def __get_weighted_mean(self, days_in_past: int) -> float:
+ """Get weighted mean temperature.
+
+ The weighted mean temperature will be calculated according the following formula: (T07 + T14 + T22 + T22) / 4 where T07 is the temperature at 7:00 (and so on)
+ It is possible to get the weighted temperature of today or of some days in the past -> defined by the days_in past attribute. If this method is called before 22:00 there will be an offset of one day.
+
+ Args:
+ days_in_past: if days in past is set to 0 it will return the mean of today. 1 will return the offset of yesterday
+
+ Returns:
+ the weighted mean according the formula in doc-string
+
+ Raises:
+ SummerWinterError: if there is not enough data for at least one evaluated hour.
+ """
+ day_offset = 0
+ if self.__now.hour < 23: # noqa: PLR2004
+ day_offset = 1
+
+ temperature_values = []
+ for hour in [7, 14, 22]:
+ start_time = datetime.datetime(self.__now.year, self.__now.month, self.__now.day, hour, 0) - datetime.timedelta(days=day_offset + days_in_past) # noqa: DTZ001
+ end_time = start_time + datetime.timedelta(hours=1)
+ persistence_data = self._config.items.outside_temperature.get_persistence_data(persistence=self._config.parameter.persistence_service, start_time=start_time, end_time=end_time)
+ if not persistence_data.data:
+ msg = f"No data for {start_time}"
+ raise SummerWinterError(msg)
+ temperature_values.append(next(iter(persistence_data.data.values())))
+ return (sum(temperature_values) + temperature_values[2]) / 4
+
+ def __is_summer(self) -> bool:
+ """Check if it is summer (or winter).
+
+ Returns:
+ Returns True if it is summer.
+
+ Raises:
+ SummerWinterError: if summer/winter could not be detected
+ """
+ self.__now = datetime.datetime.now(tz=TIMEZONE)
+ values = []
+ for day in range(self._config.parameter.days):
+ try:
+ values.append(self.__get_weighted_mean(day))
+ except SummerWinterError: # noqa: PERF203
+ self._instance_logger.warning(f"Could not get mean value of day -{day}")
+
+ if not values:
+ msg = "Not enough data to detect summer/winter"
+ raise SummerWinterError(msg)
+
+ is_summer = self._hysteresis_switch.get_output(mean_value := statistics.mean(values))
+ self._instance_logger.debug(f"Check Summer/Winter. values = {values} | mean = {mean_value} | summer = {is_summer}")
+ return is_summer
+
+ def _cb_update_summer(self) -> None:
+ """Callback to update the summer item."""
+ try:
+ is_summer = self.__is_summer()
+ except SummerWinterError:
+ self._instance_logger.exception("Could not get summer / winter")
+ return
+
+ # get target state of summer
+ target_value = "ON" if is_summer else "OFF"
+
+ # send state
+ if self._config.items.summer.value != target_value:
+ self._instance_logger.info(f"Summer changed to {target_value}")
+ self._config.items.summer.oh_send_command(target_value)
+
+ # update last update item at every call
+ if self._config.items.last_check is not None:
+ self._config.items.last_check.oh_send_command(datetime.datetime.now(tz=TIMEZONE))
diff --git a/habapp_rules/system/watchdog.py b/habapp_rules/system/watchdog.py
deleted file mode 100644
index f5d61bb..0000000
--- a/habapp_rules/system/watchdog.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Watchdog rules."""
-import HABApp
-
-import habapp_rules.core.helper
-from habapp_rules.system.config.watchdog import WatchdogConfig
-
-
-class Watchdog(HABApp.Rule):
- """Watchdog rule to check if the observed item was updated in time.
-
- # Items:
- Switch Item_To_Observe "Item which should be observed"
- Switch Warning "Warning, item was not updated in time"
-
- # Config:
- config = habapp_rules.system.config.WatchdogConfig(
- items=habapp_rules.system.config.WatchdogItems(
- observed="Item_To_Observe",
- warning="Warning")
- )
-
- # Rule init:
- habapp_rules.system.watchdog.Watchdog(config)
- """
-
- def __init__(self, config: WatchdogConfig):
- """Init watchdog rule.
-
- :param config: Config for watchdog rule
- """
- HABApp.Rule.__init__(self)
- self._config = config
-
- self._countdown = self.run.countdown(self._config.parameter.timeout, habapp_rules.core.helper.send_if_different, item=self._config.items.warning, value="ON")
- self._countdown.reset()
- self._config.items.observed.listen_event(self._cb_observed_state_updated, HABApp.openhab.events.ItemStateUpdatedEventFilter())
-
- def _cb_observed_state_updated(self, event: HABApp.openhab.events.ItemStateUpdatedEvent) -> None:
- """Callback which is called if the observed item was updated.
-
- :param event: event which triggered this callback
- """
- habapp_rules.core.helper.send_if_different(self._config.items.warning, "OFF")
- self._countdown.reset()
diff --git a/noxfile.py b/noxfile.py
deleted file mode 100644
index 14c9d43..0000000
--- a/noxfile.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import pathlib
-import subprocess
-import sys
-
-try:
- import nose_helper.nox_checks.common
-except ImportError:
- with (pathlib.Path.cwd() / "requirements_dev.txt").open() as req_file:
- nose_pkg = next((pkg for pkg in req_file.read().split("\n") if pkg.startswith("nose_helper")), None)
- if not nose_pkg:
- raise Exception("nose_helper package is missing in requirements_dev.txt")
- subprocess.check_call([sys.executable, "-m", "pip", "install", nose_pkg])
- import nose_helper.nox_checks.common
-
-import nox
-
-PYTHON_VERSION = ["3.11"]
-nox.options.sessions = ["version_check", "coverage", "pylint"]
-
-
-class Nox(nose_helper.nox_checks.common.NoxBase):
-
- def __init__(self, session: nox.Session):
- nose_helper.nox_checks.common.NoxBase.__init__(self, session, project_name="habapp_rules", changelog_path=pathlib.Path().resolve() / "changelog.md")
-
-
-@nox.session(python=PYTHON_VERSION)
-def coverage(session):
- """Run coverage"""
- Nox(session).coverage()
-
-
-@nox.session(python=PYTHON_VERSION)
-def pylint(session):
- """Run pylint."""
- Nox(session).pylint()
-
-
-@nox.session(python=PYTHON_VERSION)
-def version_check(session):
- """Check if version was updated."""
- Nox(session).version_check()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c100d48
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,149 @@
+[build-system]
+requires = ["hatchling", "hatch-requirements-txt"]
+build-backend = "hatchling.build"
+
+[project]
+name = "habapp_rules"
+description = "Basic rules for HABApp"
+long_description = "Basic rules for HABApp"
+authors = [{ name = "Seuling N." }]
+license = "Apache-2.0"
+dynamic = ["dependencies", "optional-dependencies", "version"]
+requires-python = ">=3.10"
+
+
+[tool.coverage]
+branch = true
+concurrency = "thread"
+context = "(empty)"
+disable_warnings = ["module-not-imported"]
+dynamic_context = "test_function"
+source = ["habapp_rules", "tests"]
+omit = ["*oh_item.py", "*rule_runner.py", "*run_unittest.py", "*__version__.py"]
+
+
+[tool.coverage.exclude_lines]
+pragma = "no cover"
+if_main = "if __name__ == \"__main__\":"
+if_typing = "if typing.TYPE_CHECKING:"
+typing_overload = "@(typing.)?overload"
+
+[tool.coverage.report]
+fail_under = 100
+skip_covered = true
+omit = ["*/helper/graph_machines.py", "*oh_item.py", "*rule_runner.py"]
+
+
+[tool.hatch.build.targets.wheel]
+include = ["habapp_rules"]
+
+[tool.hatch.envs.lint]
+detached = true
+dependencies = ["pre-commit"]
+[tool.hatch.envs.lint.scripts]
+check = "pre-commit run --all-files"
+
+[tool.hatch.envs.publish]
+detached = true
+
+[tool.hatch.envs.tests]
+dependencies = ["coverage", "pytest", "graphviz"]
+
+
+[tool.hatch.envs.tests.scripts]
+local = "coverage run tests/run_unittest.py && coverage html --skip-covered --fail-under=100"
+
+
+
+[tool.hatch.metadata.hooks.requirements_txt]
+files = ["requirements.txt"]
+
+[tool.hatch.metadata] # remove when HABApp dev is released
+allow-direct-references = true
+
+[tool.hatch.version]
+path = "habapp_rules/__init__.py"
+
+#[tool.mypy]
+#python_version = 3.12
+#files = "habapp_rules"
+#plugins = "pydantic.mypy"
+#exclude = "tests"
+#check_untyped_defs = true
+#disallow_any_generics = true
+#disallow_incomplete_defs = true
+#disallow_untyped_calls = true
+#extra_checks = true
+#follow_imports = "silent"
+#local_partial_types = true
+#no_implicit_reexport = true
+#show_error_codes = true
+#strict_equality = true
+#warn_redundant_casts = true
+#warn_return_any = true
+#warn_unused_configs = true
+#warn_unused_ignores = true
+
+[[tool.mypy.overrides]]
+module = ["pydantic.*"]
+ignore_missing_imports = true
+no_implicit_reexport = false
+
+[tool.ruff]
+indent-width = 4
+line-length = 250
+preview = true
+exclude=["*rule_runner.py"]
+
+[tool.ruff.format]
+docstring-code-format = true
+docstring-code-line-length = "dynamic"
+indent-style = "space"
+line-ending = "auto"
+quote-style = "double"
+skip-magic-trailing-comma = false
+
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = [
+ "COM812", # Missing trailing commas
+ "CPY", # Missing copyright notice
+ "D100", # Missing docstring in public module
+ "D104", # Missing docstring in public package
+ "D203", # 1 blank line required before class docstring
+ "D213", # Multi-line docstring summary should start at the second line
+ "DOC502", # Raised exception is not explicitly raised
+ "E501", # Line too long
+ "FBT001", # Boolean-typed positional argument in function definition
+ "FBT002", # Boolean default positional argument in function definition
+ "G004", # Logging statement uses f-string
+ "ISC001", # Implicitly concatenated string literals on one line
+ "PD", # Pandas
+ "PLR0911", # Too many return statements
+ "PLR0913", # Too many arguments in function definition
+ "PLR0917", # Too many positional arguments
+]
+
+[tool.ruff.lint.isort]
+split-on-trailing-comma = true
+
+[tool.ruff.lint.per-file-ignores]
+# Ignore certain rules in the 'tests/' directory
+"tests/*" = [
+ "DTZ001", # datetime.datetime() called without a tzinfo argument
+ "DTZ002", # datetime.datetime.today() used
+ "DTZ005", # datetime.datetime.now() called without a tz argument
+ "FBT003", # Boolean-typed keyword argument in function call
+ "PLC2701", # Private name import
+ "PLR0915", # Too many statements
+ "PLR2004", # Magic value used in comparison
+ "PLR0904", # Too many public methods
+ "PLR6301", # Method could be a function, class method, or static method
+ "PT", # pytest style -> this pkg is using unittest
+ "PYI024", # Use typing.NamedTuple instead of collections.namedtuple
+ "S101", # Use of assert detected
+ "SLF001", # Private member accessed
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
diff --git a/requirements.txt b/requirements.txt
index 1037d35..f9dffeb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
-HABApp==24.08.1
+HABApp==24.11.0
matplotlib~=3.9.0
multi-notifier~=0.5.0
transitions==0.9.1 # 0.9.2 creates strange graphs
-holidays==0.53
\ No newline at end of file
+typing_extensions~=4.12.2 # used for py3.10 support (SelfType)
+pytz~=2024.2
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 65d8163..fb04622 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,2 +1,6 @@
-graphviz~=0.20
-nose_helper~=1.5.0
\ No newline at end of file
+graphviz~=0.20.3
+hatch-requirements-txt~=0.4.0
+hatch~=1.13.0
+pre-commit~=4.0.0
+pytest~=8.3.3
+ruff~=0.7.0
diff --git a/setup.py b/setup.py
deleted file mode 100644
index cb91b0c..0000000
--- a/setup.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import typing
-
-import setuptools
-
-import habapp_rules.__version__
-
-
-def load_req() -> typing.List[str]:
- with open('requirements.txt', encoding="utf-8") as f:
- return f.readlines()
-
-
-VERSION = habapp_rules.__version__.__version__
-
-setuptools.setup(
- name="habapp_rules",
- version=VERSION,
- author="Seuling N.",
- description="Basic rules for HABApp",
- long_description="Basic rules for HABApp",
- packages=setuptools.find_packages(exclude=["tests*", "rules*"]),
- install_requires=load_req(),
- python_requires=">=3.11",
- license="Apache License 2.0",
- package_data={'': ['*.html']})
diff --git a/tests/.coveragerc b/tests/.coveragerc
deleted file mode 100644
index 4fd8968..0000000
--- a/tests/.coveragerc
+++ /dev/null
@@ -1,33 +0,0 @@
-# .coveragerc to control coverage.py
-[run]
-branch = True
-concurrency = thread
-context = (empty)
-disable_warnings = module-not-imported
-dynamic_context = test_function
-source =
- habapp_rules
- tests
-omit =
- *oh_item.py
- *rule_runner.py
- *run_unittest.py
- *__version__.py
-
-[report]
-omit =
- */helper/graph_machines.py
-
-# Regexes for lines to exclude from consideration
-exclude_lines =
- # Have to re-enable the standard pragma
- pragma: no cover
-
- # Don't complain if non-runnable code isn't run:
- if __name__ == "__main__":
-
- # Don't complain about code running only while type checking:
- if typing.TYPE_CHECKING:
-
- # Don't complain about typing overloads as they have no runtime meaning
- @(typing.)?overload
\ No newline at end of file
diff --git a/tests/actors/Shading_States/Shading_Auto_SleepingClose.png b/tests/actors/Shading_States/Shading_Auto_SleepingClose.png
deleted file mode 100644
index ae3bbe1..0000000
Binary files a/tests/actors/Shading_States/Shading_Auto_SleepingClose.png and /dev/null differ
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation.png b/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation.png
deleted file mode 100644
index 31ce69b..0000000
Binary files a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation.png and /dev/null differ
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerHand.png b/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerHand.png
deleted file mode 100644
index 81ba8c7..0000000
Binary files a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerHand.png and /dev/null differ
diff --git a/tests/actors/Ventilation_States/Ventilation.png b/tests/actors/Ventilation_States/Ventilation.png
deleted file mode 100644
index 2c91259..0000000
Binary files a/tests/actors/Ventilation_States/Ventilation.png and /dev/null differ
diff --git a/tests/actors/Ventilation_States/Ventilation_Auto_PowerHand.png b/tests/actors/Ventilation_States/Ventilation_Auto_PowerHand.png
deleted file mode 100644
index 30d7e3a..0000000
Binary files a/tests/actors/Ventilation_States/Ventilation_Auto_PowerHand.png and /dev/null differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch.png
new file mode 100644
index 0000000..b8db6cd
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch.png differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_Off.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_Off.png
new file mode 100644
index 0000000..5a2a131
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_Off.png differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_On.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_On.png
new file mode 100644
index 0000000..6c2d5fb
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_On.png differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_WaitCurrent.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_WaitCurrent.png
new file mode 100644
index 0000000..1f9c496
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Auto_WaitCurrent.png differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Hand.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Hand.png
new file mode 100644
index 0000000..5b239bc
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Hand.png differ
diff --git a/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Manual.png b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Manual.png
new file mode 100644
index 0000000..2512425
Binary files /dev/null and b/tests/actors/_state_charts/EnergySaveSwitch/EnergySaveSwitch_Manual.png differ
diff --git a/tests/actors/Light_States/Light.png b/tests/actors/_state_charts/Light/Light.png
similarity index 100%
rename from tests/actors/Light_States/Light.png
rename to tests/actors/_state_charts/Light/Light.png
diff --git a/tests/actors/Light_States/Light_auto_leaving.png b/tests/actors/_state_charts/Light/Light_auto_leaving.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_leaving.png
rename to tests/actors/_state_charts/Light/Light_auto_leaving.png
diff --git a/tests/actors/Light_States/Light_auto_off.png b/tests/actors/_state_charts/Light/Light_auto_off.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_off.png
rename to tests/actors/_state_charts/Light/Light_auto_off.png
diff --git a/tests/actors/Light_States/Light_auto_on.png b/tests/actors/_state_charts/Light/Light_auto_on.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_on.png
rename to tests/actors/_state_charts/Light/Light_auto_on.png
diff --git a/tests/actors/Light_States/Light_auto_preoff.png b/tests/actors/_state_charts/Light/Light_auto_preoff.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_preoff.png
rename to tests/actors/_state_charts/Light/Light_auto_preoff.png
diff --git a/tests/actors/Light_States/Light_auto_presleep.png b/tests/actors/_state_charts/Light/Light_auto_presleep.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_presleep.png
rename to tests/actors/_state_charts/Light/Light_auto_presleep.png
diff --git a/tests/actors/Light_States/Light_auto_restoreState.png b/tests/actors/_state_charts/Light/Light_auto_restoreState.png
similarity index 100%
rename from tests/actors/Light_States/Light_auto_restoreState.png
rename to tests/actors/_state_charts/Light/Light_auto_restoreState.png
diff --git a/tests/actors/Light_States/Light_manual.png b/tests/actors/_state_charts/Light/Light_manual.png
similarity index 100%
rename from tests/actors/Light_States/Light_manual.png
rename to tests/actors/_state_charts/Light/Light_manual.png
diff --git a/tests/actors/LightExtended_States/LightExtended.png b/tests/actors/_state_charts/LightExtended/LightExtended.png
similarity index 100%
rename from tests/actors/LightExtended_States/LightExtended.png
rename to tests/actors/_state_charts/LightExtended/LightExtended.png
diff --git a/tests/actors/LightExtended_States/LightExtended_auto_door.png b/tests/actors/_state_charts/LightExtended/LightExtended_auto_door.png
similarity index 100%
rename from tests/actors/LightExtended_States/LightExtended_auto_door.png
rename to tests/actors/_state_charts/LightExtended/LightExtended_auto_door.png
diff --git a/tests/actors/LightExtended_States/LightExtended_auto_leaving.png b/tests/actors/_state_charts/LightExtended/LightExtended_auto_leaving.png
similarity index 100%
rename from tests/actors/LightExtended_States/LightExtended_auto_leaving.png
rename to tests/actors/_state_charts/LightExtended/LightExtended_auto_leaving.png
diff --git a/tests/actors/LightExtended_States/LightExtended_auto_motion.png b/tests/actors/_state_charts/LightExtended/LightExtended_auto_motion.png
similarity index 100%
rename from tests/actors/LightExtended_States/LightExtended_auto_motion.png
rename to tests/actors/_state_charts/LightExtended/LightExtended_auto_motion.png
diff --git a/tests/actors/Light_HCL_States/HCL_Base.png b/tests/actors/_state_charts/Light_HCL/HCL_Base.png
similarity index 100%
rename from tests/actors/Light_HCL_States/HCL_Base.png
rename to tests/actors/_state_charts/Light_HCL/HCL_Base.png
diff --git a/tests/actors/Shading_States/Shading.png b/tests/actors/_state_charts/Shading/Shading.png
similarity index 100%
rename from tests/actors/Shading_States/Shading.png
rename to tests/actors/_state_charts/Shading/Shading.png
diff --git a/tests/actors/Shading_States/Shading_Auto_Init.png b/tests/actors/_state_charts/Shading/Shading_Auto_Init.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Auto_Init.png
rename to tests/actors/_state_charts/Shading/Shading_Auto_Init.png
diff --git a/tests/actors/Shading_States/Shading_Auto_NightClose.png b/tests/actors/_state_charts/Shading/Shading_Auto_NightClose.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Auto_NightClose.png
rename to tests/actors/_state_charts/Shading/Shading_Auto_NightClose.png
diff --git a/tests/actors/Shading_States/Shading_Auto_Open.png b/tests/actors/_state_charts/Shading/Shading_Auto_Open.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Auto_Open.png
rename to tests/actors/_state_charts/Shading/Shading_Auto_Open.png
diff --git a/tests/actors/_state_charts/Shading/Shading_Auto_SleepingClose.png b/tests/actors/_state_charts/Shading/Shading_Auto_SleepingClose.png
new file mode 100644
index 0000000..98a933e
Binary files /dev/null and b/tests/actors/_state_charts/Shading/Shading_Auto_SleepingClose.png differ
diff --git a/tests/actors/Shading_States/Shading_Auto_SunProtection.png b/tests/actors/_state_charts/Shading/Shading_Auto_SunProtection.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Auto_SunProtection.png
rename to tests/actors/_state_charts/Shading/Shading_Auto_SunProtection.png
diff --git a/tests/actors/Shading_States/Shading_DoorOpen_Open.png b/tests/actors/_state_charts/Shading/Shading_DoorOpen_Open.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_DoorOpen_Open.png
rename to tests/actors/_state_charts/Shading/Shading_DoorOpen_Open.png
diff --git a/tests/actors/Shading_States/Shading_DoorOpen_PostOpen.png b/tests/actors/_state_charts/Shading/Shading_DoorOpen_PostOpen.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_DoorOpen_PostOpen.png
rename to tests/actors/_state_charts/Shading/Shading_DoorOpen_PostOpen.png
diff --git a/tests/actors/Shading_States/Shading_Hand.png b/tests/actors/_state_charts/Shading/Shading_Hand.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Hand.png
rename to tests/actors/_state_charts/Shading/Shading_Hand.png
diff --git a/tests/actors/Shading_States/Shading_Manual.png b/tests/actors/_state_charts/Shading/Shading_Manual.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_Manual.png
rename to tests/actors/_state_charts/Shading/Shading_Manual.png
diff --git a/tests/actors/Shading_States/Shading_WindAlarm.png b/tests/actors/_state_charts/Shading/Shading_WindAlarm.png
similarity index 100%
rename from tests/actors/Shading_States/Shading_WindAlarm.png
rename to tests/actors/_state_charts/Shading/Shading_WindAlarm.png
diff --git a/tests/actors/_state_charts/Ventilation/Ventilation.png b/tests/actors/_state_charts/Ventilation/Ventilation.png
new file mode 100644
index 0000000..9e30409
Binary files /dev/null and b/tests/actors/_state_charts/Ventilation/Ventilation.png differ
diff --git a/tests/actors/Ventilation_States/Ventilation_Auto_Normal.png b/tests/actors/_state_charts/Ventilation/Ventilation_Auto_Normal.png
similarity index 100%
rename from tests/actors/Ventilation_States/Ventilation_Auto_Normal.png
rename to tests/actors/_state_charts/Ventilation/Ventilation_Auto_Normal.png
diff --git a/tests/actors/Ventilation_States/Ventilation_Auto_PowerExternal.png b/tests/actors/_state_charts/Ventilation/Ventilation_Auto_PowerExternal.png
similarity index 100%
rename from tests/actors/Ventilation_States/Ventilation_Auto_PowerExternal.png
rename to tests/actors/_state_charts/Ventilation/Ventilation_Auto_PowerExternal.png
diff --git a/tests/actors/_state_charts/Ventilation/Ventilation_Auto_PowerHand.png b/tests/actors/_state_charts/Ventilation/Ventilation_Auto_PowerHand.png
new file mode 100644
index 0000000..3ac150d
Binary files /dev/null and b/tests/actors/_state_charts/Ventilation/Ventilation_Auto_PowerHand.png differ
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_LongAbsence_Off.png b/tests/actors/_state_charts/Ventilation/Ventilation_LongAbsence_Off.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_LongAbsence_Off.png
rename to tests/actors/_state_charts/Ventilation/Ventilation_LongAbsence_Off.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_LongAbsence_On.png b/tests/actors/_state_charts/Ventilation/Ventilation_LongAbsence_On.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_LongAbsence_On.png
rename to tests/actors/_state_charts/Ventilation/Ventilation_LongAbsence_On.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Manual.png b/tests/actors/_state_charts/Ventilation/Ventilation_Manual.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Manual.png
rename to tests/actors/_state_charts/Ventilation/Ventilation_Manual.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_Normal.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_Normal.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_Normal.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_Normal.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerAfterRun.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerAfterRun.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerAfterRun.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerAfterRun.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerExternal.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerExternal.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_Auto_PowerExternal.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerExternal.png
diff --git a/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerHand.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerHand.png
new file mode 100644
index 0000000..7ba2234
Binary files /dev/null and b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Auto_PowerHand.png differ
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_LongAbsence_Off.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_LongAbsence_Off.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_LongAbsence_Off.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_LongAbsence_Off.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_LongAbsence_On.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_LongAbsence_On.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_LongAbsence_On.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_LongAbsence_On.png
diff --git a/tests/actors/VentilationHeliosTwoStage_States/Ventilation_Manual.png b/tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Manual.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStage_States/Ventilation_Manual.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStage/Ventilation_Manual.png
diff --git a/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation.png
new file mode 100644
index 0000000..58e807c
Binary files /dev/null and b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation.png differ
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_Normal.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_Normal.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_Normal.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_Normal.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerAfterRun.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerAfterRun.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerAfterRun.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerAfterRun.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerExternal.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerExternal.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerExternal.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerExternal.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerHand.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerHand.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerHand.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerHand.png
diff --git a/tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerHumidity.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerHumidity.png
similarity index 100%
rename from tests/actors/VentilationHeliosTwoStageHumidity_States/Ventilation_Auto_PowerHumidity.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Auto_PowerHumidity.png
diff --git a/tests/actors/Ventilation_States/Ventilation_LongAbsence_Off.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_LongAbsence_Off.png
similarity index 100%
rename from tests/actors/Ventilation_States/Ventilation_LongAbsence_Off.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_LongAbsence_Off.png
diff --git a/tests/actors/Ventilation_States/Ventilation_LongAbsence_On.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_LongAbsence_On.png
similarity index 100%
rename from tests/actors/Ventilation_States/Ventilation_LongAbsence_On.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_LongAbsence_On.png
diff --git a/tests/actors/Ventilation_States/Ventilation_Manual.png b/tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Manual.png
similarity index 100%
rename from tests/actors/Ventilation_States/Ventilation_Manual.png
rename to tests/actors/_state_charts/VentilationHeliosTwoStageHumidity/Ventilation_Manual.png
diff --git a/tests/actors/config/irrigation.py b/tests/actors/config/irrigation.py
index 12bc26f..3ecdeb2 100644
--- a/tests/actors/config/irrigation.py
+++ b/tests/actors/config/irrigation.py
@@ -1,4 +1,5 @@
"""Test config models of irrigation rules."""
+
import HABApp
import pydantic
@@ -8,40 +9,26 @@
class TestIrrigationConfig(tests.helper.test_case_base.TestCaseBase):
- """Test IrrigationConfig class"""
+ """Test IrrigationConfig class."""
- def test_model_validation(self):
- """Test model validation."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_valve", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_active", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_hour", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_minute", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_duration", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_repetitions", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_brake", None)
+ def test_model_validation(self) -> None:
+ """Test model validation."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_valve", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_active", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_hour", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_minute", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_duration", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_repetitions", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_brake", None)
- with self.assertRaises(pydantic.ValidationError):
- # config without repetitions
- habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration",
- brake="Unittest_brake"
- )
- )
+ with self.assertRaises(pydantic.ValidationError):
+ # config without repetitions
+ habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration", brake="Unittest_brake")
+ )
- with self.assertRaises(pydantic.ValidationError):
- # config without brake
- habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration",
- repetitions="Unittest_repetitions"
- )
- )
+ with self.assertRaises(pydantic.ValidationError):
+ # config without brake
+ habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration", repetitions="Unittest_repetitions")
+ )
diff --git a/tests/actors/config/light.py b/tests/actors/config/light.py
index e5f129b..7a0da3b 100644
--- a/tests/actors/config/light.py
+++ b/tests/actors/config/light.py
@@ -1,4 +1,5 @@
"""Test config models for light rules."""
+
import collections
import unittest.mock
@@ -13,255 +14,175 @@
class TestBrightnessTimeout(unittest.TestCase):
- """Tests for BrightnessTimeout."""
-
- def test_post_init(self):
- """Test post init checks."""
- TestCase = collections.namedtuple("TestCase", "value, timeout, valid")
-
- test_cases = [
- # valid config
- TestCase(100, 1, True),
- TestCase(1, 100, True),
- TestCase(True, 20, True),
- TestCase(False, 0, True),
- TestCase(False, 10, True),
- TestCase(False, 100, True),
- TestCase(0, 100, True),
-
- # not valid
- TestCase(100, 0, False),
- TestCase(True, 0, False),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- if test_case.valid:
- brightness_timeout = BrightnessTimeout(test_case.value, test_case.timeout)
- self.assertEqual(test_case.value, brightness_timeout.brightness)
- if test_case.value is False:
- if test_case.timeout:
- self.assertEqual(test_case.timeout, brightness_timeout.timeout)
- else:
- self.assertEqual(0.5, brightness_timeout.timeout)
- else:
- with self.assertRaises(pydantic.ValidationError):
- BrightnessTimeout(test_case.value, test_case.timeout)
+ """Tests for BrightnessTimeout."""
+
+ def test_post_init(self) -> None:
+ """Test post init checks."""
+ TestCase = collections.namedtuple("TestCase", "value, timeout, valid")
+
+ test_cases = [
+ # valid config
+ TestCase(100, 1, True),
+ TestCase(1, 100, True),
+ TestCase(True, 20, True),
+ TestCase(False, 0, True),
+ TestCase(False, 10, True),
+ TestCase(False, 100, True),
+ TestCase(0, 100, True),
+ # not valid
+ TestCase(100, 0, False),
+ TestCase(True, 0, False),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ if test_case.valid:
+ brightness_timeout = BrightnessTimeout(test_case.value, test_case.timeout)
+ self.assertEqual(test_case.value, brightness_timeout.brightness)
+ if test_case.value is False:
+ if test_case.timeout:
+ self.assertEqual(test_case.timeout, brightness_timeout.timeout)
+ else:
+ self.assertEqual(0.5, brightness_timeout.timeout)
+ else:
+ with self.assertRaises(pydantic.ValidationError):
+ BrightnessTimeout(test_case.value, test_case.timeout)
class TestLightParameter(unittest.TestCase):
- """Tests for LightParameter."""
-
- def test_post_init(self):
- """Test check in post init."""
- TestCase = collections.namedtuple("TestCase", "on, pre_off, leaving, pre_sleep, pre_sleep_prevent, valid")
-
- func_int = FunctionConfig(day=BrightnessTimeout(80, 3), night=BrightnessTimeout(40, 2), sleeping=BrightnessTimeout(20, 1))
- func_int_partial1 = FunctionConfig(day=None, night=BrightnessTimeout(40, 2), sleeping=BrightnessTimeout(20, 1))
- func_int_partial2 = FunctionConfig(day=BrightnessTimeout(80, 3), night=None, sleeping=BrightnessTimeout(20, 1))
- func_int_partial3 = FunctionConfig(day=BrightnessTimeout(80, 3), night=BrightnessTimeout(40, 2), sleeping=None)
- func_bool = FunctionConfig(day=BrightnessTimeout(True, 3), night=BrightnessTimeout(True, 2), sleeping=BrightnessTimeout(True, 1))
-
- test_cases = [
- TestCase(on=func_bool, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_bool, pre_off=func_bool, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_bool, pre_off=func_bool, leaving=func_bool, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_bool, pre_off=func_bool, leaving=func_bool, pre_sleep=func_bool, pre_sleep_prevent=None, valid=True),
-
- TestCase(on=func_int, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_int, pre_off=func_int, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_int, pre_off=func_int, leaving=func_int, pre_sleep=None, pre_sleep_prevent=None, valid=True),
- TestCase(on=func_int, pre_off=func_int, leaving=func_int, pre_sleep=func_int, pre_sleep_prevent=None, valid=True),
-
- TestCase(on=None, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_bool, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_bool, leaving=func_bool, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_bool, leaving=func_bool, pre_sleep=func_bool, pre_sleep_prevent=None, valid=False),
-
- TestCase(on=None, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_int, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_int, leaving=func_int, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=None, pre_off=func_int, leaving=func_int, pre_sleep=func_int, pre_sleep_prevent=None, valid=False),
-
- TestCase(on=func_int_partial1, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=func_int_partial2, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- TestCase(on=func_int_partial3, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- if test_case.valid:
- LightParameter(on=test_case.on, pre_off=test_case.pre_off, leaving=test_case.leaving, pre_sleep=test_case.pre_sleep, pre_sleep_prevent=test_case.pre_sleep_prevent)
- else:
- with self.assertRaises(pydantic.ValidationError):
- LightParameter(on=test_case.on, pre_off=test_case.pre_off, leaving=test_case.leaving, pre_sleep=test_case.pre_sleep, pre_sleep_prevent=test_case.pre_sleep_prevent)
-
- def test_sleep_of_pre_sleep(self):
- """Test if sleep of pre_sleep is set correctly"""
- light_config = LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 3), night=BrightnessTimeout(True, 2), sleeping=BrightnessTimeout(True, 1)),
- pre_off=None,
- leaving=None,
- pre_sleep=FunctionConfig(day=None, night=None, sleeping=BrightnessTimeout(True, 1))
- )
-
- self.assertIsNone(light_config.pre_sleep.sleeping)
+ """Tests for LightParameter."""
+
+ def test_post_init(self) -> None:
+ """Test check in post init."""
+ TestCase = collections.namedtuple("TestCase", "on, pre_off, leaving, pre_sleep, pre_sleep_prevent, valid")
+
+ func_int = FunctionConfig(day=BrightnessTimeout(80, 3), night=BrightnessTimeout(40, 2), sleeping=BrightnessTimeout(20, 1))
+ func_int_partial1 = FunctionConfig(day=None, night=BrightnessTimeout(40, 2), sleeping=BrightnessTimeout(20, 1))
+ func_int_partial2 = FunctionConfig(day=BrightnessTimeout(80, 3), night=None, sleeping=BrightnessTimeout(20, 1))
+ func_int_partial3 = FunctionConfig(day=BrightnessTimeout(80, 3), night=BrightnessTimeout(40, 2), sleeping=None)
+ func_bool = FunctionConfig(day=BrightnessTimeout(True, 3), night=BrightnessTimeout(True, 2), sleeping=BrightnessTimeout(True, 1))
+
+ test_cases = [
+ TestCase(on=func_bool, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_bool, pre_off=func_bool, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_bool, pre_off=func_bool, leaving=func_bool, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_bool, pre_off=func_bool, leaving=func_bool, pre_sleep=func_bool, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_int, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_int, pre_off=func_int, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_int, pre_off=func_int, leaving=func_int, pre_sleep=None, pre_sleep_prevent=None, valid=True),
+ TestCase(on=func_int, pre_off=func_int, leaving=func_int, pre_sleep=func_int, pre_sleep_prevent=None, valid=True),
+ TestCase(on=None, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_bool, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_bool, leaving=func_bool, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_bool, leaving=func_bool, pre_sleep=func_bool, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_int, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_int, leaving=func_int, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=None, pre_off=func_int, leaving=func_int, pre_sleep=func_int, pre_sleep_prevent=None, valid=False),
+ TestCase(on=func_int_partial1, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=func_int_partial2, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ TestCase(on=func_int_partial3, pre_off=None, leaving=None, pre_sleep=None, pre_sleep_prevent=None, valid=False),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ if test_case.valid:
+ LightParameter(on=test_case.on, pre_off=test_case.pre_off, leaving=test_case.leaving, pre_sleep=test_case.pre_sleep, pre_sleep_prevent=test_case.pre_sleep_prevent)
+ else:
+ with self.assertRaises(pydantic.ValidationError):
+ LightParameter(on=test_case.on, pre_off=test_case.pre_off, leaving=test_case.leaving, pre_sleep=test_case.pre_sleep, pre_sleep_prevent=test_case.pre_sleep_prevent)
+
+ def test_sleep_of_pre_sleep(self) -> None:
+ """Test if sleep of pre_sleep is set correctly."""
+ light_config = LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 3), night=BrightnessTimeout(True, 2), sleeping=BrightnessTimeout(True, 1)), pre_off=None, leaving=None, pre_sleep=FunctionConfig(day=None, night=None, sleeping=BrightnessTimeout(True, 1))
+ )
+
+ self.assertIsNone(light_config.pre_sleep.sleeping)
class TestLightConfig(tests.helper.test_case_base.TestCaseBase):
- """Tests for LightConfig."""
-
- def test_validate_config(self):
- """Test validate_config"""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_CustomState", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep_prevent", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion", None)
-
- # ======= min config =======
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- )
- )
-
- # ======= validate motion =======
- # motion correctly configured
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- motion="Unittest_Motion"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- motion=FunctionConfig(day=None, night=None, sleeping=None)
- )
- )
-
- # motion parameter is missing
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- motion="Unittest_Motion"
- )
- )
-
- # ======= validate door =======
- # door correctly configured
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- doors=["Unittest_Door_1"]
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- door=FunctionConfig(day=None, night=None, sleeping=None)
- )
- )
-
- # door parameter is missing
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- doors=["Unittest_Door_1"]
- )
- )
-
- # ======= validate sleep =======
- # sleep correctly configured
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- sleeping_state="Unittest_Sleep_state"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- pre_sleep=FunctionConfig(day=None, night=None, sleeping=None)
- )
- )
-
- # sleep parameter is missing
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- sleeping_state="Unittest_Sleep_state"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- pre_sleep=None
- )
- )
-
- # ======= validate presence =======
- # presence correctly configured
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- presence_state="Unittest_Presence_state"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- leaving=FunctionConfig(day=None, night=None, sleeping=None)
- )
- )
-
- # presence parameter is missing
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- presence_state="Unittest_Presence_state"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- leaving=None
- )
- )
-
- # ======= validate sleep_prevent =======
- # warning if item and parameter are given
- with unittest.mock.patch.object(habapp_rules.actors.config.light.LOGGER, "warning") as mock_warning:
- habapp_rules.actors.config.light.LightConfig(
- items=habapp_rules.actors.config.light.LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- day="Unittest_Day",
- state="H_CustomState",
- pre_sleep_prevent="Unittest_Sleep_prevent"
- ),
- parameter=habapp_rules.actors.config.light.LightParameter(
- pre_sleep_prevent=unittest.mock.Mock()
- )
- )
- mock_warning.assert_called_once()
+ """Tests for LightConfig."""
+
+ def test_validate_config(self) -> None:
+ """Test validate_config."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_CustomState", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep_prevent", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion", None)
+
+ # ======= min config =======
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(
+ light="Unittest_Light",
+ manual="Unittest_Manual",
+ day="Unittest_Day",
+ state="H_CustomState",
+ )
+ )
+
+ # ======= validate motion =======
+ # motion correctly configured
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", motion="Unittest_Motion"),
+ parameter=habapp_rules.actors.config.light.LightParameter(motion=FunctionConfig(day=None, night=None, sleeping=None)),
+ )
+
+ # motion parameter is missing
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.config.light.LightConfig(items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", motion="Unittest_Motion"))
+
+ # ======= validate door =======
+ # door correctly configured
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", doors=["Unittest_Door_1"]),
+ parameter=habapp_rules.actors.config.light.LightParameter(door=FunctionConfig(day=None, night=None, sleeping=None)),
+ )
+
+ # door parameter is missing
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.config.light.LightConfig(items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", doors=["Unittest_Door_1"]))
+
+ # ======= validate sleep =======
+ # sleep correctly configured
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", sleeping_state="Unittest_Sleep_state"),
+ parameter=habapp_rules.actors.config.light.LightParameter(pre_sleep=FunctionConfig(day=None, night=None, sleeping=None)),
+ )
+
+ # sleep parameter is missing
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", sleeping_state="Unittest_Sleep_state"),
+ parameter=habapp_rules.actors.config.light.LightParameter(pre_sleep=None),
+ )
+
+ # ======= validate presence =======
+ # presence correctly configured
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", presence_state="Unittest_Presence_state"),
+ parameter=habapp_rules.actors.config.light.LightParameter(leaving=FunctionConfig(day=None, night=None, sleeping=None)),
+ )
+
+ # presence parameter is missing
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", presence_state="Unittest_Presence_state"),
+ parameter=habapp_rules.actors.config.light.LightParameter(leaving=None),
+ )
+
+ # ======= validate sleep_prevent =======
+ # warning if item and parameter are given
+ with unittest.mock.patch.object(habapp_rules.actors.config.light.LOGGER, "warning") as mock_warning:
+ habapp_rules.actors.config.light.LightConfig(
+ items=habapp_rules.actors.config.light.LightItems(light="Unittest_Light", manual="Unittest_Manual", day="Unittest_Day", state="H_CustomState", pre_sleep_prevent="Unittest_Sleep_prevent"),
+ parameter=habapp_rules.actors.config.light.LightParameter(pre_sleep_prevent=unittest.mock.Mock()),
+ )
+ mock_warning.assert_called_once()
diff --git a/tests/actors/config/light_hcl.py b/tests/actors/config/light_hcl.py
index be3e264..0e7dbcd 100644
--- a/tests/actors/config/light_hcl.py
+++ b/tests/actors/config/light_hcl.py
@@ -1,4 +1,5 @@
"""Test config models for HCL light rules."""
+
import collections
import habapp_rules.actors.config.light_hcl
@@ -8,19 +9,15 @@
class TestLightHclConfig(tests.helper.test_case_base.TestCaseBase):
- """Test HCL config."""
+ """Test HCL config."""
- def test_sorted_color_config(self):
- """Test sorting of HCL values."""
- TestCase = collections.namedtuple("TestCase", "input, output")
+ def test_sorted_color_config(self) -> None:
+ """Test sorting of HCL values."""
+ TestCase = collections.namedtuple("TestCase", "input, output")
- test_cases = [
- TestCase([(-1, 42), (0, 100), (1, 500)], [(-1, 42), (0, 100), (1, 500)]),
- TestCase([(0, 100), (-1, 42), (1, 500)], [(-1, 42), (0, 100), (1, 500)]),
- TestCase([(1, 500), (0, 100), (-1, 42)], [(-1, 42), (0, 100), (1, 500)])
- ]
+ test_cases = [TestCase([(-1, 42), (0, 100), (1, 500)], [(-1, 42), (0, 100), (1, 500)]), TestCase([(0, 100), (-1, 42), (1, 500)], [(-1, 42), (0, 100), (1, 500)]), TestCase([(1, 500), (0, 100), (-1, 42)], [(-1, 42), (0, 100), (1, 500)])]
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- parameter = habapp_rules.actors.config.light_hcl.HclElevationParameter(color_map=test_case.input)
- self.assertEqual(test_case.output, parameter.color_map)
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ parameter = habapp_rules.actors.config.light_hcl.HclElevationParameter(color_map=test_case.input)
+ self.assertEqual(test_case.output, parameter.color_map)
diff --git a/tests/actors/config/shading.py b/tests/actors/config/shading.py
index 0a40b82..3e6849f 100644
--- a/tests/actors/config/shading.py
+++ b/tests/actors/config/shading.py
@@ -1,6 +1,8 @@
"""Test config models for shading rules."""
+
import collections
import unittest
+from itertools import starmap
import HABApp
import pydantic
@@ -12,97 +14,65 @@
class TestShadingConfig(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing ShadingConfig."""
-
- def tests_validate_model(self) -> None:
- """Test validate_model."""
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", None)
-
- # parameter NOT given | item summer NOT given
- habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- # parameter NOT given | item summer given
- habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state",
- summer="Unittest_Summer"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- # parameter given | item summer NOT given
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter(
- pos_night_close_summer=habapp_rules.actors.config.shading.ShadingPosition(42, 80)
- )
- )
-
- # parameter given | item summer given
- habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state",
- summer="Unittest_Summer"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter(
- pos_night_close_summer=habapp_rules.actors.config.shading.ShadingPosition(42, 80)
- )
- )
+ """Tests cases for testing ShadingConfig."""
+
+ def tests_validate_model(self) -> None:
+ """Test validate_model."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", None)
+
+ # parameter NOT given | item summer NOT given
+ habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", manual="Unittest_Manual", state="H_Unittest_Shading_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+
+ # parameter NOT given | item summer given
+ habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", manual="Unittest_Manual", state="H_Unittest_Shading_state", summer="Unittest_Summer"),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+
+ # parameter given | item summer NOT given
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", manual="Unittest_Manual", state="H_Unittest_Shading_state"),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(pos_night_close_summer=habapp_rules.actors.config.shading.ShadingPosition(42, 80)),
+ )
+
+ # parameter given | item summer given
+ habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", manual="Unittest_Manual", state="H_Unittest_Shading_state", summer="Unittest_Summer"),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(pos_night_close_summer=habapp_rules.actors.config.shading.ShadingPosition(42, 80)),
+ )
class TestSlatValueParameter(unittest.TestCase):
- """Test slat value parameter."""
-
- def test__check_and_sort_characteristic(self):
- """Test __check_and_sort_characteristic."""
- TestCase = collections.namedtuple("TestCase", "input, expected_output, raises")
-
- test_cases = [
- TestCase([(0, 100), (10, 50)], [(0, 100), (10, 50)], False),
- TestCase([(10, 50), (0, 100)], [(0, 100), (10, 50)], False),
-
- TestCase([(0, 100), (10, 50), (20, 50)], [(0, 100), (10, 50), (20, 50)], False),
- TestCase([(10, 50), (0, 100), (20, 50)], [(0, 100), (10, 50), (20, 50)], False),
- TestCase([(10, 50), (20, 50), (0, 100)], [(0, 100), (10, 50), (20, 50)], False),
-
- TestCase([(0, 50), (0, 40)], None, True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- input_conf = [habapp_rules.actors.config.shading.ElevationSlatMapping(*conf) for conf in test_case.input]
- output = [habapp_rules.actors.config.shading.ElevationSlatMapping(*conf) for conf in test_case.expected_output] if test_case.expected_output else None
- if test_case.raises:
- with self.assertRaises(pydantic.ValidationError):
- habapp_rules.actors.config.shading.SlatValueParameter(
- elevation_slat_characteristic=input_conf,
- elevation_slat_characteristic_summer=input_conf
- )
- else:
- config = habapp_rules.actors.config.shading.SlatValueParameter(
- elevation_slat_characteristic=input_conf,
- elevation_slat_characteristic_summer=input_conf
- )
-
- self.assertEqual(output, config.elevation_slat_characteristic)
- self.assertEqual(output, config.elevation_slat_characteristic_summer)
+ """Test slat value parameter."""
+
+ def test__check_and_sort_characteristic(self) -> None:
+ """Test __check_and_sort_characteristic."""
+ TestCase = collections.namedtuple("TestCase", "input, expected_output, raises")
+
+ test_cases = [
+ TestCase([(0, 100), (10, 50)], [(0, 100), (10, 50)], False),
+ TestCase([(10, 50), (0, 100)], [(0, 100), (10, 50)], False),
+ TestCase([(0, 100), (10, 50), (20, 50)], [(0, 100), (10, 50), (20, 50)], False),
+ TestCase([(10, 50), (0, 100), (20, 50)], [(0, 100), (10, 50), (20, 50)], False),
+ TestCase([(10, 50), (20, 50), (0, 100)], [(0, 100), (10, 50), (20, 50)], False),
+ TestCase([(0, 50), (0, 40)], None, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ input_conf = list(starmap(habapp_rules.actors.config.shading.ElevationSlatMapping, test_case.input))
+ output = list(starmap(habapp_rules.actors.config.shading.ElevationSlatMapping, test_case.expected_output)) if test_case.expected_output else None
+ if test_case.raises:
+ with self.assertRaises(pydantic.ValidationError):
+ habapp_rules.actors.config.shading.SlatValueParameter(elevation_slat_characteristic=input_conf, elevation_slat_characteristic_summer=input_conf)
+ else:
+ config = habapp_rules.actors.config.shading.SlatValueParameter(elevation_slat_characteristic=input_conf, elevation_slat_characteristic_summer=input_conf)
+
+ self.assertEqual(output, config.elevation_slat_characteristic)
+ self.assertEqual(output, config.elevation_slat_characteristic_summer)
diff --git a/tests/actors/energy_save_switch.py b/tests/actors/energy_save_switch.py
index 801f37a..c68858c 100644
--- a/tests/actors/energy_save_switch.py
+++ b/tests/actors/energy_save_switch.py
@@ -1,6 +1,6 @@
"""Test energy save switch rules."""
+
import collections
-import os
import pathlib
import sys
import unittest.mock
@@ -19,332 +19,290 @@
import tests.helper.oh_item
import tests.helper.test_case_base
import tests.helper.timer
-from habapp_rules.system import SleepState, PresenceState
+from habapp_rules.system import PresenceState, SleepState
-# pylint: disable=protected-access,no-member,too-many-public-methods
class TestEnergySaveSwitch(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing energy save switch."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Min_Switch")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Min_State")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Max_Switch")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Max_State")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Max_Manual")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_External_Request")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Current_Switch")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Current_State")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Current_Manual")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Current")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state")
-
- self._config_min = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
- items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(
- switch="Unittest_Min_Switch",
- state="Unittest_Min_State"
- )
- )
-
- self._config_max_without_current = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
- items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(
- switch="Unittest_Max_Switch",
- state="Unittest_Max_State",
- manual="Unittest_Max_Manual",
- external_request="Unittest_External_Request",
- presence_state="Unittest_Presence_state",
- sleeping_state="Unittest_Sleep_state"
- ),
- parameter=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchParameter(
- max_on_time=3600,
- hand_timeout=1800
- )
- )
-
- self._config_current = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
- items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(
- switch="Unittest_Current_Switch",
- state="Unittest_Current_State",
- manual="Unittest_Current_Manual",
- current="Unittest_Current",
- presence_state="Unittest_Presence_state",
- sleeping_state="Unittest_Sleep_state"
- ),
- parameter=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchParameter(
- current_threshold=0.1
- )
- )
-
- self._rule_min = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_min)
- self._rule_max_without_current = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_max_without_current)
- self._rule_with_current = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_current)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "energy_save_switchs"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self._rule_min.states,
- transitions=self._rule_min.trans,
- initial=self._rule_min.state,
- show_conditions=False)
-
- graph.get_graph().draw(picture_dir / "EnergySaveSwitch.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self._rule_min.states) if state not in ["Auto_Init"]]:
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self._rule_min.states,
- transitions=self._rule_min.trans,
- initial=state_name,
- show_conditions=True)
- graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"EnergySaveSwitch_{state_name}.png", format="png", prog="dot")
-
- def test_set_timeout(self):
- """Test set timeout."""
- self.assertEqual(self._rule_min.state_machine.states["Hand"].timeout, 0)
- self.assertEqual(self._rule_max_without_current.state_machine.states["Hand"].timeout, 1800)
- self.assertEqual(self._rule_with_current.state_machine.states["Hand"].timeout, 0)
-
- def test_get_initial_state(self):
- """Test get initial state."""
- TestCase = collections.namedtuple("TestCase", "current_above_threshold, manual, on_conditions_met, expected_state")
-
- test_cases = [
- # current below threshold
- TestCase(current_above_threshold=False, manual=None, on_conditions_met=False, expected_state="Auto_Off"),
- TestCase(current_above_threshold=False, manual=None, on_conditions_met=True, expected_state="Auto_On"),
-
- TestCase(current_above_threshold=False, manual=False, on_conditions_met=False, expected_state="Auto_Off"),
- TestCase(current_above_threshold=False, manual=False, on_conditions_met=True, expected_state="Auto_On"),
-
- TestCase(current_above_threshold=False, manual=True, on_conditions_met=False, expected_state="Manual"),
- TestCase(current_above_threshold=False, manual=True, on_conditions_met=True, expected_state="Manual"),
-
- # current above threshold
- TestCase(current_above_threshold=True, manual=None, on_conditions_met=False, expected_state="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, manual=None, on_conditions_met=True, expected_state="Auto_On"),
-
- TestCase(current_above_threshold=True, manual=False, on_conditions_met=False, expected_state="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, manual=False, on_conditions_met=True, expected_state="Auto_On"),
-
- TestCase(current_above_threshold=True, manual=True, on_conditions_met=False, expected_state="Manual"),
- TestCase(current_above_threshold=True, manual=True, on_conditions_met=True, expected_state="Manual"),
- ]
-
- with (unittest.mock.patch.object(self._rule_max_without_current, "_get_on_off_conditions_met") as on_conditions_mock,
- unittest.mock.patch.object(self._rule_max_without_current, "_current_above_threshold") as current_above_threshold_mock):
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- on_conditions_mock.return_value = test_case.on_conditions_met
- current_above_threshold_mock.return_value = test_case.current_above_threshold
-
- if test_case.manual is None:
- self._rule_max_without_current._config.items.manual = None
- else:
- self._rule_max_without_current._config.items.manual = unittest.mock.MagicMock()
- self._rule_max_without_current._config.items.manual.is_on.return_value = test_case.manual
-
- self.assertEqual(test_case.expected_state, self._rule_max_without_current._get_initial_state())
-
- def test_current_above_threshold(self):
- """Test current above threshold."""
- TestCase = collections.namedtuple("TestCase", "current, threshold, expected_result")
-
- test_cases = [
- TestCase(current=None, threshold=0.1, expected_result=False),
- TestCase(current=None, threshold=0.1, expected_result=False),
- TestCase(current=None, threshold=0.1, expected_result=False),
-
- TestCase(current=0.0, threshold=0.1, expected_result=False),
- TestCase(current=0.1, threshold=0.1, expected_result=False),
- TestCase(current=0.2, threshold=0.1, expected_result=True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- if test_case.current is None:
- self._rule_with_current._config.items.current = None
- else:
- self._rule_with_current._config.items.current = unittest.mock.MagicMock(value=test_case.current)
-
- self._rule_with_current._config.parameter.current_threshold = test_case.threshold
- self.assertEqual(test_case.expected_result, self._rule_with_current._current_above_threshold())
-
- def test_auto_off_transitions(self):
- """Test auto off transitions."""
- TestCase = collections.namedtuple("TestCase", "external_req, sleeping_state, presence_state, expected_state")
- test_cases = [
- TestCase(external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state="Auto_Off"),
- TestCase(external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state="Auto_Off"),
- TestCase(external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state="Auto_Off"),
- TestCase(external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
-
- TestCase(external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state="Auto_On"),
- TestCase(external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
- TestCase(external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state="Auto_On"),
- TestCase(external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
- ]
-
- tests.helper.oh_item.assert_value("Unittest_Max_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_Off")
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.item_state_change_event("Unittest_External_Request", test_case.external_req)
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleeping_state.value)
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", test_case.presence_state.value)
-
- tests.helper.oh_item.assert_value("Unittest_Max_State", test_case.expected_state)
- tests.helper.oh_item.assert_value("Unittest_Max_Switch", "ON" if test_case.expected_state == "Auto_On" else "OFF")
-
- tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_Off")
-
- def test_auto_on_transitions(self):
- """Test auto on transitions."""
- # max on time
- self._rule_min.to_Auto_On()
- self._rule_max_without_current.to_Auto_On()
- self._rule_with_current.to_Auto_On()
-
- self.assertIsNone(self._rule_min._max_on_countdown)
- self.assertIsNotNone(self._rule_max_without_current._max_on_countdown)
- self.assertIsNone(self._rule_with_current._max_on_countdown)
-
- self._rule_min._cb_max_on_countdown()
- self._rule_max_without_current._cb_max_on_countdown()
- self._rule_with_current._cb_max_on_countdown()
-
- tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_On")
- tests.helper.oh_item.assert_value("Unittest_Max_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
-
- # off conditions met
- TestCase = collections.namedtuple("TestCase", "current_above_threshold, external_req, sleeping_state, presence_state, expected_state_max, expected_state_current")
- test_cases = [
- TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
-
- TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
- TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
-
- TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
-
- TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
- TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
- ]
-
- with unittest.mock.patch.object(self._rule_with_current, "_current_above_threshold", return_value=None) as mock_current_above_threshold:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- mock_current_above_threshold.return_value = test_case.current_above_threshold
- self._rule_min.to_Auto_On()
- self._rule_max_without_current.to_Auto_On()
- self._rule_with_current.to_Auto_On()
-
- tests.helper.oh_item.item_state_change_event("Unittest_External_Request", test_case.external_req)
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleeping_state.value)
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", test_case.presence_state.value)
-
- tests.helper.oh_item.assert_value("Unittest_Max_State", test_case.expected_state_max)
- tests.helper.oh_item.assert_value("Unittest_Max_Switch", "ON" if test_case.expected_state_max == "Auto_On" else "OFF")
-
- tests.helper.oh_item.assert_value("Unittest_Current_State", test_case.expected_state_current)
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON" if test_case.expected_state_current in {"Auto_On", "Auto_WaitCurrent"} else "OFF")
-
- tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_On")
-
- def test_auto_wait_current_transitions(self):
- """Test Auto_WaitCurrent transitions."""
- # on conditions met
- self._rule_with_current.to_Auto_WaitCurrent()
- self._rule_with_current.on_conditions_met()
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON")
-
- # current below threshold
- self._rule_with_current.to_Auto_WaitCurrent()
- self._rule_with_current.current_below_threshold()
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
-
- # max_on_countdown
- self._rule_with_current.to_Auto_WaitCurrent()
- self._rule_with_current.max_on_countdown()
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
-
- def test_hand_transitions(self):
- """Test Hand transitions."""
- # max_on_countdown
- self._rule_max_without_current.to_Hand()
- self._rule_max_without_current.max_on_countdown()
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
-
- # hand timeout
- self._rule_max_without_current.to_Hand()
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
-
- # manual off
- self._rule_max_without_current.to_Hand()
- tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "ON")
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Manual")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
-
- def test_to_hand_transitions(self):
- """Test to Hand transitions."""
- for state in ["Auto_On", "Auto_WaitCurrent", "Auto_Off"]:
- with self.subTest(state=state):
- eval(f"self._rule_with_current.to_{state}()") # pylint: disable=eval-used
- tests.helper.oh_item.item_state_change_event("Unittest_Current_Switch", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Current_Switch", "ON")
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Hand")
-
- def test_manual_transitions(self):
- """Test Manual transitions."""
- # manual off | on_off_conditions not met
- self._rule_with_current.to_Manual()
- tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
-
- # manual off | on_off_conditions met
- self._rule_with_current.to_Manual()
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", PresenceState.PRESENCE.value)
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", SleepState.AWAKE.value)
- tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
-
- def test_current_switch_off(self):
- """Test current switch off."""
- tests.helper.oh_item.set_state("Unittest_Current_Switch", "ON")
- self._rule_with_current.to_Auto_WaitCurrent()
- tests.helper.oh_item.item_state_change_event("Unittest_Current", 2)
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_WaitCurrent")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON")
-
- tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.09)
- tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
- tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+ """Tests cases for testing energy save switch."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Min_Switch")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Min_State")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Max_Switch")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Max_State")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Max_Manual")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_External_Request")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Current_Switch")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Current_State")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Current_Manual")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Current")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state")
+
+ self._config_min = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(switch="Unittest_Min_Switch", state="Unittest_Min_State"))
+
+ self._config_max_without_current = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
+ items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(
+ switch="Unittest_Max_Switch", state="Unittest_Max_State", manual="Unittest_Max_Manual", external_request="Unittest_External_Request", presence_state="Unittest_Presence_state", sleeping_state="Unittest_Sleep_state"
+ ),
+ parameter=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchParameter(max_on_time=3600, hand_timeout=1800),
+ )
+
+ self._config_current = habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchConfig(
+ items=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchItems(
+ switch="Unittest_Current_Switch", state="Unittest_Current_State", manual="Unittest_Current_Manual", current="Unittest_Current", presence_state="Unittest_Presence_state", sleeping_state="Unittest_Sleep_state"
+ ),
+ parameter=habapp_rules.actors.config.energy_save_switch.EnergySaveSwitchParameter(current_threshold=0.1),
+ )
+
+ self._rule_min = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_min)
+ self._rule_max_without_current = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_max_without_current)
+ self._rule_with_current = habapp_rules.actors.energy_save_switch.EnergySaveSwitch(self._config_current)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "EnergySaveSwitch"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self._rule_min.states, transitions=self._rule_min.trans, initial=self._rule_min.state, show_conditions=False)
+
+ graph.get_graph().draw(picture_dir / "EnergySaveSwitch.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self._rule_min.states) if state != "Auto_Init"]:
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self._rule_min.states, transitions=self._rule_min.trans, initial=state_name, show_conditions=True)
+ graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"EnergySaveSwitch_{state_name}.png", format="png", prog="dot")
+
+ def test_set_timeout(self) -> None:
+ """Test set timeout."""
+ self.assertEqual(self._rule_min.state_machine.states["Hand"].timeout, 0)
+ self.assertEqual(self._rule_max_without_current.state_machine.states["Hand"].timeout, 1800)
+ self.assertEqual(self._rule_with_current.state_machine.states["Hand"].timeout, 0)
+
+ def test_get_initial_state(self) -> None:
+ """Test get initial state."""
+ TestCase = collections.namedtuple("TestCase", "current_above_threshold, manual, on_conditions_met, expected_state")
+
+ test_cases = [
+ # current below threshold
+ TestCase(current_above_threshold=False, manual=None, on_conditions_met=False, expected_state="Auto_Off"),
+ TestCase(current_above_threshold=False, manual=None, on_conditions_met=True, expected_state="Auto_On"),
+ TestCase(current_above_threshold=False, manual=False, on_conditions_met=False, expected_state="Auto_Off"),
+ TestCase(current_above_threshold=False, manual=False, on_conditions_met=True, expected_state="Auto_On"),
+ TestCase(current_above_threshold=False, manual=True, on_conditions_met=False, expected_state="Manual"),
+ TestCase(current_above_threshold=False, manual=True, on_conditions_met=True, expected_state="Manual"),
+ # current above threshold
+ TestCase(current_above_threshold=True, manual=None, on_conditions_met=False, expected_state="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, manual=None, on_conditions_met=True, expected_state="Auto_On"),
+ TestCase(current_above_threshold=True, manual=False, on_conditions_met=False, expected_state="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, manual=False, on_conditions_met=True, expected_state="Auto_On"),
+ TestCase(current_above_threshold=True, manual=True, on_conditions_met=False, expected_state="Manual"),
+ TestCase(current_above_threshold=True, manual=True, on_conditions_met=True, expected_state="Manual"),
+ ]
+
+ with unittest.mock.patch.object(self._rule_max_without_current, "_get_on_off_conditions_met") as on_conditions_mock, unittest.mock.patch.object(self._rule_max_without_current, "_current_above_threshold") as current_above_threshold_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ on_conditions_mock.return_value = test_case.on_conditions_met
+ current_above_threshold_mock.return_value = test_case.current_above_threshold
+
+ if test_case.manual is None:
+ self._rule_max_without_current._config.items.manual = None
+ else:
+ self._rule_max_without_current._config.items.manual = unittest.mock.MagicMock()
+ self._rule_max_without_current._config.items.manual.is_on.return_value = test_case.manual
+
+ self.assertEqual(test_case.expected_state, self._rule_max_without_current._get_initial_state())
+
+ def test_current_above_threshold(self) -> None:
+ """Test current above threshold."""
+ TestCase = collections.namedtuple("TestCase", "current, threshold, expected_result")
+
+ test_cases = [
+ TestCase(current=None, threshold=0.1, expected_result=False),
+ TestCase(current=None, threshold=0.1, expected_result=False),
+ TestCase(current=None, threshold=0.1, expected_result=False),
+ TestCase(current=0.0, threshold=0.1, expected_result=False),
+ TestCase(current=0.1, threshold=0.1, expected_result=False),
+ TestCase(current=0.2, threshold=0.1, expected_result=True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ if test_case.current is None:
+ self._rule_with_current._config.items.current = None
+ else:
+ self._rule_with_current._config.items.current = unittest.mock.MagicMock(value=test_case.current)
+
+ self._rule_with_current._config.parameter.current_threshold = test_case.threshold
+ self.assertEqual(test_case.expected_result, self._rule_with_current._current_above_threshold())
+
+ def test_auto_off_transitions(self) -> None:
+ """Test auto off transitions."""
+ TestCase = collections.namedtuple("TestCase", "external_req, sleeping_state, presence_state, expected_state")
+ test_cases = [
+ TestCase(external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state="Auto_Off"),
+ TestCase(external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state="Auto_Off"),
+ TestCase(external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state="Auto_Off"),
+ TestCase(external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
+ TestCase(external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state="Auto_On"),
+ TestCase(external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
+ TestCase(external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state="Auto_On"),
+ TestCase(external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state="Auto_On"),
+ ]
+
+ tests.helper.oh_item.assert_value("Unittest_Max_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_Off")
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.item_state_change_event("Unittest_External_Request", test_case.external_req)
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleeping_state.value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", test_case.presence_state.value)
+
+ tests.helper.oh_item.assert_value("Unittest_Max_State", test_case.expected_state)
+ tests.helper.oh_item.assert_value("Unittest_Max_Switch", "ON" if test_case.expected_state == "Auto_On" else "OFF")
+
+ tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_Off")
+
+ def test_auto_on_transitions(self) -> None:
+ """Test auto on transitions."""
+ # max on time
+ self._rule_min.to_Auto_On()
+ self._rule_max_without_current.to_Auto_On()
+ self._rule_with_current.to_Auto_On()
+
+ self.assertIsNone(self._rule_min._max_on_countdown)
+ self.assertIsNotNone(self._rule_max_without_current._max_on_countdown)
+ self.assertIsNone(self._rule_with_current._max_on_countdown)
+
+ self._rule_min._cb_max_on_countdown()
+ self._rule_max_without_current._cb_max_on_countdown()
+ self._rule_with_current._cb_max_on_countdown()
+
+ tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_On")
+ tests.helper.oh_item.assert_value("Unittest_Max_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
+
+ # off conditions met
+ TestCase = collections.namedtuple("TestCase", "current_above_threshold, external_req, sleeping_state, presence_state, expected_state_max, expected_state_current")
+ test_cases = [
+ TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
+ TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_Off"),
+ TestCase(current_above_threshold=False, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
+ TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_Off", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="OFF", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
+ TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.SLEEPING, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.ABSENCE, expected_state_max="Auto_On", expected_state_current="Auto_WaitCurrent"),
+ TestCase(current_above_threshold=True, external_req="ON", sleeping_state=SleepState.AWAKE, presence_state=PresenceState.PRESENCE, expected_state_max="Auto_On", expected_state_current="Auto_On"),
+ ]
+
+ with unittest.mock.patch.object(self._rule_with_current, "_current_above_threshold", return_value=None) as mock_current_above_threshold:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ mock_current_above_threshold.return_value = test_case.current_above_threshold
+ self._rule_min.to_Auto_On()
+ self._rule_max_without_current.to_Auto_On()
+ self._rule_with_current.to_Auto_On()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_External_Request", test_case.external_req)
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleeping_state.value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", test_case.presence_state.value)
+
+ tests.helper.oh_item.assert_value("Unittest_Max_State", test_case.expected_state_max)
+ tests.helper.oh_item.assert_value("Unittest_Max_Switch", "ON" if test_case.expected_state_max == "Auto_On" else "OFF")
+
+ tests.helper.oh_item.assert_value("Unittest_Current_State", test_case.expected_state_current)
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON" if test_case.expected_state_current in {"Auto_On", "Auto_WaitCurrent"} else "OFF")
+
+ tests.helper.oh_item.assert_value("Unittest_Min_State", "Auto_On")
+
+ def test_auto_wait_current_transitions(self) -> None:
+ """Test Auto_WaitCurrent transitions."""
+ # on conditions met
+ self._rule_with_current.to_Auto_WaitCurrent()
+ self._rule_with_current.on_conditions_met()
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON")
+
+ # current below threshold
+ self._rule_with_current.to_Auto_WaitCurrent()
+ self._rule_with_current.current_below_threshold()
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+
+ # max_on_countdown
+ self._rule_with_current.to_Auto_WaitCurrent()
+ self._rule_with_current.max_on_countdown()
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+
+ def test_hand_transitions(self) -> None:
+ """Test Hand transitions."""
+ # max_on_countdown
+ self._rule_max_without_current.to_Hand()
+ self._rule_max_without_current.max_on_countdown()
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+
+ # hand timeout
+ self._rule_max_without_current.to_Hand()
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+
+ # manual off
+ self._rule_max_without_current.to_Hand()
+ tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Manual")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
+
+ def test_to_hand_transitions(self) -> None:
+ """Test to Hand transitions."""
+ for state in ["Auto_On", "Auto_WaitCurrent", "Auto_Off"]:
+ with self.subTest(state=state):
+ eval(f"self._rule_with_current.to_{state}()") # noqa: S307
+ tests.helper.oh_item.item_state_change_event("Unittest_Current_Switch", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Current_Switch", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Hand")
+
+ def test_manual_transitions(self) -> None:
+ """Test Manual transitions."""
+ # manual off | on_off_conditions not met
+ self._rule_with_current.to_Manual()
+ tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+
+ # manual off | on_off_conditions met
+ self._rule_with_current.to_Manual()
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", PresenceState.PRESENCE.value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", SleepState.AWAKE.value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Current_Manual", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_On")
+
+ def test_current_switch_off(self) -> None:
+ """Test current switch off."""
+ tests.helper.oh_item.set_state("Unittest_Current_Switch", "ON")
+ self._rule_with_current.to_Auto_WaitCurrent()
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", 2)
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_WaitCurrent")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "ON")
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.09)
+ tests.helper.oh_item.assert_value("Unittest_Current_State", "Auto_Off")
+ tests.helper.oh_item.assert_value("Unittest_Current_Switch", "OFF")
diff --git a/tests/actors/heating.py b/tests/actors/heating.py
index ebd25f4..d37d7c3 100644
--- a/tests/actors/heating.py
+++ b/tests/actors/heating.py
@@ -11,77 +11,70 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access, no-member
class TestKnxHeating(tests.helper.test_case_base.TestCaseBase):
- """Test KnxHeating """
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_OH", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_KNX", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Offset", None)
-
- self._config = habapp_rules.actors.config.heating.KnxHeatingConfig(
- items=habapp_rules.actors.config.heating.KnxHeatingItems(
- virtual_temperature="Unittest_Temperature_OH",
- actor_feedback_temperature="Unittest_Temperature_KNX",
- temperature_offset="Unittest_Offset"
- )
- )
-
- self._rule = habapp_rules.actors.heating.KnxHeating(self._config)
-
- def test_init(self):
- """Test __init__."""
- rule = habapp_rules.actors.heating.KnxHeating(self._config)
- self.assertIsNone(rule._temperature)
-
- tests.helper.oh_item.set_state("Unittest_Temperature_KNX", 42)
- rule = habapp_rules.actors.heating.KnxHeating(self._config)
- self.assertEqual(42, rule._temperature)
-
- def test_feedback_temperature_changed(self):
- """Test _cb_actor_feedback_temperature_changed."""
- tests.helper.oh_item.assert_value("Unittest_Temperature_OH", None)
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_KNX", 42)
- tests.helper.oh_item.assert_value("Unittest_Temperature_OH", 42)
- self.assertEqual(42, self._rule._temperature)
-
- def test_virtual_temperature_command(self):
- """Test _cb_virtual_temperature_command."""
- # _temperature and temperature_offset are None
- self.assertIsNone(self._rule._temperature)
- tests.helper.oh_item.assert_value("Unittest_Offset", None)
-
- tests.helper.oh_item.item_command_event("Unittest_Temperature_OH", 42)
-
- self.assertEqual(42, self._rule._temperature)
- tests.helper.oh_item.assert_value("Unittest_Offset", 0)
-
- TestCase = collections.namedtuple("TestCase", "event_value, rule_temperature, offset_value, expected_new_offset")
-
- test_cases = [
- TestCase(20, 19, 0, 1),
- TestCase(21.5, 19, 0, 2.5),
- TestCase(19, 20, 0, -1),
- TestCase(15.5, 20, 0, -4.5),
-
- TestCase(20, 19, 1, 2),
- TestCase(21.5, 19, 1.5, 4),
- TestCase(22.4, 19, 1, 4.4),
-
- TestCase(20, 19, -1, 0),
- TestCase(21.5, 19, -1.5, 1),
- TestCase(22.4, 19, -1, 2.4),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self._rule._temperature = test_case.rule_temperature
- tests.helper.oh_item.set_state("Unittest_Offset", test_case.offset_value)
-
- tests.helper.oh_item.item_command_event("Unittest_Temperature_OH", test_case.event_value)
-
- self.assertEqual(test_case.expected_new_offset, round(self._rule._config.items.temperature_offset.value, 1))
- self.assertEqual(test_case.event_value, self._rule._temperature)
+ """Test KnxHeating."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_OH", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_KNX", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Offset", None)
+
+ self._config = habapp_rules.actors.config.heating.KnxHeatingConfig(
+ items=habapp_rules.actors.config.heating.KnxHeatingItems(virtual_temperature="Unittest_Temperature_OH", actor_feedback_temperature="Unittest_Temperature_KNX", temperature_offset="Unittest_Offset")
+ )
+
+ self._rule = habapp_rules.actors.heating.KnxHeating(self._config)
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ rule = habapp_rules.actors.heating.KnxHeating(self._config)
+ self.assertIsNone(rule._temperature)
+
+ tests.helper.oh_item.set_state("Unittest_Temperature_KNX", 42)
+ rule = habapp_rules.actors.heating.KnxHeating(self._config)
+ self.assertEqual(42, rule._temperature)
+
+ def test_feedback_temperature_changed(self) -> None:
+ """Test _cb_actor_feedback_temperature_changed."""
+ tests.helper.oh_item.assert_value("Unittest_Temperature_OH", None)
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_KNX", 42)
+ tests.helper.oh_item.assert_value("Unittest_Temperature_OH", 42)
+ self.assertEqual(42, self._rule._temperature)
+
+ def test_virtual_temperature_command(self) -> None:
+ """Test _cb_virtual_temperature_command."""
+ # _temperature and temperature_offset are None
+ self.assertIsNone(self._rule._temperature)
+ tests.helper.oh_item.assert_value("Unittest_Offset", None)
+
+ tests.helper.oh_item.item_command_event("Unittest_Temperature_OH", 42)
+
+ self.assertEqual(42, self._rule._temperature)
+ tests.helper.oh_item.assert_value("Unittest_Offset", 0)
+
+ TestCase = collections.namedtuple("TestCase", "event_value, rule_temperature, offset_value, expected_new_offset")
+
+ test_cases = [
+ TestCase(20, 19, 0, 1),
+ TestCase(21.5, 19, 0, 2.5),
+ TestCase(19, 20, 0, -1),
+ TestCase(15.5, 20, 0, -4.5),
+ TestCase(20, 19, 1, 2),
+ TestCase(21.5, 19, 1.5, 4),
+ TestCase(22.4, 19, 1, 4.4),
+ TestCase(20, 19, -1, 0),
+ TestCase(21.5, 19, -1.5, 1),
+ TestCase(22.4, 19, -1, 2.4),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self._rule._temperature = test_case.rule_temperature
+ tests.helper.oh_item.set_state("Unittest_Offset", test_case.offset_value)
+
+ tests.helper.oh_item.item_command_event("Unittest_Temperature_OH", test_case.event_value)
+
+ self.assertEqual(test_case.expected_new_offset, round(self._rule._config.items.temperature_offset.value, 1))
+ self.assertEqual(test_case.event_value, self._rule._temperature)
diff --git a/tests/actors/irrigation.py b/tests/actors/irrigation.py
index cb61b10..9b702c6 100644
--- a/tests/actors/irrigation.py
+++ b/tests/actors/irrigation.py
@@ -1,4 +1,5 @@
"""Test irrigation rule."""
+
import collections
import datetime
import unittest
@@ -13,194 +14,167 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access, no-member
class TestIrrigation(tests.helper.test_case_base.TestCaseBase):
- """Tests for Irrigation."""
-
- def setUp(self):
- """Set up test cases"""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_valve", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_active", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_hour", 12)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_minute", 30)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_duration", 5)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_repetitions", 3)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_brake", 10)
-
- config = habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration"
- ),
- parameter=None
- )
-
- self._irrigation_min = habapp_rules.actors.irrigation.Irrigation(config)
-
- def test__init__(self):
- """Test __init__"""
- self.assertIsNone(self._irrigation_min._config.items.repetitions)
- self.assertIsNone(self._irrigation_min._config.items.brake)
-
- # init max
- config_max = habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration",
- repetitions="Unittest_repetitions",
- brake="Unittest_brake"
- ),
- parameter=None
- )
-
- irrigation_max = habapp_rules.actors.irrigation.Irrigation(config_max)
- self.assertEqual(3, irrigation_max._config.items.repetitions.value)
- self.assertEqual(10, irrigation_max._config.items.brake.value)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_valve", None)
- tests.helper.oh_item.set_state("Unittest_active", None)
- tests.helper.oh_item.set_state("Unittest_hour", None)
- tests.helper.oh_item.set_state("Unittest_minute", None)
- tests.helper.oh_item.set_state("Unittest_duration", None)
- tests.helper.oh_item.set_state("Unittest_repetitions", None)
- tests.helper.oh_item.set_state("Unittest_brake", None)
-
- config = habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration",
- repetitions="Unittest_repetitions",
- brake="Unittest_brake"
- ),
- parameter=None
- )
-
- habapp_rules.actors.irrigation.Irrigation(config)
-
- def test_get_target_valve_state(self):
- """Test _get_target_valve_state."""
- # irrigation is not active
- for state in (None, "OFF"):
- tests.helper.oh_item.set_state("Unittest_active", state)
- self.assertFalse(self._irrigation_min._get_target_valve_state())
-
- # irrigation is active
- tests.helper.oh_item.set_state("Unittest_active", "ON")
- datetime_now = datetime.datetime(2023, 1, 1, 12, 00)
- with unittest.mock.patch("datetime.datetime") as datetime_mock, unittest.mock.patch.object(self._irrigation_min, "_is_in_time_range", return_value=False):
- datetime_mock.now.return_value = datetime_now
- self.assertFalse(self._irrigation_min._get_target_valve_state())
- datetime_mock.combine.assert_called_once_with(date=datetime_now, time=datetime.time(12, 30))
- self._irrigation_min._is_in_time_range.assert_called_once()
-
- with unittest.mock.patch("datetime.datetime") as datetime_mock, unittest.mock.patch.object(self._irrigation_min, "_is_in_time_range", return_value=True):
- datetime_mock.now.return_value = datetime_now
- self.assertTrue(self._irrigation_min._get_target_valve_state())
- datetime_mock.combine.assert_called_once_with(date=datetime_now, time=datetime.time(12, 30))
- self._irrigation_min._is_in_time_range.assert_called_once()
-
- def test_get_target_valve_state_with_repetitions(self):
- """Test _get_target_valve_state with repetitions."""
- config_max = habapp_rules.actors.config.irrigation.IrrigationConfig(
- items=habapp_rules.actors.config.irrigation.IrrigationItems(
- valve="Unittest_valve",
- active="Unittest_active",
- hour="Unittest_hour",
- minute="Unittest_minute",
- duration="Unittest_duration",
- repetitions="Unittest_repetitions",
- brake="Unittest_brake"
- ),
- parameter=None
- )
-
- irrigation_max = habapp_rules.actors.irrigation.Irrigation(config_max)
- tests.helper.oh_item.set_state("Unittest_active", "ON")
- tests.helper.oh_item.set_state("Unittest_repetitions", 2)
-
- # value of hour item is None
- with unittest.mock.patch.object(self._irrigation_min._config.items.hour, "value", None):
- self.assertFalse(self._irrigation_min._get_target_valve_state())
-
- # value of minute item is None
- with unittest.mock.patch.object(self._irrigation_min._config.items.minute, "value", None):
- self.assertFalse(self._irrigation_min._get_target_valve_state())
-
- # value of duration item is None
- with unittest.mock.patch.object(self._irrigation_min._config.items.duration, "value", None):
- self.assertFalse(self._irrigation_min._get_target_valve_state())
-
- # hour, minute and duration are valid
- with unittest.mock.patch.object(irrigation_max, "_is_in_time_range", return_value=False):
- self.assertFalse(irrigation_max._get_target_valve_state())
- self.assertEqual(3, irrigation_max._is_in_time_range.call_count)
- irrigation_max._is_in_time_range.assert_has_calls([
- unittest.mock.call(datetime.time(12, 30), datetime.time(12, 35), unittest.mock.ANY),
- unittest.mock.call(datetime.time(12, 45), datetime.time(12, 50), unittest.mock.ANY),
- unittest.mock.call(datetime.time(13, 0), datetime.time(13, 5), unittest.mock.ANY)
- ])
-
- with unittest.mock.patch.object(irrigation_max, "_is_in_time_range", side_effect=[False, True]):
- self.assertTrue(irrigation_max._get_target_valve_state())
- self.assertEqual(2, irrigation_max._is_in_time_range.call_count)
- irrigation_max._is_in_time_range.assert_has_calls([
- unittest.mock.call(datetime.time(12, 30), datetime.time(12, 35), unittest.mock.ANY),
- unittest.mock.call(datetime.time(12, 45), datetime.time(12, 50), unittest.mock.ANY),
- ])
-
- def test_is_in_time_range(self):
- """Test _is_in_time_range."""
- TestCase = collections.namedtuple("TestCase", "start_time, end_time, time_to_check, expected_result")
-
- test_cases = [
- TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(12, 30), True),
- TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(14, 30), False),
- TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(13, 00), False),
- TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(12, 00), True),
-
- TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(23, 0), True),
- TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(23, 59), True),
- TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(0, 0), True),
- TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(0, 30), True),
- TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(1, 0), False),
- ]
-
- for test_case in test_cases:
- self.assertEqual(test_case.expected_result, self._irrigation_min._is_in_time_range(test_case.start_time, test_case.end_time, test_case.time_to_check))
-
- def test_cb_set_valve_state(self):
- """Test _cb_set_valve_state."""
- # called from cyclic call
- with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=True):
- self._irrigation_min._cb_set_valve_state()
- self.assertEqual("ON", self._irrigation_min._config.items.valve.value)
-
- # called by event
- with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=False):
- self._irrigation_min._cb_set_valve_state(HABApp.openhab.events.ItemStateChangedEvent("Unittest_active", "ON", "OFF"))
- self.assertEqual("OFF", self._irrigation_min._config.items.valve.value)
-
- # same state -> no oh command
- with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=False), unittest.mock.patch.object(self._irrigation_min._config.items, "valve") as valve_mock:
- valve_mock.is_on.return_value = False
- self._irrigation_min._cb_set_valve_state()
- valve_mock.oh_send_command.assert_not_called()
-
- # exception at _get_target_valve_stat
- tests.helper.oh_item.set_state("Unittest_valve", "ON")
- with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", side_effect=habapp_rules.core.exceptions.HabAppRulesException("Could not get target state")):
- self._irrigation_min._cb_set_valve_state()
- self.assertEqual("OFF", self._irrigation_min._config.items.valve.value)
+ """Tests for Irrigation."""
+
+ def setUp(self) -> None:
+ """Set up test cases."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_valve", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_active", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_hour", 12)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_minute", 30)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_duration", 5)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_repetitions", 3)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_brake", 10)
+
+ config = habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration"), parameter=None
+ )
+
+ self._irrigation_min = habapp_rules.actors.irrigation.Irrigation(config)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ self.assertIsNone(self._irrigation_min._config.items.repetitions)
+ self.assertIsNone(self._irrigation_min._config.items.brake)
+
+ # init max
+ config_max = habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(
+ valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration", repetitions="Unittest_repetitions", brake="Unittest_brake"
+ ),
+ parameter=None,
+ )
+
+ irrigation_max = habapp_rules.actors.irrigation.Irrigation(config_max)
+ self.assertEqual(3, irrigation_max._config.items.repetitions.value)
+ self.assertEqual(10, irrigation_max._config.items.brake.value)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_valve", None)
+ tests.helper.oh_item.set_state("Unittest_active", None)
+ tests.helper.oh_item.set_state("Unittest_hour", None)
+ tests.helper.oh_item.set_state("Unittest_minute", None)
+ tests.helper.oh_item.set_state("Unittest_duration", None)
+ tests.helper.oh_item.set_state("Unittest_repetitions", None)
+ tests.helper.oh_item.set_state("Unittest_brake", None)
+
+ config = habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(
+ valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration", repetitions="Unittest_repetitions", brake="Unittest_brake"
+ ),
+ parameter=None,
+ )
+
+ habapp_rules.actors.irrigation.Irrigation(config)
+
+ def test_get_target_valve_state(self) -> None:
+ """Test _get_target_valve_state."""
+ # irrigation is not active
+ for state in (None, "OFF"):
+ tests.helper.oh_item.set_state("Unittest_active", state)
+ self.assertFalse(self._irrigation_min._get_target_valve_state())
+
+ # irrigation is active
+ tests.helper.oh_item.set_state("Unittest_active", "ON")
+ datetime_now = datetime.datetime(2023, 1, 1, 12, 00)
+ with unittest.mock.patch("datetime.datetime") as datetime_mock, unittest.mock.patch.object(self._irrigation_min, "_is_in_time_range", return_value=False):
+ datetime_mock.now.return_value = datetime_now
+ self.assertFalse(self._irrigation_min._get_target_valve_state())
+ datetime_mock.combine.assert_called_once_with(date=datetime_now, time=datetime.time(12, 30))
+ self._irrigation_min._is_in_time_range.assert_called_once()
+
+ with unittest.mock.patch("datetime.datetime") as datetime_mock, unittest.mock.patch.object(self._irrigation_min, "_is_in_time_range", return_value=True):
+ datetime_mock.now.return_value = datetime_now
+ self.assertTrue(self._irrigation_min._get_target_valve_state())
+ datetime_mock.combine.assert_called_once_with(date=datetime_now, time=datetime.time(12, 30))
+ self._irrigation_min._is_in_time_range.assert_called_once()
+
+ def test_get_target_valve_state_with_repetitions(self) -> None:
+ """Test _get_target_valve_state with repetitions."""
+ config_max = habapp_rules.actors.config.irrigation.IrrigationConfig(
+ items=habapp_rules.actors.config.irrigation.IrrigationItems(
+ valve="Unittest_valve", active="Unittest_active", hour="Unittest_hour", minute="Unittest_minute", duration="Unittest_duration", repetitions="Unittest_repetitions", brake="Unittest_brake"
+ ),
+ parameter=None,
+ )
+
+ irrigation_max = habapp_rules.actors.irrigation.Irrigation(config_max)
+ tests.helper.oh_item.set_state("Unittest_active", "ON")
+ tests.helper.oh_item.set_state("Unittest_repetitions", 2)
+
+ # value of hour item is None
+ with unittest.mock.patch.object(self._irrigation_min._config.items.hour, "value", None):
+ self.assertFalse(self._irrigation_min._get_target_valve_state())
+
+ # value of minute item is None
+ with unittest.mock.patch.object(self._irrigation_min._config.items.minute, "value", None):
+ self.assertFalse(self._irrigation_min._get_target_valve_state())
+
+ # value of duration item is None
+ with unittest.mock.patch.object(self._irrigation_min._config.items.duration, "value", None):
+ self.assertFalse(self._irrigation_min._get_target_valve_state())
+
+ # hour, minute and duration are valid
+ with unittest.mock.patch.object(irrigation_max, "_is_in_time_range", return_value=False):
+ self.assertFalse(irrigation_max._get_target_valve_state())
+ self.assertEqual(3, irrigation_max._is_in_time_range.call_count)
+ irrigation_max._is_in_time_range.assert_has_calls([
+ unittest.mock.call(datetime.time(12, 30), datetime.time(12, 35), unittest.mock.ANY),
+ unittest.mock.call(datetime.time(12, 45), datetime.time(12, 50), unittest.mock.ANY),
+ unittest.mock.call(datetime.time(13, 0), datetime.time(13, 5), unittest.mock.ANY),
+ ])
+
+ with unittest.mock.patch.object(irrigation_max, "_is_in_time_range", side_effect=[False, True]):
+ self.assertTrue(irrigation_max._get_target_valve_state())
+ self.assertEqual(2, irrigation_max._is_in_time_range.call_count)
+ irrigation_max._is_in_time_range.assert_has_calls([
+ unittest.mock.call(datetime.time(12, 30), datetime.time(12, 35), unittest.mock.ANY),
+ unittest.mock.call(datetime.time(12, 45), datetime.time(12, 50), unittest.mock.ANY),
+ ])
+
+ def test_is_in_time_range(self) -> None:
+ """Test _is_in_time_range."""
+ TestCase = collections.namedtuple("TestCase", "start_time, end_time, time_to_check, expected_result")
+
+ test_cases = [
+ TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(12, 30), True),
+ TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(14, 30), False),
+ TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(13, 00), False),
+ TestCase(datetime.time(12, 00), datetime.time(13, 00), datetime.time(12, 00), True),
+ TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(23, 0), True),
+ TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(23, 59), True),
+ TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(0, 0), True),
+ TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(0, 30), True),
+ TestCase(datetime.time(23, 00), datetime.time(1, 00), datetime.time(1, 0), False),
+ ]
+
+ for test_case in test_cases:
+ self.assertEqual(test_case.expected_result, self._irrigation_min._is_in_time_range(test_case.start_time, test_case.end_time, test_case.time_to_check))
+
+ def test_cb_set_valve_state(self) -> None:
+ """Test _cb_set_valve_state."""
+ # called from cyclic call
+ with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=True):
+ self._irrigation_min._cb_set_valve_state()
+ self.assertEqual("ON", self._irrigation_min._config.items.valve.value)
+
+ # called by event
+ with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=False):
+ self._irrigation_min._cb_set_valve_state(HABApp.openhab.events.ItemStateChangedEvent("Unittest_active", "ON", "OFF"))
+ self.assertEqual("OFF", self._irrigation_min._config.items.valve.value)
+
+ # same state -> no oh command
+ with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", return_value=False), unittest.mock.patch.object(self._irrigation_min._config.items, "valve") as valve_mock:
+ valve_mock.is_on.return_value = False
+ self._irrigation_min._cb_set_valve_state()
+ valve_mock.oh_send_command.assert_not_called()
+
+ # exception at _get_target_valve_stat
+ tests.helper.oh_item.set_state("Unittest_valve", "ON")
+ with unittest.mock.patch.object(self._irrigation_min, "_get_target_valve_state", side_effect=habapp_rules.core.exceptions.HabAppRulesError("Could not get target state")):
+ self._irrigation_min._cb_set_valve_state()
+ self.assertEqual("OFF", self._irrigation_min._config.items.valve.value)
diff --git a/tests/actors/light.py b/tests/actors/light.py
index bae422f..cc7dc78 100644
--- a/tests/actors/light.py
+++ b/tests/actors/light.py
@@ -1,7 +1,6 @@
"""Test light rules."""
-# pylint: disable=too-many-lines
+
import collections
-import os
import pathlib
import sys
import time
@@ -19,1883 +18,1798 @@
import tests.helper.oh_item
import tests.helper.test_case_base
import tests.helper.timer
-from habapp_rules.actors.config.light import LightConfig, LightItems, LightParameter, FunctionConfig, BrightnessTimeout
+from habapp_rules.actors.config.light import BrightnessTimeout, FunctionConfig, LightConfig, LightItems, LightParameter
-# pylint: disable=protected-access,no-member,too-many-public-methods
class TestLightBase(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Light rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
-
- light_parameter = LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None)
- )
-
- self.config_full = LightConfig(
- items=LightItems(
- light="Unittest_Light",
- light_control=["Unittest_Light_ctr"],
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_state",
-
- ),
- paramter=light_parameter
- )
-
- self.config_without_sleep = LightConfig(
- items=LightItems(
- light="Unittest_Light_2",
- light_control=["Unittest_Light_2_ctr"],
- manual="Unittest_Manual_2",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- state="H_Unittest_Light_2_state",
-
- ),
- paramter=light_parameter
- )
-
- with unittest.mock.patch("habapp_rules.actors.light._LightBase.__abstractmethods__", set()), unittest.mock.patch("habapp_rules.actors.light._LightBase._get_initial_state", return_value="auto_off"):
- self.light_base = habapp_rules.actors.light._LightBase(self.config_full)
- self.light_base_without_sleep = habapp_rules.actors.light._LightBase(self.config_without_sleep)
-
- self.light_base._item_light = HABApp.openhab.items.DimmerItem.get_item("Unittest_Light")
- self.light_base_without_sleep._item_light = HABApp.openhab.items.DimmerItem.get_item("Unittest_Light_2")
- self.light_base._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Light", self.light_base._cb_hand_on, self.light_base._cb_hand_off, control_names=["Unittest_Light_ctr"])
- self.light_base_without_sleep._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Light_2", self.light_base._cb_hand_on, self.light_base._cb_hand_off, control_names=["Unittest_Light_ctr"])
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "manual"},
- {"name": "auto", "initial": "init",
- "children": [
- {"name": "init"},
- {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
- {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
- {"name": "off"},
- {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
- {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
- {"name": "restoreState"}
- ]}]
- self.assertEqual(expected_states, self.light_base.states)
-
- expected_trans = [
- {"trigger": "manual_on", "source": "auto", "dest": "manual"},
- {"trigger": "manual_off", "source": "manual", "dest": "auto"},
- {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
- {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
- {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
- {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
- {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
- {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
- {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"}
- ]
- self.assertEqual(expected_trans, self.light_base.trans)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Light", None)
- tests.helper.oh_item.set_state("Unittest_Manual", None)
- tests.helper.oh_item.set_state("Unittest_Presence_state", None)
- tests.helper.oh_item.set_state("Unittest_Day", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
- with unittest.mock.patch("habapp_rules.actors.light._LightBase.__abstractmethods__", set()), unittest.mock.patch("habapp_rules.actors.light._LightBase._get_initial_state", return_value="auto_off"):
- habapp_rules.actors.light._LightBase(self.config_full)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Light_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- light_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.light_base.states,
- transitions=self.light_base.trans,
- initial=self.light_base.state,
- show_conditions=False)
-
- light_graph.get_graph().draw(picture_dir / "Light.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self.light_base.states) if state not in ["auto_init"]]:
- light_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.light_base.states,
- transitions=self.light_base.trans,
- initial=state_name,
- show_conditions=True)
- light_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Light_{state_name}.png", format="png", prog="dot")
-
- @staticmethod
- def get_initial_state_test_cases() -> collections.namedtuple:
- """Get test cases for initial state tests
-
- :return: tests cases
- """
- TestCase = collections.namedtuple("TestCase", "light_value, manual_value, sleep_value, presence_value, expected_state")
- return [
- # state OFF + Manual OFF
- TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
-
- TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
-
- TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
-
- TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
-
- TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
- TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
-
- # state OFF + Manual ON
- TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- # state ON + Manual OFF
- TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
-
- TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_presleep"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_presleep"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
-
- TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_presleep"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_presleep"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
-
- TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
-
- TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
-
- # state ON + Manual ON
- TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
-
- TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
- TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
- ]
-
- def test_get_initial_state(self):
- """Test if correct initial state will be set."""
- test_cases = self.get_initial_state_test_cases()
-
- # pre sleep configured
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True), \
- unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True), \
- unittest.mock.patch.object(self.light_base_without_sleep, "_pre_sleep_configured", return_value=False), \
- unittest.mock.patch.object(self.light_base_without_sleep, "_leaving_configured", return_value=True):
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
-
- self.assertEqual(test_case.expected_state, self.light_base._get_initial_state("default"))
-
- if (expected_state := test_case.expected_state) == "auto_presleep":
- if test_case.presence_value == habapp_rules.system.PresenceState.LEAVING.value:
- expected_state = "auto_leaving"
- else:
- expected_state = "auto_on"
-
- self.assertEqual(expected_state, self.light_base_without_sleep._get_initial_state("default"))
-
- # pre sleep not configured
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False), unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
- for test_case in test_cases:
- tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
-
- expected_state = "auto_on" if test_case.expected_state in {"auto_leaving", "auto_presleep"} else test_case.expected_state
-
- self.assertEqual(expected_state, self.light_base._get_initial_state("default"), test_case)
-
- # assert that all combinations of sleeping / presence are tested
- self.assertEqual(2 * 2 * len(habapp_rules.system.SleepState) * len(habapp_rules.system.PresenceState), len(test_cases))
-
- def test_preoff_configured(self):
- """Test _pre_off_configured."""
- TestCase = collections.namedtuple("TestCase", "timeout, result")
-
- test_cases = [
- TestCase(None, False),
- TestCase(0, False),
- TestCase(1, True),
- TestCase(42, True)
- ]
-
- for test_case in test_cases:
- self.light_base._timeout_pre_off = test_case.timeout
- self.assertEqual(test_case.result, self.light_base._pre_off_configured())
-
- def test_leaving_configured(self):
- """Test _leaving_configured."""
- TestCase = collections.namedtuple("TestCase", "leaving_only_if_on, light_value, timeout, result")
-
- test_cases = [
- TestCase(False, 0, None, False),
- TestCase(False, 0, 0, False),
- TestCase(False, 0, 1, True),
- TestCase(False, 0, 42, True),
-
- TestCase(False, 42, None, False),
- TestCase(False, 42, 0, False),
- TestCase(False, 42, 1, True),
- TestCase(False, 42, 42, True),
-
- TestCase(True, 0, None, False),
- TestCase(True, 0, 0, False),
- TestCase(True, 0, 1, False),
- TestCase(True, 0, 42, False),
-
- TestCase(True, 100, None, False),
- TestCase(True, 100, 0, False),
- TestCase(True, 100, 1, True),
- TestCase(True, 100, 42, True)
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_base._config.parameter.leaving_only_if_on = test_case.leaving_only_if_on
- self.light_base._config.items.light.value = test_case.light_value
- self.light_base._timeout_leaving = test_case.timeout
- self.assertEqual(test_case.result, self.light_base._leaving_configured())
-
- def test_pre_sleep_configured(self):
- """Test _pre_sleep_configured."""
- TestCase = collections.namedtuple("TestCase", "timeout, prevent_param, prevent_item, result")
-
- always_true = unittest.mock.Mock(return_value=True)
- always_false = unittest.mock.Mock(return_value=False)
-
- test_cases = [
- # no pre sleep prevent
- TestCase(None, None, None, False),
- TestCase(0, None, None, False),
- TestCase(1, None, None, True),
- TestCase(42, None, None, True),
-
- # prevent as item
- TestCase(None, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
- TestCase(0, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
- TestCase(1, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
- TestCase(42, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
-
- TestCase(None, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), False),
- TestCase(0, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), False),
- TestCase(1, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
- TestCase(42, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
-
- # pre sleep prevent as callable
- TestCase(None, always_true, None, False),
- TestCase(0, always_true, None, False),
- TestCase(1, always_true, None, False),
- TestCase(42, always_true, None, False),
-
- TestCase(None, always_false, None, False),
- TestCase(0, always_false, None, False),
- TestCase(1, always_false, None, True),
- TestCase(42, always_false, None, True),
-
- # pre sleep prevent as callable and item -> item has priority
- TestCase(42, always_false, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
- TestCase(42, always_false, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
- TestCase(42, always_true, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
- TestCase(42, always_true, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_base._timeout_pre_sleep = test_case.timeout
- self.light_base._config.parameter.pre_sleep_prevent = test_case.prevent_param
- self.light_base._config.items.pre_sleep_prevent = test_case.prevent_item
-
- self.light_base_without_sleep._timeout_pre_sleep = test_case.timeout
- self.light_base_without_sleep._config.parameter.pre_sleep_prevent = test_case.prevent_param
- self.light_base_without_sleep._config.items.pre_sleep_prevent = test_case.prevent_item
-
- self.assertEqual(test_case.result, self.light_base._pre_sleep_configured())
- self.assertFalse(self.light_base_without_sleep._pre_sleep_configured())
-
- # exception at callback
- with unittest.mock.patch.object(self.light_base, "_instance_logger") as logger_mock:
- self.light_base._config.items.pre_sleep_prevent = None
- self.light_base._config.parameter.pre_sleep_prevent = unittest.mock.Mock(side_effect=Exception("something went wrong"))
- self.light_base._timeout_pre_sleep = 42
- self.assertTrue(self.light_base._pre_sleep_configured())
- logger_mock.error.assert_called_once()
-
- with unittest.mock.patch.object(self.light_base, "_instance_logger") as logger_mock:
- self.light_base._config.items.pre_sleep_prevent = None
- self.light_base._config.parameter.pre_sleep_prevent = unittest.mock.Mock(side_effect=Exception("something went wrong"))
- self.light_base._timeout_pre_sleep = 0
- self.assertFalse(self.light_base._pre_sleep_configured())
- logger_mock.error.assert_called_once()
-
- def test_was_on_before(self):
- """Test _was_on_before."""
- TestCase = collections.namedtuple("TestCase", "value, result")
-
- test_cases = [
- TestCase(None, False),
- TestCase(0, False),
- TestCase(1, True),
- TestCase(42, True),
- TestCase(True, True),
- TestCase(False, False)
- ]
-
- for test_case in test_cases:
- self.light_base._brightness_before = test_case.value
- self.assertEqual(test_case.result, self.light_base._was_on_before())
-
- def test_set_timeouts(self):
- """Test _set_timeouts."""
- TestCase = collections.namedtuple("TestCase", "config, day, sleeping, timeout_on, timeout_pre_off, timeout_leaving, timeout_pre_sleep")
-
- light_config_max = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 1), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None)
- )
- )
-
- light_config_min = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=None,
- leaving=FunctionConfig(day=None, night=None, sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=None, sleeping=None),
- )
- )
-
- test_cases = [
- TestCase(light_config_max, False, False, 5, 1, 15, 7),
- TestCase(light_config_max, False, True, 2, 0, 0, 0),
- TestCase(light_config_max, True, False, 10, 4, 0, 0),
- TestCase(light_config_max, True, True, 2, 0, 0, 0),
-
- TestCase(light_config_min, False, False, 5, 0, 0, 0),
- TestCase(light_config_min, False, True, 2, 0, 0, 0),
- TestCase(light_config_min, True, False, 10, 0, 0, 0),
- TestCase(light_config_min, True, True, 2, 0, 0, 0),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_base._config.items.day = HABApp.openhab.items.SwitchItem("day", "ON" if test_case.day else "OFF")
- self.light_base._config.items.sleeping_state = _item_sleeping_state = HABApp.openhab.items.SwitchItem("sleeping", "sleeping" if test_case.sleeping else "awake")
- self.light_base._config = test_case.config
-
- self.light_base._set_timeouts()
-
- self.assertEqual(test_case.timeout_on, self.light_base.state_machine.states["auto"].states["on"].timeout)
- self.assertEqual(test_case.timeout_pre_off, self.light_base.state_machine.states["auto"].states["preoff"].timeout)
- self.assertEqual(test_case.timeout_leaving, self.light_base.state_machine.states["auto"].states["leaving"].timeout)
- self.assertEqual(test_case.timeout_pre_sleep, self.light_base.state_machine.states["auto"].states["presleep"].timeout)
-
- @staticmethod
- def get_target_brightness_test_cases() -> collections.namedtuple:
- """Get test cases for target brightness tests.
-
- :return: test cases
- """
- TestCase = collections.namedtuple("TestCase", "state, previous_state, day, sleeping, expected_value")
- return [
- # ============================== auto ON ==============================
- TestCase("auto_on", previous_state="manual", day=False, sleeping=False, expected_value=None),
- TestCase("auto_on", previous_state="manual", day=False, sleeping=True, expected_value=None),
- TestCase("auto_on", previous_state="manual", day=True, sleeping=False, expected_value=None),
- TestCase("auto_on", previous_state="manual", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_on", previous_state="auto_preoff", day=False, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_preoff", day=False, sleeping=True, expected_value=42),
- TestCase("auto_on", previous_state="auto_preoff", day=True, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_preoff", day=True, sleeping=True, expected_value=42),
-
- TestCase("auto_on", previous_state="auto_off", day=False, sleeping=False, expected_value=80),
- TestCase("auto_on", previous_state="auto_off", day=False, sleeping=True, expected_value=40),
- TestCase("auto_on", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
- TestCase("auto_on", previous_state="auto_off", day=True, sleeping=True, expected_value=40),
-
- TestCase("auto_on", previous_state="auto_leaving", day=False, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_leaving", day=False, sleeping=True, expected_value=42),
- TestCase("auto_on", previous_state="auto_leaving", day=True, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_leaving", day=True, sleeping=True, expected_value=42),
-
- TestCase("auto_on", previous_state="auto_presleep", day=False, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_presleep", day=False, sleeping=True, expected_value=42),
- TestCase("auto_on", previous_state="auto_presleep", day=True, sleeping=False, expected_value=42),
- TestCase("auto_on", previous_state="auto_presleep", day=True, sleeping=True, expected_value=42),
-
- # ============================== auto PRE_OFF ==============================
- TestCase("auto_preoff", previous_state="auto_on", day=False, sleeping=False, expected_value=32),
- TestCase("auto_preoff", previous_state="auto_on", day=False, sleeping=True, expected_value=None),
- TestCase("auto_preoff", previous_state="auto_on", day=True, sleeping=False, expected_value=40),
- TestCase("auto_preoff", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
-
- # ============================== auto OFF ==============================
- TestCase("auto_off", previous_state="manual", day=False, sleeping=False, expected_value=None),
- TestCase("auto_off", previous_state="manual", day=False, sleeping=True, expected_value=None),
- TestCase("auto_off", previous_state="manual", day=True, sleeping=False, expected_value=None),
- TestCase("auto_off", previous_state="manual", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_off", previous_state="auto_on", day=False, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_on", day=False, sleeping=True, expected_value=False),
- TestCase("auto_off", previous_state="auto_on", day=True, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_on", day=True, sleeping=True, expected_value=False),
-
- TestCase("auto_off", previous_state="auto_preoff", day=False, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_preoff", day=False, sleeping=True, expected_value=False),
- TestCase("auto_off", previous_state="auto_preoff", day=True, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_preoff", day=True, sleeping=True, expected_value=False),
-
- TestCase("auto_off", previous_state="auto_leaving", day=False, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_leaving", day=False, sleeping=True, expected_value=False),
- TestCase("auto_off", previous_state="auto_leaving", day=True, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_leaving", day=True, sleeping=True, expected_value=False),
-
- TestCase("auto_off", previous_state="auto_presleep", day=False, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_presleep", day=False, sleeping=True, expected_value=False),
- TestCase("auto_off", previous_state="auto_presleep", day=True, sleeping=False, expected_value=False),
- TestCase("auto_off", previous_state="auto_presleep", day=True, sleeping=True, expected_value=False),
-
- # ============================== auto leaving ==============================
- TestCase("auto_leaving", previous_state="auto_on", day=False, sleeping=False, expected_value=40),
- TestCase("auto_leaving", previous_state="auto_on", day=False, sleeping=True, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_on", day=True, sleeping=False, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_leaving", previous_state="auto_preoff", day=False, sleeping=False, expected_value=40),
- TestCase("auto_leaving", previous_state="auto_preoff", day=False, sleeping=True, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_preoff", day=True, sleeping=False, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_preoff", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_leaving", previous_state="auto_off", day=False, sleeping=False, expected_value=40),
- TestCase("auto_leaving", previous_state="auto_off", day=False, sleeping=True, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_off", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_leaving", previous_state="auto_presleep", day=False, sleeping=False, expected_value=40),
- TestCase("auto_leaving", previous_state="auto_presleep", day=False, sleeping=True, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_presleep", day=True, sleeping=False, expected_value=None),
- TestCase("auto_leaving", previous_state="auto_presleep", day=True, sleeping=True, expected_value=None),
-
- # ============================== auto PRE_SLEEP ==============================
- TestCase("auto_presleep", previous_state="auto_on", day=False, sleeping=False, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_on", day=False, sleeping=True, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_on", day=True, sleeping=False, expected_value=None),
- TestCase("auto_presleep", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_presleep", previous_state="auto_preoff", day=False, sleeping=False, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_preoff", day=False, sleeping=True, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_preoff", day=True, sleeping=False, expected_value=None),
- TestCase("auto_presleep", previous_state="auto_preoff", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_presleep", previous_state="auto_off", day=False, sleeping=False, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_off", day=False, sleeping=True, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
- TestCase("auto_presleep", previous_state="auto_off", day=True, sleeping=True, expected_value=None),
-
- TestCase("auto_presleep", previous_state="auto_leaving", day=False, sleeping=False, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_leaving", day=False, sleeping=True, expected_value=10),
- TestCase("auto_presleep", previous_state="auto_leaving", day=True, sleeping=False, expected_value=None),
- TestCase("auto_presleep", previous_state="auto_leaving", day=True, sleeping=True, expected_value=None),
-
- TestCase("init", previous_state="does_not_matter", day=False, sleeping=False, expected_value=None),
- TestCase("init", previous_state="does_not_matter", day=False, sleeping=True, expected_value=None),
- TestCase("init", previous_state="does_not_matter", day=True, sleeping=False, expected_value=None),
- TestCase("init", previous_state="does_not_matter", day=True, sleeping=True, expected_value=None)
- ]
-
- def test_get_target_brightness(self):
- """Test _get_target_brightness."""
-
- light_config = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(32, 1), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None)
- )
- )
- self.light_base._config = light_config
- self.light_base._brightness_before = 42
- self.light_base._state_observer._value = 100
- self.light_base._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
-
- self.light_base_without_sleep._config = light_config
- self.light_base_without_sleep._brightness_before = 42
- self.light_base_without_sleep._state_observer._value = 100
- self.light_base_without_sleep._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
-
- for test_case in self.get_target_brightness_test_cases():
- self.light_base._config.items.sleeping_state.value = habapp_rules.system.SleepState.SLEEPING.value if test_case.sleeping else habapp_rules.system.SleepState.AWAKE.value
- self.light_base._config.items.day.value = "ON" if test_case.day else "OFF"
- self.light_base.state = test_case.state
- self.light_base._previous_state = test_case.previous_state
-
- self.light_base_without_sleep._config.items.day.value = "ON" if test_case.day else "OFF"
- self.light_base_without_sleep.state = test_case.state
- self.light_base_without_sleep._previous_state = test_case.previous_state
-
- self.assertEqual(test_case.expected_value, self.light_base._get_target_brightness(), test_case)
-
- if test_case.state != "auto_presleep" and test_case.previous_state != "auto_presleep" and not test_case.sleeping:
- self.assertEqual(test_case.expected_value, self.light_base_without_sleep._get_target_brightness(), test_case)
-
- # switch on by value
- for switch_on_value in [20, "INCREASE"]:
- self.light_base._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", switch_on_value)
- for test_case in self.get_target_brightness_test_cases():
- if test_case.state == "auto_on" and test_case.previous_state == "auto_off":
- self.light_base.state = test_case.state
- self.light_base._previous_state = test_case.previous_state
- self.assertIsNone(self.light_base._get_target_brightness())
-
- def test_auto_off_transitions(self):
- """Test transitions of auto_off."""
- # to auto_on by hand trigger
- self.light_base.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Light", "ON", "OFF")
- self.assertEqual("auto_on", self.light_base.state)
-
- # to leaving (configured)
- self.light_base.to_auto_off()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_leaving", self.light_base.state)
-
- # to leaving (NOT configured)
- self.light_base.to_auto_off()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_off", self.light_base.state)
-
- # to pre sleep (configured)
- self.light_base.to_auto_off()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True), unittest.mock.patch.object(self.config_full.parameter.pre_sleep, "day", BrightnessTimeout(67, 20)):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_presleep", self.light_base.state)
-
- # to pre sleep (NOT configured)
- self.light_base.to_auto_off()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_off", self.light_base.state)
-
- def test_auto_on_transitions(self):
- """Test transitions of auto_on."""
- self.light_base._state_observer._value = 20
-
- # to auto_off by hand
- self.light_base.to_auto_on()
- tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
- self.assertEqual("auto_off", self.light_base.state)
-
- # to leaving (configured)
- self.light_base.to_auto_on()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_leaving", self.light_base.state)
-
- # to leaving (NOT configured)
- self.light_base.to_auto_on()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_on", self.light_base.state)
-
- # to sleeping (configured)
- self.light_base.to_auto_on()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_presleep", self.light_base.state)
-
- # to sleeping (NOT configured)
- self.light_base.to_auto_on()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_on", self.light_base.state)
-
- def test_auto_pre_off_transitions(self):
- """Test transitions of auto_preoff."""
- event_mock = unittest.mock.MagicMock()
-
- # to auto off by timeout
- self.light_base.to_auto_preoff()
- self.light_base.preoff_timeout()
- tests.helper.oh_item.item_state_change_event("Unittest_Light", 0.0)
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto on by hand_on
- self.light_base.to_auto_preoff()
- self.light_base._cb_hand_on(event_mock)
- self.assertEqual("auto_on", self.light_base.state)
-
- # to auto on by hand_off
- self.light_base.to_auto_preoff()
- self.light_base._cb_hand_off(event_mock)
- self.assertEqual("auto_on", self.light_base.state)
-
- # to leaving (configured)
- self.light_base.to_auto_preoff()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_leaving", self.light_base.state)
-
- # to leaving (NOT configured)
- self.light_base.to_auto_preoff()
- with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_preoff", self.light_base.state)
-
- # to sleeping (configured)
- self.light_base.to_auto_preoff()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_presleep", self.light_base.state)
-
- # to sleeping (NOT configured)
- self.light_base.to_auto_preoff()
- with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("auto_preoff", self.light_base.state)
-
- def test_auto_pre_sleep(self):
- """Test transitions of auto_presleep."""
- # to auto_off by hand_off
- self.light_base.to_auto_presleep()
- self.light_base._state_observer._value = 20
- tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_off by timeout
- self.light_base.to_auto_presleep()
- self.light_base.presleep_timeout()
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_off by sleep_aborted | was_on_before = False
- self.light_base.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value)
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_on by sleep_aborted | was_on_before = True
- self.light_base.to_auto_on()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value)
- self.assertEqual("auto_on", self.light_base.state)
-
- def test_auto_leaving(self):
- """Test transitions of auto_presleep."""
- # to auto_off by hand_off
- self.light_base.to_auto_leaving()
- self.light_base._state_observer._value = 20
- tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_off by timeout
- self.light_base.to_auto_leaving()
- self.light_base.leaving_timeout()
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_off by sleep_aborted | was_on_before = False
- self.light_base.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value, )
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
- self.assertEqual("auto_off", self.light_base.state)
-
- # to auto_on by sleep_aborted | was_on_before = True
- self.light_base.to_auto_on()
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value, )
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
- self.assertEqual("auto_on", self.light_base.state)
-
- def test_auto_restoreState(self): # pylint: disable=invalid-name
- """Test transitions of auto_restoreState"""
- self.light_base.to_auto_preoff()
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value, )
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
- self.assertEqual("auto_off", self.light_base.state)
-
- def test_manual(self):
- """Test manual switch."""
- auto_state = self.light_base.states[1]
- self.assertEqual("auto", auto_state["name"])
-
- for item_state in (0, 50, "OFF", "ON"):
- self.light_base._item_light.value = item_state
- for state_name in [f"auto_{state['name']}" for state in auto_state["children"] if "init" not in state["name"]]:
- eval(f"self.light_base.to_{state_name}()") # pylint: disable=eval-used
- self.assertEqual(state_name, self.light_base.state)
- tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
- self.assertEqual("manual", self.light_base.state)
- tests.helper.oh_item.send_command("Unittest_Manual", "OFF", "ON")
- if self.light_base._item_light:
- self.assertEqual("auto_on", self.light_base.state)
- else:
- self.assertEqual("auto_off", self.light_base.state)
-
- def test_cb_day(self):
- """Test callback_day."""
- # ON
- with unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
- tests.helper.oh_item.send_command("Unittest_Day", "ON", "OFF")
- set_timeouts_mock.assert_called_once()
-
- # OFF
- with unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
- tests.helper.oh_item.send_command("Unittest_Day", "OFF", "ON")
- set_timeouts_mock.assert_called_once()
-
- def test_cb_presence(self):
- """Test callback_presence -> only states where nothing should happen."""
- for state_name in ["presence", "absence", "long_absence"]:
- with unittest.mock.patch.object(self.light_base, "leaving_started") as started_mock, \
- unittest.mock.patch.object(self.light_base, "leaving_aborted") as aborted_mock, \
- unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState(state_name).value, habapp_rules.system.PresenceState.LEAVING.value)
- set_timeouts_mock.assert_called_once()
- started_mock.assert_not_called()
- aborted_mock.assert_not_called()
-
- def test_cb_sleeping(self):
- """Test callback_presence -> only states where nothing should happen."""
- for state_name in ["awake", "sleeping", "post_sleeping", "locked"]:
- with unittest.mock.patch.object(self.light_base, "sleep_started") as started_mock, \
- unittest.mock.patch.object(self.light_base, "sleep_aborted") as aborted_mock, \
- unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState(state_name).value, habapp_rules.system.SleepState.PRE_SLEEPING.value)
- set_timeouts_mock.assert_called_once()
- started_mock.assert_not_called()
- aborted_mock.assert_not_called()
-
-
-# pylint: disable=protected-access,no-member,too-many-public-methods
+ """Tests cases for testing Light rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
+
+ light_parameter = LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None),
+ )
+
+ self.config_full = LightConfig(
+ items=LightItems(
+ light="Unittest_Light",
+ light_control=["Unittest_Light_ctr"],
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_state",
+ ),
+ paramter=light_parameter,
+ )
+
+ self.config_without_sleep = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_2",
+ light_control=["Unittest_Light_2_ctr"],
+ manual="Unittest_Manual_2",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ state="H_Unittest_Light_2_state",
+ ),
+ paramter=light_parameter,
+ )
+
+ with unittest.mock.patch("habapp_rules.actors.light._LightBase.__abstractmethods__", set()), unittest.mock.patch("habapp_rules.actors.light._LightBase._get_initial_state", return_value="auto_off"):
+ self.light_base = habapp_rules.actors.light._LightBase(self.config_full)
+ self.light_base_without_sleep = habapp_rules.actors.light._LightBase(self.config_without_sleep)
+
+ self.light_base._item_light = HABApp.openhab.items.DimmerItem.get_item("Unittest_Light")
+ self.light_base_without_sleep._item_light = HABApp.openhab.items.DimmerItem.get_item("Unittest_Light_2")
+ self.light_base._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Light", self.light_base._cb_hand_on, self.light_base._cb_hand_off, control_names=["Unittest_Light_ctr"])
+ self.light_base_without_sleep._state_observer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Light_2", self.light_base._cb_hand_on, self.light_base._cb_hand_off, control_names=["Unittest_Light_ctr"])
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "manual"},
+ {
+ "name": "auto",
+ "initial": "init",
+ "children": [
+ {"name": "init"},
+ {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
+ {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
+ {"name": "off"},
+ {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
+ {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
+ {"name": "restoreState"},
+ ],
+ },
+ ]
+ self.assertEqual(expected_states, self.light_base.states)
+
+ expected_trans = [
+ {"trigger": "manual_on", "source": "auto", "dest": "manual"},
+ {"trigger": "manual_off", "source": "manual", "dest": "auto"},
+ {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
+ {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
+ {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
+ {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
+ {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
+ {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
+ ]
+ self.assertEqual(expected_trans, self.light_base.trans)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Light", None)
+ tests.helper.oh_item.set_state("Unittest_Manual", None)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", None)
+ tests.helper.oh_item.set_state("Unittest_Day", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
+ with unittest.mock.patch("habapp_rules.actors.light._LightBase.__abstractmethods__", set()), unittest.mock.patch("habapp_rules.actors.light._LightBase._get_initial_state", return_value="auto_off"):
+ habapp_rules.actors.light._LightBase(self.config_full)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Light"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ light_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.light_base.states, transitions=self.light_base.trans, initial=self.light_base.state, show_conditions=False)
+
+ light_graph.get_graph().draw(picture_dir / "Light.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self.light_base.states) if state != "auto_init"]:
+ light_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.light_base.states, transitions=self.light_base.trans, initial=state_name, show_conditions=True)
+ light_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Light_{state_name}.png", format="png", prog="dot")
+
+ @staticmethod
+ def get_initial_state_test_cases() -> collections.namedtuple:
+ """Get test cases for initial state tests.
+
+ Returns:
+ tests cases
+ """
+ TestCase = collections.namedtuple("TestCase", "light_value, manual_value, sleep_value, presence_value, expected_state")
+ return [
+ # state OFF + Manual OFF
+ TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_off"),
+ TestCase(0, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_off"),
+ # state OFF + Manual ON
+ TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(0, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ # state ON + Manual OFF
+ TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_presleep"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_presleep"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_presleep"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_presleep"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "auto_leaving"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "auto_leaving"),
+ # state ON + Manual ON
+ TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LEAVING.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.ABSENCE.value, "manual"),
+ TestCase(42, "ON", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.LONG_ABSENCE.value, "manual"),
+ ]
+
+ def test_get_initial_state(self) -> None:
+ """Test if correct initial state will be set."""
+ test_cases = self.get_initial_state_test_cases()
+
+ # pre sleep configured
+ with (
+ unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True),
+ unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True),
+ unittest.mock.patch.object(self.light_base_without_sleep, "_pre_sleep_configured", return_value=False),
+ unittest.mock.patch.object(self.light_base_without_sleep, "_leaving_configured", return_value=True),
+ ):
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
+
+ self.assertEqual(test_case.expected_state, self.light_base._get_initial_state("default"))
+
+ if (expected_state := test_case.expected_state) == "auto_presleep":
+ expected_state = "auto_leaving" if test_case.presence_value == habapp_rules.system.PresenceState.LEAVING.value else "auto_on"
+
+ self.assertEqual(expected_state, self.light_base_without_sleep._get_initial_state("default"))
+
+ # pre sleep not configured
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False), unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
+ for test_case in test_cases:
+ tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
+
+ expected_state = "auto_on" if test_case.expected_state in {"auto_leaving", "auto_presleep"} else test_case.expected_state
+
+ self.assertEqual(expected_state, self.light_base._get_initial_state("default"), test_case)
+
+ # assert that all combinations of sleeping / presence are tested
+ self.assertEqual(2 * 2 * len(habapp_rules.system.SleepState) * len(habapp_rules.system.PresenceState), len(test_cases))
+
+ def test_preoff_configured(self) -> None:
+ """Test _pre_off_configured."""
+ TestCase = collections.namedtuple("TestCase", "timeout, result")
+
+ test_cases = [TestCase(None, False), TestCase(0, False), TestCase(1, True), TestCase(42, True)]
+
+ for test_case in test_cases:
+ self.light_base._timeout_pre_off = test_case.timeout
+ self.assertEqual(test_case.result, self.light_base._pre_off_configured())
+
+ def test_leaving_configured(self) -> None:
+ """Test _leaving_configured."""
+ TestCase = collections.namedtuple("TestCase", "leaving_only_if_on, light_value, timeout, result")
+
+ test_cases = [
+ TestCase(False, 0, None, False),
+ TestCase(False, 0, 0, False),
+ TestCase(False, 0, 1, True),
+ TestCase(False, 0, 42, True),
+ TestCase(False, 42, None, False),
+ TestCase(False, 42, 0, False),
+ TestCase(False, 42, 1, True),
+ TestCase(False, 42, 42, True),
+ TestCase(True, 0, None, False),
+ TestCase(True, 0, 0, False),
+ TestCase(True, 0, 1, False),
+ TestCase(True, 0, 42, False),
+ TestCase(True, 100, None, False),
+ TestCase(True, 100, 0, False),
+ TestCase(True, 100, 1, True),
+ TestCase(True, 100, 42, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_base._config.parameter.leaving_only_if_on = test_case.leaving_only_if_on
+ self.light_base._config.items.light.value = test_case.light_value
+ self.light_base._timeout_leaving = test_case.timeout
+ self.assertEqual(test_case.result, self.light_base._leaving_configured())
+
+ def test_pre_sleep_configured(self) -> None:
+ """Test _pre_sleep_configured."""
+ TestCase = collections.namedtuple("TestCase", "timeout, prevent_param, prevent_item, result")
+
+ always_true = unittest.mock.Mock(return_value=True)
+ always_false = unittest.mock.Mock(return_value=False)
+
+ test_cases = [
+ # no pre sleep prevent
+ TestCase(None, None, None, False),
+ TestCase(0, None, None, False),
+ TestCase(1, None, None, True),
+ TestCase(42, None, None, True),
+ # prevent as item
+ TestCase(None, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ TestCase(0, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ TestCase(1, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ TestCase(42, None, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ TestCase(None, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), False),
+ TestCase(0, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), False),
+ TestCase(1, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
+ TestCase(42, None, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
+ # pre sleep prevent as callable
+ TestCase(None, always_true, None, False),
+ TestCase(0, always_true, None, False),
+ TestCase(1, always_true, None, False),
+ TestCase(42, always_true, None, False),
+ TestCase(None, always_false, None, False),
+ TestCase(0, always_false, None, False),
+ TestCase(1, always_false, None, True),
+ TestCase(42, always_false, None, True),
+ # pre sleep prevent as callable and item -> item has priority
+ TestCase(42, always_false, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
+ TestCase(42, always_false, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ TestCase(42, always_true, HABApp.openhab.items.SwitchItem("Test", "OFF"), True),
+ TestCase(42, always_true, HABApp.openhab.items.SwitchItem("Test", "ON"), False),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_base._timeout_pre_sleep = test_case.timeout
+ self.light_base._config.parameter.pre_sleep_prevent = test_case.prevent_param
+ self.light_base._config.items.pre_sleep_prevent = test_case.prevent_item
+
+ self.light_base_without_sleep._timeout_pre_sleep = test_case.timeout
+ self.light_base_without_sleep._config.parameter.pre_sleep_prevent = test_case.prevent_param
+ self.light_base_without_sleep._config.items.pre_sleep_prevent = test_case.prevent_item
+
+ self.assertEqual(test_case.result, self.light_base._pre_sleep_configured())
+ self.assertFalse(self.light_base_without_sleep._pre_sleep_configured())
+
+ # exception at callback
+ with unittest.mock.patch.object(self.light_base, "_instance_logger") as logger_mock:
+ self.light_base._config.items.pre_sleep_prevent = None
+ self.light_base._config.parameter.pre_sleep_prevent = unittest.mock.Mock(side_effect=Exception("something went wrong"))
+ self.light_base._timeout_pre_sleep = 42
+ self.assertTrue(self.light_base._pre_sleep_configured())
+ logger_mock.exception.assert_called_once()
+
+ with unittest.mock.patch.object(self.light_base, "_instance_logger") as logger_mock:
+ self.light_base._config.items.pre_sleep_prevent = None
+ self.light_base._config.parameter.pre_sleep_prevent = unittest.mock.Mock(side_effect=Exception("something went wrong"))
+ self.light_base._timeout_pre_sleep = 0
+ self.assertFalse(self.light_base._pre_sleep_configured())
+ logger_mock.exception.assert_called_once()
+
+ def test_was_on_before(self) -> None:
+ """Test _was_on_before."""
+ TestCase = collections.namedtuple("TestCase", "value, result")
+
+ test_cases = [TestCase(None, False), TestCase(0, False), TestCase(1, True), TestCase(42, True), TestCase(True, True), TestCase(False, False)]
+
+ for test_case in test_cases:
+ self.light_base._brightness_before = test_case.value
+ self.assertEqual(test_case.result, self.light_base._was_on_before())
+
+ def test_set_timeouts(self) -> None:
+ """Test _set_timeouts."""
+ TestCase = collections.namedtuple("TestCase", "config, day, sleeping, timeout_on, timeout_pre_off, timeout_leaving, timeout_pre_sleep")
+
+ light_config_max = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 1), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
+ ),
+ )
+
+ light_config_min = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=None,
+ leaving=FunctionConfig(day=None, night=None, sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=None, sleeping=None),
+ ),
+ )
+
+ test_cases = [
+ TestCase(light_config_max, False, False, 5, 1, 15, 7),
+ TestCase(light_config_max, False, True, 2, 0, 0, 0),
+ TestCase(light_config_max, True, False, 10, 4, 0, 0),
+ TestCase(light_config_max, True, True, 2, 0, 0, 0),
+ TestCase(light_config_min, False, False, 5, 0, 0, 0),
+ TestCase(light_config_min, False, True, 2, 0, 0, 0),
+ TestCase(light_config_min, True, False, 10, 0, 0, 0),
+ TestCase(light_config_min, True, True, 2, 0, 0, 0),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_base._config.items.day = HABApp.openhab.items.SwitchItem("day", "ON" if test_case.day else "OFF")
+ self.light_base._config.items.sleeping_state = _item_sleeping_state = HABApp.openhab.items.SwitchItem("sleeping", "sleeping" if test_case.sleeping else "awake")
+ self.light_base._config = test_case.config
+
+ self.light_base._set_timeouts()
+
+ self.assertEqual(test_case.timeout_on, self.light_base.state_machine.states["auto"].states["on"].timeout)
+ self.assertEqual(test_case.timeout_pre_off, self.light_base.state_machine.states["auto"].states["preoff"].timeout)
+ self.assertEqual(test_case.timeout_leaving, self.light_base.state_machine.states["auto"].states["leaving"].timeout)
+ self.assertEqual(test_case.timeout_pre_sleep, self.light_base.state_machine.states["auto"].states["presleep"].timeout)
+
+ @staticmethod
+ def get_target_brightness_test_cases() -> collections.namedtuple:
+ """Get test cases for target brightness tests.
+
+ Returns:
+ test cases
+ """
+ TestCase = collections.namedtuple("TestCase", "state, previous_state, day, sleeping, expected_value")
+ return [
+ # ============================== auto ON ==============================
+ TestCase("auto_on", previous_state="manual", day=False, sleeping=False, expected_value=None),
+ TestCase("auto_on", previous_state="manual", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_on", previous_state="manual", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_on", previous_state="manual", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_on", previous_state="auto_preoff", day=False, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_preoff", day=False, sleeping=True, expected_value=42),
+ TestCase("auto_on", previous_state="auto_preoff", day=True, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_preoff", day=True, sleeping=True, expected_value=42),
+ TestCase("auto_on", previous_state="auto_off", day=False, sleeping=False, expected_value=80),
+ TestCase("auto_on", previous_state="auto_off", day=False, sleeping=True, expected_value=40),
+ TestCase("auto_on", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_on", previous_state="auto_off", day=True, sleeping=True, expected_value=40),
+ TestCase("auto_on", previous_state="auto_leaving", day=False, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_leaving", day=False, sleeping=True, expected_value=42),
+ TestCase("auto_on", previous_state="auto_leaving", day=True, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_leaving", day=True, sleeping=True, expected_value=42),
+ TestCase("auto_on", previous_state="auto_presleep", day=False, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_presleep", day=False, sleeping=True, expected_value=42),
+ TestCase("auto_on", previous_state="auto_presleep", day=True, sleeping=False, expected_value=42),
+ TestCase("auto_on", previous_state="auto_presleep", day=True, sleeping=True, expected_value=42),
+ # ============================== auto PRE_OFF ==============================
+ TestCase("auto_preoff", previous_state="auto_on", day=False, sleeping=False, expected_value=32),
+ TestCase("auto_preoff", previous_state="auto_on", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_preoff", previous_state="auto_on", day=True, sleeping=False, expected_value=40),
+ TestCase("auto_preoff", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
+ # ============================== auto OFF ==============================
+ TestCase("auto_off", previous_state="manual", day=False, sleeping=False, expected_value=None),
+ TestCase("auto_off", previous_state="manual", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_off", previous_state="manual", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_off", previous_state="manual", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_off", previous_state="auto_on", day=False, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_on", day=False, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_on", day=True, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_on", day=True, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_preoff", day=False, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_preoff", day=False, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_preoff", day=True, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_preoff", day=True, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_leaving", day=False, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_leaving", day=False, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_leaving", day=True, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_leaving", day=True, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_presleep", day=False, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_presleep", day=False, sleeping=True, expected_value=False),
+ TestCase("auto_off", previous_state="auto_presleep", day=True, sleeping=False, expected_value=False),
+ TestCase("auto_off", previous_state="auto_presleep", day=True, sleeping=True, expected_value=False),
+ # ============================== auto leaving ==============================
+ TestCase("auto_leaving", previous_state="auto_on", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_leaving", previous_state="auto_on", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_on", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_preoff", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_leaving", previous_state="auto_preoff", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_preoff", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_preoff", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_off", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_leaving", previous_state="auto_off", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_off", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_presleep", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_leaving", previous_state="auto_presleep", day=False, sleeping=True, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_presleep", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_leaving", previous_state="auto_presleep", day=True, sleeping=True, expected_value=None),
+ # ============================== auto PRE_SLEEP ==============================
+ TestCase("auto_presleep", previous_state="auto_on", day=False, sleeping=False, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_on", day=False, sleeping=True, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_on", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_on", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_preoff", day=False, sleeping=False, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_preoff", day=False, sleeping=True, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_preoff", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_preoff", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_off", day=False, sleeping=False, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_off", day=False, sleeping=True, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_off", day=True, sleeping=True, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_leaving", day=False, sleeping=False, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_leaving", day=False, sleeping=True, expected_value=10),
+ TestCase("auto_presleep", previous_state="auto_leaving", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_presleep", previous_state="auto_leaving", day=True, sleeping=True, expected_value=None),
+ TestCase("init", previous_state="does_not_matter", day=False, sleeping=False, expected_value=None),
+ TestCase("init", previous_state="does_not_matter", day=False, sleeping=True, expected_value=None),
+ TestCase("init", previous_state="does_not_matter", day=True, sleeping=False, expected_value=None),
+ TestCase("init", previous_state="does_not_matter", day=True, sleeping=True, expected_value=None),
+ ]
+
+ def test_get_target_brightness(self) -> None:
+ """Test _get_target_brightness."""
+ light_config = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(32, 1), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
+ ),
+ )
+ self.light_base._config = light_config
+ self.light_base._brightness_before = 42
+ self.light_base._state_observer._value = 100
+ self.light_base._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
+
+ self.light_base_without_sleep._config = light_config
+ self.light_base_without_sleep._brightness_before = 42
+ self.light_base_without_sleep._state_observer._value = 100
+ self.light_base_without_sleep._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
+
+ for test_case in self.get_target_brightness_test_cases():
+ self.light_base._config.items.sleeping_state.value = habapp_rules.system.SleepState.SLEEPING.value if test_case.sleeping else habapp_rules.system.SleepState.AWAKE.value
+ self.light_base._config.items.day.value = "ON" if test_case.day else "OFF"
+ self.light_base.state = test_case.state
+ self.light_base._previous_state = test_case.previous_state
+
+ self.light_base_without_sleep._config.items.day.value = "ON" if test_case.day else "OFF"
+ self.light_base_without_sleep.state = test_case.state
+ self.light_base_without_sleep._previous_state = test_case.previous_state
+
+ self.assertEqual(test_case.expected_value, self.light_base._get_target_brightness(), test_case)
+
+ if test_case.state != "auto_presleep" and test_case.previous_state != "auto_presleep" and not test_case.sleeping:
+ self.assertEqual(test_case.expected_value, self.light_base_without_sleep._get_target_brightness(), test_case)
+
+ # switch on by value
+ for switch_on_value in [20, "INCREASE"]:
+ self.light_base._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", switch_on_value)
+ for test_case in self.get_target_brightness_test_cases():
+ if test_case.state == "auto_on" and test_case.previous_state == "auto_off":
+ self.light_base.state = test_case.state
+ self.light_base._previous_state = test_case.previous_state
+ self.assertIsNone(self.light_base._get_target_brightness())
+
+ def test_auto_off_transitions(self) -> None:
+ """Test transitions of auto_off."""
+ # to auto_on by hand trigger
+ self.light_base.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Light", "ON", "OFF")
+ self.assertEqual("auto_on", self.light_base.state)
+
+ # to leaving (configured)
+ self.light_base.to_auto_off()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_leaving", self.light_base.state)
+
+ # to leaving (NOT configured)
+ self.light_base.to_auto_off()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to pre sleep (configured)
+ self.light_base.to_auto_off()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True), unittest.mock.patch.object(self.config_full.parameter.pre_sleep, "day", BrightnessTimeout(67, 20)):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_presleep", self.light_base.state)
+
+ # to pre sleep (NOT configured)
+ self.light_base.to_auto_off()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ def test_auto_on_transitions(self) -> None:
+ """Test transitions of auto_on."""
+ self.light_base._state_observer._value = 20
+
+ # to auto_off by hand
+ self.light_base.to_auto_on()
+ tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to leaving (configured)
+ self.light_base.to_auto_on()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_leaving", self.light_base.state)
+
+ # to leaving (NOT configured)
+ self.light_base.to_auto_on()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ # to sleeping (configured)
+ self.light_base.to_auto_on()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_presleep", self.light_base.state)
+
+ # to sleeping (NOT configured)
+ self.light_base.to_auto_on()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ def test_auto_pre_off_transitions(self) -> None:
+ """Test transitions of auto_preoff."""
+ event_mock = unittest.mock.MagicMock()
+
+ # to auto off by timeout
+ self.light_base.to_auto_preoff()
+ self.light_base.preoff_timeout()
+ tests.helper.oh_item.item_state_change_event("Unittest_Light", 0.0)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto on by hand_on
+ self.light_base.to_auto_preoff()
+ self.light_base._cb_hand_on(event_mock)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ # to auto on by hand_off
+ self.light_base.to_auto_preoff()
+ self.light_base._cb_hand_off(event_mock)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ # to leaving (configured)
+ self.light_base.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_leaving", self.light_base.state)
+
+ # to leaving (NOT configured)
+ self.light_base.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_base, "_leaving_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_preoff", self.light_base.state)
+
+ # to sleeping (configured)
+ self.light_base.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_presleep", self.light_base.state)
+
+ # to sleeping (NOT configured)
+ self.light_base.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_base, "_pre_sleep_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("auto_preoff", self.light_base.state)
+
+ def test_auto_pre_sleep(self) -> None:
+ """Test transitions of auto_presleep."""
+ # to auto_off by hand_off
+ self.light_base.to_auto_presleep()
+ self.light_base._state_observer._value = 20
+ tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_off by timeout
+ self.light_base.to_auto_presleep()
+ self.light_base.presleep_timeout()
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_off by sleep_aborted | was_on_before = False
+ self.light_base.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_on by sleep_aborted | was_on_before = True
+ self.light_base.to_auto_on()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.SleepState.POST_SLEEPING.value)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ def test_auto_leaving(self) -> None:
+ """Test transitions of auto_presleep."""
+ # to auto_off by hand_off
+ self.light_base.to_auto_leaving()
+ self.light_base._state_observer._value = 20
+ tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_off by timeout
+ self.light_base.to_auto_leaving()
+ self.light_base.leaving_timeout()
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_off by sleep_aborted | was_on_before = False
+ self.light_base.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ # to auto_on by sleep_aborted | was_on_before = True
+ self.light_base.to_auto_on()
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
+ self.assertEqual("auto_on", self.light_base.state)
+
+ def test_auto_restore_state(self) -> None:
+ """Test transitions of auto_restoreState."""
+ self.light_base.to_auto_preoff()
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
+ self.assertEqual("auto_off", self.light_base.state)
+
+ def test_manual(self) -> None:
+ """Test manual switch."""
+ auto_state = self.light_base.states[1]
+ self.assertEqual("auto", auto_state["name"])
+
+ for item_state in (0, 50, "OFF", "ON"):
+ self.light_base._item_light.value = item_state
+ for state_name in [f"auto_{state['name']}" for state in auto_state["children"] if "init" not in state["name"]]:
+ eval(f"self.light_base.to_{state_name}()") # noqa: S307
+ self.assertEqual(state_name, self.light_base.state)
+ tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
+ self.assertEqual("manual", self.light_base.state)
+ tests.helper.oh_item.send_command("Unittest_Manual", "OFF", "ON")
+ if self.light_base._item_light:
+ self.assertEqual("auto_on", self.light_base.state)
+ else:
+ self.assertEqual("auto_off", self.light_base.state)
+
+ def test_cb_day(self) -> None:
+ """Test callback_day."""
+ # ON
+ with unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
+ tests.helper.oh_item.send_command("Unittest_Day", "ON", "OFF")
+ set_timeouts_mock.assert_called_once()
+
+ # OFF
+ with unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock:
+ tests.helper.oh_item.send_command("Unittest_Day", "OFF", "ON")
+ set_timeouts_mock.assert_called_once()
+
+ def test_cb_presence(self) -> None:
+ """Test callback_presence -> only states where nothing should happen."""
+ for state_name in ["presence", "absence", "long_absence"]:
+ with (
+ unittest.mock.patch.object(self.light_base, "leaving_started") as started_mock,
+ unittest.mock.patch.object(self.light_base, "leaving_aborted") as aborted_mock,
+ unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock,
+ ):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState(state_name).value, habapp_rules.system.PresenceState.LEAVING.value)
+ set_timeouts_mock.assert_called_once()
+ started_mock.assert_not_called()
+ aborted_mock.assert_not_called()
+
+ def test_cb_sleeping(self) -> None:
+ """Test callback_presence -> only states where nothing should happen."""
+ for state_name in ["awake", "sleeping", "post_sleeping", "locked"]:
+ with (
+ unittest.mock.patch.object(self.light_base, "sleep_started") as started_mock,
+ unittest.mock.patch.object(self.light_base, "sleep_aborted") as aborted_mock,
+ unittest.mock.patch.object(self.light_base, "_set_timeouts") as set_timeouts_mock,
+ ):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState(state_name).value, habapp_rules.system.SleepState.PRE_SLEEPING.value)
+ set_timeouts_mock.assert_called_once()
+ started_mock.assert_not_called()
+ aborted_mock.assert_not_called()
+
class TestLightSwitch(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Light rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_Dimmer", 0)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_2", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
-
- self.light_parameter = LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None)
- )
-
- self.config_full = LightConfig(
- items=LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_state",
-
- ),
- paramter=self.light_parameter
- )
-
- self.config_without_sleep = LightConfig(
- items=LightItems(
- light="Unittest_Light_2",
- manual="Unittest_Manual_2",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- state="H_Unittest_Light_2_state",
-
- ),
- paramter=self.light_parameter
- )
-
- self.light_switch = habapp_rules.actors.light.LightSwitch(self.config_full)
- self.light_switch_without_sleep = habapp_rules.actors.light.LightSwitch(self.config_without_sleep)
-
- def test_init_with_dimmer(self):
- """Test init with switch_item"""
- config = LightConfig(
- items=LightItems(
- light="Unittest_Light_Dimmer",
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_state",
-
- ),
- paramter=self.light_parameter
- )
-
- with self.assertRaises(TypeError):
- habapp_rules.actors.light.LightSwitch(config)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Light", None)
- tests.helper.oh_item.set_state("Unittest_Manual", None)
- tests.helper.oh_item.set_state("Unittest_Presence_state", None)
- tests.helper.oh_item.set_state("Unittest_Day", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
-
- habapp_rules.actors.light.LightSwitch(self.config_full)
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "manual"},
- {"name": "auto", "initial": "init",
- "children": [
- {"name": "init"},
- {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
- {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
- {"name": "off"},
- {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
- {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
- {"name": "restoreState"}
- ]}]
- self.assertEqual(expected_states, self.light_switch.states)
-
- expected_trans = [
- {"trigger": "manual_on", "source": "auto", "dest": "manual"},
- {"trigger": "manual_off", "source": "manual", "dest": "auto"},
- {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
- {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
- {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
- {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
- {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
- {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
- {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"}
- ]
- self.assertEqual(expected_trans, self.light_switch.trans)
-
- def test_set_light_state(self):
- """Test _set_brightness."""
- TestCase = collections.namedtuple("TestCase", "input_value, output_value")
-
- test_cases = [
- TestCase(None, None),
- TestCase(0, "OFF"),
- TestCase(40, "ON"),
- TestCase(True, "ON"),
- TestCase(False, "OFF")
- ]
-
- for test_case in test_cases:
- with unittest.mock.patch.object(self.light_switch, "_get_target_brightness", return_value=test_case.input_value), unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_command_mock:
- self.light_switch._set_light_state()
- if test_case.output_value is None:
- send_command_mock.assert_not_called()
- else:
- send_command_mock.assert_called_with(test_case.output_value)
-
- # first call after init should not set brightness
- self.light_switch._previous_state = None
- with unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_command_mock:
- self.light_switch._set_light_state()
- send_command_mock.assert_not_called()
-
- def test_update_openhab_state(self):
- """Test _update_openhab_state"""
- states = self._get_state_names(self.light_switch.states)
-
- # test auto_preoff state with timeout <= 60
- self.light_switch.state_machine.set_state("auto_preoff")
- mock_thread_1 = unittest.mock.MagicMock()
- mock_thread_2 = unittest.mock.MagicMock()
- self.light_switch.state_machine.get_state("auto_preoff").timeout = 60
-
- with unittest.mock.patch("threading.Thread", side_effect=[mock_thread_1, mock_thread_2]) as thread_mock:
- self.light_switch._update_openhab_state()
-
- thread_mock.assert_called_once_with(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 0, 1), daemon=True)
- mock_thread_1.start.assert_called_once()
- mock_thread_2.start.assert_not_called()
-
- # test auto_preoff state with timeout > 60
- mock_thread_1 = unittest.mock.MagicMock()
- mock_thread_2 = unittest.mock.MagicMock()
- self.light_switch.state_machine.get_state("auto_preoff").timeout = 61
-
- with unittest.mock.patch("threading.Thread", side_effect=[mock_thread_1, mock_thread_2]) as thread_mock:
- self.light_switch._update_openhab_state()
-
- thread_mock.assert_has_calls([
- unittest.mock.call(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 0, 1), daemon=True),
- unittest.mock.call(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 30.5, 2), daemon=True)
- ])
- mock_thread_1.start.assert_called_once()
- mock_thread_2.start.assert_called_once()
-
- # test all other states
- for state in [state_name for state_name in states if state_name != "auto_preoff"]:
- with unittest.mock.patch("threading.Thread") as thread_mock:
- self.light_switch.state_machine.set_state(state)
- self.light_switch._update_openhab_state()
- thread_mock.assert_not_called()
-
- def test_trigger_warning(self):
- """Test __trigger_warning."""
- TestCase = collections.namedtuple("TestCase", "state_name, wait_time, switch_off_amount, real_state")
- test_cases = [
- TestCase("auto_preoff", 0, 0, "auto_preoff"),
- TestCase("auto_preoff", 0, 1, "auto_preoff"),
- TestCase("auto_preoff", 0, 2, "auto_preoff"),
- TestCase("auto_preoff", 10, 0, "auto_preoff"),
- TestCase("auto_preoff", 10, 1, "auto_preoff"),
- TestCase("auto_preoff", 10, 2, "auto_preoff"),
- TestCase("auto_preoff", 10, 2, "auto_off"),
- ]
-
- with unittest.mock.patch("time.sleep", spec=time.sleep) as sleep_mock, unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_mock:
- for test_case in test_cases:
- sleep_mock.reset_mock()
- send_mock.reset_mock()
-
- self.light_switch.state_machine.set_state(test_case.real_state)
- self.light_switch._LightSwitch__trigger_warning(test_case.state_name, test_case.wait_time, test_case.switch_off_amount)
-
- if test_case.state_name == test_case.real_state:
- sleep_calls = []
- on_off_calls = []
-
- if test_case.wait_time:
- sleep_calls.append(unittest.mock.call(test_case.wait_time))
- for idx in range(test_case.switch_off_amount):
- sleep_calls.append(unittest.mock.call(0.2))
- on_off_calls.append(unittest.mock.call("OFF"))
- on_off_calls.append(unittest.mock.call("ON"))
- if idx + 1 < test_case.switch_off_amount:
- sleep_calls.append(unittest.mock.call(0.5))
-
- sleep_mock.assert_has_calls(sleep_calls)
- send_mock.assert_has_calls(on_off_calls)
- else:
- send_mock.assert_not_called()
-
- # state changes after OFF was sent
- with unittest.mock.patch("time.sleep", spec=time.sleep), \
- unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_mock, \
- unittest.mock.patch.object(self.light_switch, "state") as state_mock:
- state_mock.__ne__.side_effect = [False, True]
-
- self.light_switch._LightSwitch__trigger_warning("auto_preoff", 0, 1)
-
- send_mock.assert_called_once_with("OFF")
-
-
-# pylint: disable=protected-access,no-member,too-many-public-methods
+ """Tests cases for testing Light rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_Dimmer", 0)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_2", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
+
+ self.light_parameter = LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None),
+ )
+
+ self.config_full = LightConfig(
+ items=LightItems(
+ light="Unittest_Light",
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ self.config_without_sleep = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_2",
+ manual="Unittest_Manual_2",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ state="H_Unittest_Light_2_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ self.light_switch = habapp_rules.actors.light.LightSwitch(self.config_full)
+ self.light_switch_without_sleep = habapp_rules.actors.light.LightSwitch(self.config_without_sleep)
+
+ def test_init_with_dimmer(self) -> None:
+ """Test init with switch_item."""
+ config = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_Dimmer",
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ with self.assertRaises(TypeError):
+ habapp_rules.actors.light.LightSwitch(config)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Light", None)
+ tests.helper.oh_item.set_state("Unittest_Manual", None)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", None)
+ tests.helper.oh_item.set_state("Unittest_Day", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
+
+ habapp_rules.actors.light.LightSwitch(self.config_full)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "manual"},
+ {
+ "name": "auto",
+ "initial": "init",
+ "children": [
+ {"name": "init"},
+ {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
+ {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
+ {"name": "off"},
+ {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
+ {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
+ {"name": "restoreState"},
+ ],
+ },
+ ]
+ self.assertEqual(expected_states, self.light_switch.states)
+
+ expected_trans = [
+ {"trigger": "manual_on", "source": "auto", "dest": "manual"},
+ {"trigger": "manual_off", "source": "manual", "dest": "auto"},
+ {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
+ {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
+ {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
+ {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
+ {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
+ {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
+ ]
+ self.assertEqual(expected_trans, self.light_switch.trans)
+
+ def test_set_light_state(self) -> None:
+ """Test _set_brightness."""
+ TestCase = collections.namedtuple("TestCase", "input_value, output_value")
+
+ test_cases = [TestCase(None, None), TestCase(0, "OFF"), TestCase(40, "ON"), TestCase(True, "ON"), TestCase(False, "OFF")]
+
+ for test_case in test_cases:
+ with unittest.mock.patch.object(self.light_switch, "_get_target_brightness", return_value=test_case.input_value), unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_command_mock:
+ self.light_switch._set_light_state()
+ if test_case.output_value is None:
+ send_command_mock.assert_not_called()
+ else:
+ send_command_mock.assert_called_with(test_case.output_value)
+
+ # first call after init should not set brightness
+ self.light_switch._previous_state = None
+ with unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_command_mock:
+ self.light_switch._set_light_state()
+ send_command_mock.assert_not_called()
+
+ def test_update_openhab_state(self) -> None:
+ """Test _update_openhab_state."""
+ states = self._get_state_names(self.light_switch.states)
+
+ # test auto_preoff state with timeout <= 60
+ self.light_switch.state_machine.set_state("auto_preoff")
+ mock_thread_1 = unittest.mock.MagicMock()
+ mock_thread_2 = unittest.mock.MagicMock()
+ self.light_switch.state_machine.get_state("auto_preoff").timeout = 60
+
+ with unittest.mock.patch("threading.Thread", side_effect=[mock_thread_1, mock_thread_2]) as thread_mock:
+ self.light_switch._update_openhab_state()
+
+ thread_mock.assert_called_once_with(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 0, 1), daemon=True)
+ mock_thread_1.start.assert_called_once()
+ mock_thread_2.start.assert_not_called()
+
+ # test auto_preoff state with timeout > 60
+ mock_thread_1 = unittest.mock.MagicMock()
+ mock_thread_2 = unittest.mock.MagicMock()
+ self.light_switch.state_machine.get_state("auto_preoff").timeout = 61
+
+ with unittest.mock.patch("threading.Thread", side_effect=[mock_thread_1, mock_thread_2]) as thread_mock:
+ self.light_switch._update_openhab_state()
+
+ thread_mock.assert_has_calls([
+ unittest.mock.call(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 0, 1), daemon=True),
+ unittest.mock.call(target=self.light_switch._LightSwitch__trigger_warning, args=("auto_preoff", 30.5, 2), daemon=True),
+ ])
+ mock_thread_1.start.assert_called_once()
+ mock_thread_2.start.assert_called_once()
+
+ # test all other states
+ for state in [state_name for state_name in states if state_name != "auto_preoff"]:
+ with unittest.mock.patch("threading.Thread") as thread_mock:
+ self.light_switch.state_machine.set_state(state)
+ self.light_switch._update_openhab_state()
+ thread_mock.assert_not_called()
+
+ def test_trigger_warning(self) -> None:
+ """Test __trigger_warning."""
+ TestCase = collections.namedtuple("TestCase", "state_name, wait_time, switch_off_amount, real_state")
+ test_cases = [
+ TestCase("auto_preoff", 0, 0, "auto_preoff"),
+ TestCase("auto_preoff", 0, 1, "auto_preoff"),
+ TestCase("auto_preoff", 0, 2, "auto_preoff"),
+ TestCase("auto_preoff", 10, 0, "auto_preoff"),
+ TestCase("auto_preoff", 10, 1, "auto_preoff"),
+ TestCase("auto_preoff", 10, 2, "auto_preoff"),
+ TestCase("auto_preoff", 10, 2, "auto_off"),
+ ]
+
+ with unittest.mock.patch("time.sleep", spec=time.sleep) as sleep_mock, unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_mock:
+ for test_case in test_cases:
+ sleep_mock.reset_mock()
+ send_mock.reset_mock()
+
+ self.light_switch.state_machine.set_state(test_case.real_state)
+ self.light_switch._LightSwitch__trigger_warning(test_case.state_name, test_case.wait_time, test_case.switch_off_amount)
+
+ if test_case.state_name == test_case.real_state:
+ sleep_calls = []
+ on_off_calls = []
+
+ if test_case.wait_time:
+ sleep_calls.append(unittest.mock.call(test_case.wait_time))
+ for idx in range(test_case.switch_off_amount):
+ sleep_calls.append(unittest.mock.call(0.2))
+ on_off_calls.extend((unittest.mock.call("OFF"), unittest.mock.call("ON")))
+ if idx + 1 < test_case.switch_off_amount:
+ sleep_calls.append(unittest.mock.call(0.5))
+
+ sleep_mock.assert_has_calls(sleep_calls)
+ send_mock.assert_has_calls(on_off_calls)
+ else:
+ send_mock.assert_not_called()
+
+ # state changes after OFF was sent
+ with unittest.mock.patch("time.sleep", spec=time.sleep), unittest.mock.patch.object(self.light_switch._state_observer, "send_command") as send_mock, unittest.mock.patch.object(self.light_switch, "state") as state_mock:
+ state_mock.__ne__.side_effect = [False, True]
+
+ self.light_switch._LightSwitch__trigger_warning("auto_preoff", 0, 1)
+
+ send_mock.assert_called_once_with("OFF")
+
+
class TestLightDimmer(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Light rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_Switch", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
-
- self.light_parameter = LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None)
- )
-
- self.config_full = LightConfig(
- items=LightItems(
- light="Unittest_Light",
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_state",
-
- ),
- paramter=self.light_parameter
- )
-
- self.config_without_sleep = LightConfig(
- items=LightItems(
- light="Unittest_Light_2",
- manual="Unittest_Manual_2",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- state="H_Unittest_Light_2_state",
-
- ),
- paramter=self.light_parameter
- )
-
- self.light_dimmer = habapp_rules.actors.light.LightDimmer(self.config_full)
- self.light_dimmer_without_sleep = habapp_rules.actors.light.LightDimmer(self.config_without_sleep)
-
- def test_init_with_switch(self):
- """Test init with switch_item"""
- config = LightConfig(
- items=LightItems(
- light="Unittest_Light_Switch",
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_state",
-
- ),
- paramter=self.light_parameter
- )
-
- with self.assertRaises(TypeError):
- habapp_rules.actors.light.LightDimmer(config)
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "manual"},
- {"name": "auto", "initial": "init",
- "children": [
- {"name": "init"},
- {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
- {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
- {"name": "off"},
- {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
- {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
- {"name": "restoreState"}
- ]}]
- self.assertEqual(expected_states, self.light_dimmer.states)
-
- expected_trans = [
- {"trigger": "manual_on", "source": "auto", "dest": "manual"},
- {"trigger": "manual_off", "source": "manual", "dest": "auto"},
- {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
- {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
- {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
- {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
- {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
- {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
- {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
- {"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"}
- ]
- self.assertEqual(expected_trans, self.light_dimmer.trans)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Light", None)
- tests.helper.oh_item.set_state("Unittest_Light_ctr", None)
- tests.helper.oh_item.set_state("Unittest_Manual", None)
- tests.helper.oh_item.set_state("Unittest_Presence_state", None)
- tests.helper.oh_item.set_state("Unittest_Day", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
-
- habapp_rules.actors.light.LightDimmer(self.config_full)
-
- def test_set_light_state(self):
- """Test _set_brightness."""
- TestCase = collections.namedtuple("TestCase", "input_value, output_value")
-
- test_cases = [
- TestCase(None, None),
- TestCase(0, 0),
- TestCase(40, 40),
- TestCase(True, "ON"),
- TestCase(False, "OFF")
- ]
-
- for test_case in test_cases:
- with unittest.mock.patch.object(self.light_dimmer, "_get_target_brightness", return_value=test_case.input_value), unittest.mock.patch.object(self.light_dimmer._state_observer, "send_command") as send_command_mock:
- self.light_dimmer._set_light_state()
- if test_case.output_value is None:
- send_command_mock.assert_not_called()
- else:
- send_command_mock.assert_called_with(test_case.output_value)
-
- # first call after init should not set brightness
- self.light_dimmer._previous_state = None
- with unittest.mock.patch.object(self.light_dimmer._state_observer, "send_command") as send_command_mock:
- self.light_dimmer._set_light_state()
- send_command_mock.assert_not_called()
-
- def test_auto_on_transitions(self):
- """Test transitions of auto_on."""
- # timer is re-triggered by hand_changed if value change > 5
- self.light_dimmer._state_observer._value = 20
- self.light_dimmer.to_auto_on()
- self.light_dimmer.state_machine.states["auto"].states["on"].runner = {} # remove timer
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.send_command("Unittest_Light", 26, 20)
- self.assertEqual("auto_on", self.light_dimmer.state)
- next(iter(self.light_dimmer.state_machine.states["auto"].states["on"].runner.values())).start.assert_called_once() # check if timer was called
-
- # timer is NOT re-triggered by hand_changed if value change <= 5
- self.light_dimmer._state_observer._value = 20
- self.light_dimmer.to_auto_on()
- self.light_dimmer.state_machine.states["auto"].states["on"].runner = {} # remove timer
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.send_command("Unittest_Light", 25, 20)
- self.assertEqual("auto_on", self.light_dimmer.state)
- self.assertTrue(not self.light_dimmer.state_machine.states["auto"].states["on"].runner) # check if timer was NOT called
+ """Tests cases for testing Light rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_Switch", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", True)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", True)
+
+ self.light_parameter = LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 5), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 5)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 4), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 10), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 20), sleeping=None),
+ )
+
+ self.config_full = LightConfig(
+ items=LightItems(
+ light="Unittest_Light",
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ self.config_without_sleep = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_2",
+ manual="Unittest_Manual_2",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ state="H_Unittest_Light_2_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ self.light_dimmer = habapp_rules.actors.light.LightDimmer(self.config_full)
+ self.light_dimmer_without_sleep = habapp_rules.actors.light.LightDimmer(self.config_without_sleep)
+
+ def test_init_with_switch(self) -> None:
+ """Test init with switch_item."""
+ config = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_Switch",
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_state",
+ ),
+ paramter=self.light_parameter,
+ )
+
+ with self.assertRaises(TypeError):
+ habapp_rules.actors.light.LightDimmer(config)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "manual"},
+ {
+ "name": "auto",
+ "initial": "init",
+ "children": [
+ {"name": "init"},
+ {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
+ {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
+ {"name": "off"},
+ {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
+ {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
+ {"name": "restoreState"},
+ ],
+ },
+ ]
+ self.assertEqual(expected_states, self.light_dimmer.states)
+
+ expected_trans = [
+ {"trigger": "manual_on", "source": "auto", "dest": "manual"},
+ {"trigger": "manual_off", "source": "manual", "dest": "auto"},
+ {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
+ {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
+ {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
+ {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
+ {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
+ {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
+ {"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"},
+ ]
+ self.assertEqual(expected_trans, self.light_dimmer.trans)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Light", None)
+ tests.helper.oh_item.set_state("Unittest_Light_ctr", None)
+ tests.helper.oh_item.set_state("Unittest_Manual", None)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", None)
+ tests.helper.oh_item.set_state("Unittest_Day", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
+
+ habapp_rules.actors.light.LightDimmer(self.config_full)
+
+ def test_set_light_state(self) -> None:
+ """Test _set_brightness."""
+ TestCase = collections.namedtuple("TestCase", "input_value, output_value")
+
+ test_cases = [TestCase(None, None), TestCase(0, 0), TestCase(40, 40), TestCase(True, "ON"), TestCase(False, "OFF")]
+
+ for test_case in test_cases:
+ with unittest.mock.patch.object(self.light_dimmer, "_get_target_brightness", return_value=test_case.input_value), unittest.mock.patch.object(self.light_dimmer._state_observer, "send_command") as send_command_mock:
+ self.light_dimmer._set_light_state()
+ if test_case.output_value is None:
+ send_command_mock.assert_not_called()
+ else:
+ send_command_mock.assert_called_with(test_case.output_value)
+
+ # first call after init should not set brightness
+ self.light_dimmer._previous_state = None
+ with unittest.mock.patch.object(self.light_dimmer._state_observer, "send_command") as send_command_mock:
+ self.light_dimmer._set_light_state()
+ send_command_mock.assert_not_called()
+
+ def test_auto_on_transitions(self) -> None:
+ """Test transitions of auto_on."""
+ # timer is re-triggered by hand_changed if value change > 5
+ self.light_dimmer._state_observer._value = 20
+ self.light_dimmer.to_auto_on()
+ self.light_dimmer.state_machine.states["auto"].states["on"].runner = {} # remove timer
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.send_command("Unittest_Light", 26, 20)
+ self.assertEqual("auto_on", self.light_dimmer.state)
+ next(iter(self.light_dimmer.state_machine.states["auto"].states["on"].runner.values())).start.assert_called_once() # check if timer was called
+
+ # timer is NOT re-triggered by hand_changed if value change <= 5
+ self.light_dimmer._state_observer._value = 20
+ self.light_dimmer.to_auto_on()
+ self.light_dimmer.state_machine.states["auto"].states["on"].runner = {} # remove timer
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.send_command("Unittest_Light", 25, 20)
+ self.assertEqual("auto_on", self.light_dimmer.state)
+ self.assertTrue(not self.light_dimmer.state_machine.states["auto"].states["on"].runner) # check if timer was NOT called
class TestLightExtended(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing LightExtended rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_Switch", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "ON")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", "ON")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", "ON")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_1", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_2", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion", "OFF")
-
- self.light_parameter = LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=BrightnessTimeout(20, 6)),
- pre_off=FunctionConfig(day=BrightnessTimeout(50, 7), night=BrightnessTimeout(40, 6), sleeping=BrightnessTimeout(10, 5)),
- leaving=FunctionConfig(day=BrightnessTimeout(False, 4), night=BrightnessTimeout(50, 10), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(30, 7), sleeping=None),
- motion=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=BrightnessTimeout(20, 6)),
- door=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=None)
- )
-
- self.config_full = LightConfig(
- items=LightItems(
- light="Unittest_Light",
- light_control=["Unittest_Light_ctr"],
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- motion="Unittest_Motion",
- doors=["Unittest_Door_1", "Unittest_Door_2"],
- state="CustomState",
- ),
- parameter=self.light_parameter
- )
-
- self.config_without_door_motion = LightConfig(
- items=LightItems(
- light="Unittest_Light_2",
- light_control=["Unittest_Light_2_ctr"],
- manual="Unittest_Manual_2",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- state="H_Unittest_Light_2_state",
- ),
- parameter=self.light_parameter
- )
-
- self.light_extended = habapp_rules.actors.light.LightDimmerExtended(self.config_full)
- self.light_extended_2 = habapp_rules.actors.light.LightDimmerExtended(self.config_without_door_motion)
-
- def test__init__min_config(self):
- """Test __init__ with minimum config."""
- config_min = LightConfig(
- items=LightItems(
- light="Unittest_Light_2",
- light_control=["Unittest_Light_2_ctr"],
- manual="Unittest_Manual_2",
- day="Unittest_Day",
- state="H_Unittest_Light_2_state",
- ),
- parameter=self.light_parameter
- )
-
- habapp_rules.actors.light.LightDimmerExtended(config_min)
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "manual"},
- {"name": "auto", "initial": "init",
- "children": [
- {"name": "init"},
- {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
- {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
- {"name": "off"},
- {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
- {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
- {"name": "restoreState"},
- {"name": "door", "timeout": 999, "on_timeout": "door_timeout"},
- {"name": "motion", "timeout": 999, "on_timeout": "motion_timeout"}
- ]}]
- self.assertEqual(expected_states, self.light_extended.states)
-
- expected_trans = [
- {"trigger": "manual_on", "source": "auto", "dest": "manual"},
- {"trigger": "manual_off", "source": "manual", "dest": "auto"},
- {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
- {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
- {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
- {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
- {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
- {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
- {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
- {"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"},
- {"trigger": "motion_on", "source": "auto_door", "dest": "auto_motion", "conditions": "_motion_configured"},
- {"trigger": "motion_on", "source": "auto_off", "dest": "auto_motion", "conditions": ["_motion_configured", "_motion_door_allowed"]},
- {"trigger": "motion_on", "source": "auto_preoff", "dest": "auto_motion", "conditions": "_motion_configured"},
- {"trigger": "motion_off", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "motion_off", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured", "before": "_log_motion_timeout_warning"},
- {"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured", "before": "_log_motion_timeout_warning"},
- {"trigger": "hand_off", "source": "auto_motion", "dest": "auto_off"},
- {"trigger": "door_opened", "source": ["auto_off", "auto_preoff", "auto_door"], "dest": "auto_door", "conditions": ["_door_configured", "_motion_door_allowed"]},
- {"trigger": "door_timeout", "source": "auto_door", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
- {"trigger": "door_timeout", "source": "auto_door", "dest": "auto_off", "unless": "_pre_off_configured"},
- {"trigger": "door_closed", "source": "auto_leaving", "dest": "auto_off", "conditions": "_door_off_leaving_configured"},
- {"trigger": "hand_off", "source": "auto_door", "dest": "auto_off"},
- {"trigger": "leaving_started", "source": ["auto_motion", "auto_door"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
- {"trigger": "sleep_started", "source": ["auto_motion", "auto_door"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"}
- ]
-
- self.assertEqual(expected_trans, self.light_extended.trans)
- self.assertEqual(expected_trans, self.light_extended_2.trans)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Light", None)
- tests.helper.oh_item.set_state("Unittest_Light_ctr", None)
- tests.helper.oh_item.set_state("Unittest_Manual", None)
- tests.helper.oh_item.set_state("Unittest_Presence_state", None)
- tests.helper.oh_item.set_state("Unittest_Day", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
- tests.helper.oh_item.set_state("Unittest_Motion", None)
- tests.helper.oh_item.set_state("Unittest_Door_1", None)
- tests.helper.oh_item.set_state("Unittest_Door_2", None)
-
- habapp_rules.actors.light.LightDimmerExtended(self.config_full)
-
- def test__init_switch(self):
- """Test init of switch"""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_Switch_state", "")
- config = LightConfig(
- items=LightItems(
- light="Unittest_Light_Switch",
- light_control=["Unittest_Light_ctr"],
- manual="Unittest_Manual",
- presence_state="Unittest_Presence_state",
- day="Unittest_Day",
- sleeping_state="Unittest_Sleep_state",
- motion="Unittest_Motion",
- doors=["Unittest_Door_1", "Unittest_Door_2"],
- state="H_Unittest_Light_Switch_state",
- ),
- parameter=self.light_parameter
- )
-
- light_extended_switch = habapp_rules.actors.light.LightSwitchExtended(config)
-
- self.assertEqual("Unittest_Light_Switch", light_extended_switch._config.items.light.name)
- self.assertEqual("Unittest_Manual", light_extended_switch._config.items.manual.name)
- self.assertEqual("Unittest_Presence_state", light_extended_switch._config.items.presence_state.name)
- self.assertEqual("Unittest_Day", light_extended_switch._config.items.day.name)
- self.assertEqual("Unittest_Sleep_state", light_extended_switch._config.items.sleeping_state.name)
- self.assertEqual("Unittest_Motion", light_extended_switch._config.items.motion.name)
- self.assertEqual(["Unittest_Door_1", "Unittest_Door_2"], [item.name for item in light_extended_switch._config.items.doors])
- self.assertEqual(config, light_extended_switch._config)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "LightExtended_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- light_extended_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=self.light_extended,
- states=self.light_extended.states,
- transitions=self.light_extended.trans,
- initial=self.light_extended.state,
- show_conditions=False)
-
- light_extended_graph.get_graph().draw(picture_dir / "LightExtended.png", format="png", prog="dot")
-
- for state_name in ["auto_door", "auto_motion", "auto_leaving"]:
- light_extended_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.light_extended.states,
- transitions=self.light_extended.trans,
- initial=self.light_extended.state,
- show_conditions=True)
-
- light_extended_graph.set_state(state_name)
- light_extended_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"LightExtended_{state_name}.png", format="png", prog="dot")
-
- def test_get_initial_state(self):
- """Test _get_initial_state."""
- test_cases = TestLightBase.get_initial_state_test_cases()
-
- # no motion
- with (unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended_2, "_pre_sleep_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended_2, "_leaving_configured", return_value=True)):
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
-
- self.assertEqual(test_case.expected_state, self.light_extended._get_initial_state("default"))
- self.assertEqual(test_case.expected_state, self.light_extended_2._get_initial_state("default"))
-
- # motion active
- TestCase = collections.namedtuple("TestCase", "light_value, manual_value, sleep_value, presence_value, expected_state")
- additional_test_cases = [
- TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
- ]
-
- tests.helper.oh_item.set_state("Unittest_Motion", "ON")
- with (unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended_2, "_pre_sleep_configured", return_value=True),
- unittest.mock.patch.object(self.light_extended_2, "_leaving_configured", return_value=True)):
- for test_case in additional_test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
- tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
-
- self.assertEqual("auto_motion", self.light_extended._get_initial_state("default"))
- self.assertEqual("auto_on", self.light_extended_2._get_initial_state("default"))
-
- def test_set_timeouts(self):
- """Test _set_timeouts."""
- TestCase = collections.namedtuple("TestCase", "config, day, sleeping, timeout_on, timeout_pre_off, timeout_leaving, timeout_pre_sleep, timeout_motion, timeout_door")
-
- light_config_max = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 1), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
- motion=FunctionConfig(day=None, night=BrightnessTimeout(40, 20), sleeping=BrightnessTimeout(40, 9)),
- door=FunctionConfig(day=None, night=BrightnessTimeout(10, 21), sleeping=BrightnessTimeout(40, 8)),
- off_at_door_closed_during_leaving=True
- )
- )
-
- light_config_min = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=None,
- leaving=FunctionConfig(day=None, night=None, sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=None, sleeping=None),
- motion=FunctionConfig(day=None, night=None, sleeping=None),
- door=FunctionConfig(day=None, night=None, sleeping=None),
- )
- )
-
- test_cases = [
- TestCase(light_config_max, False, False, 5, 1, 15, 7, 20, 21),
- TestCase(light_config_max, False, True, 2, 0, 0, 0, 9, 8),
- TestCase(light_config_max, True, False, 10, 4, 0, 0, 0, 0),
- TestCase(light_config_max, True, True, 2, 0, 0, 0, 9, 8),
-
- TestCase(light_config_min, False, False, 5, 0, 0, 0, 0, 0),
- TestCase(light_config_min, False, True, 2, 0, 0, 0, 0, 0),
- TestCase(light_config_min, True, False, 10, 0, 0, 0, 0, 0),
- TestCase(light_config_min, True, True, 2, 0, 0, 0, 0, 0),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_extended._config.items.day = HABApp.openhab.items.SwitchItem("day", "ON" if test_case.day else "OFF")
- self.light_extended._config.items.sleeping_state = HABApp.openhab.items.SwitchItem("sleeping", "sleeping" if test_case.sleeping else "awake")
- self.light_extended._config = test_case.config
-
- self.light_extended._set_timeouts()
-
- self.assertEqual(test_case.timeout_on, self.light_extended.state_machine.states["auto"].states["on"].timeout)
- self.assertEqual(test_case.timeout_pre_off, self.light_extended.state_machine.states["auto"].states["preoff"].timeout)
- self.assertEqual(test_case.timeout_leaving, self.light_extended.state_machine.states["auto"].states["leaving"].timeout)
- self.assertEqual(test_case.timeout_pre_sleep, self.light_extended.state_machine.states["auto"].states["presleep"].timeout)
- self.assertEqual(test_case.timeout_motion, self.light_extended.state_machine.states["auto"].states["motion"].timeout)
- self.assertEqual(test_case.timeout_door, self.light_extended.state_machine.states["auto"].states["door"].timeout)
-
- def test_get_target_brightness(self):
- """Test _get_target_brightness."""
- light_config = LightConfig(
- items=self.config_full.items,
- parameter=LightParameter(
- on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
- pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(32, 1), sleeping=None),
- leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
- pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
- motion=FunctionConfig(day=None, night=BrightnessTimeout(40, 20), sleeping=BrightnessTimeout(30, 9)),
- door=FunctionConfig(day=None, night=BrightnessTimeout(20, 21), sleeping=BrightnessTimeout(10, 8))
- )
- )
-
- self.light_extended._config = light_config
- self.light_extended._brightness_before = 42
- self.light_extended._state_observer._value = 100
- self.light_extended._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
-
- # tests for motion and door
- TestCase = collections.namedtuple("TestCase", "state, previous_state, day, sleeping, expected_value")
- test_cases = [
- # ============================== auto motion ==============================
- TestCase("auto_motion", previous_state="auto_off", day=False, sleeping=False, expected_value=40),
- TestCase("auto_motion", previous_state="auto_off", day=False, sleeping=True, expected_value=30),
- TestCase("auto_motion", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
- TestCase("auto_motion", previous_state="auto_off", day=True, sleeping=True, expected_value=30),
-
- TestCase("auto_motion", previous_state="auto_door", day=False, sleeping=False, expected_value=40),
- TestCase("auto_motion", previous_state="auto_door", day=False, sleeping=True, expected_value=30),
- TestCase("auto_motion", previous_state="auto_door", day=True, sleeping=False, expected_value=None),
- TestCase("auto_motion", previous_state="auto_door", day=True, sleeping=True, expected_value=30),
-
- # ============================== auto door ==============================
- TestCase("auto_door", previous_state="auto_off", day=False, sleeping=False, expected_value=20),
- TestCase("auto_door", previous_state="auto_off", day=False, sleeping=True, expected_value=10),
- TestCase("auto_door", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
- TestCase("auto_door", previous_state="auto_off", day=True, sleeping=True, expected_value=10),
- ]
-
- # add test cases from normal light
- test_cases += TestLightBase.get_target_brightness_test_cases()
-
- # No motion and no door
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_extended._config.items.sleeping_state.value = habapp_rules.system.SleepState.SLEEPING.value if test_case.sleeping else habapp_rules.system.SleepState.AWAKE.value
- self.light_extended._config.items.day.value = "ON" if test_case.day else "OFF"
- self.light_extended.state = test_case.state
- self.light_extended._previous_state = test_case.previous_state
-
- self.assertEqual(test_case.expected_value, self.light_extended._get_target_brightness(), test_case)
-
- def test_motion_configured(self):
- """Test _moving_configured."""
- TestCase = collections.namedtuple("TestCase", "motion_item, timeout, result")
- item_motion = HABApp.openhab.items.SwitchItem.get_item("Unittest_Motion")
-
- test_cases = [
- TestCase(None, None, False),
- TestCase(None, 0, False),
- TestCase(None, 1, False),
- TestCase(None, 42, False),
-
- TestCase(item_motion, None, False),
- TestCase(item_motion, 0, False),
- TestCase(item_motion, 1, True),
- TestCase(item_motion, 42, True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_extended._config.items.motion = test_case.motion_item
- self.light_extended._timeout_motion = test_case.timeout
- self.assertEqual(test_case.result, self.light_extended._motion_configured())
-
- def test_door_configured(self):
- """Test _door_configured."""
- TestCase = collections.namedtuple("TestCase", "door_items, timeout, result")
- door_items = [HABApp.openhab.items.ContactItem.get_item("Unittest_Door_1")]
-
- test_cases = [
- TestCase([], None, False),
- TestCase([], 0, False),
- TestCase([], 1, False),
- TestCase([], 42, False),
-
- TestCase(door_items, None, False),
- TestCase(door_items, 0, False),
- TestCase(door_items, 1, True),
- TestCase(door_items, 42, True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.light_extended._config.items.doors = test_case.door_items
- self.light_extended._timeout_door = test_case.timeout
- self.assertEqual(test_case.result, self.light_extended._door_configured())
-
- def test_door_off_leaving_configured(self):
- """Test _door_off_leaving_configured."""
- self.light_extended._config.parameter.off_at_door_closed_during_leaving = True
- self.assertTrue(self.light_extended._door_off_leaving_configured())
-
- self.light_extended._config.parameter.off_at_door_closed_during_leaving = False
- self.assertFalse(self.light_extended._door_off_leaving_configured())
-
- def test_motion_door_allowed(self):
- """Test _motion_door_allowed"""
- with unittest.mock.patch("time.time", return_value=1000), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
- self.assertTrue(self.light_extended._motion_door_allowed())
-
- with unittest.mock.patch("time.time", return_value=121), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
- self.assertTrue(self.light_extended._motion_door_allowed())
-
- with unittest.mock.patch("time.time", return_value=120), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
- self.assertFalse(self.light_extended._motion_door_allowed())
-
- def test_auto_motion(self):
- """Test transitions of auto_motion."""
- # to auto_off by hand_off
- self.light_extended.to_auto_motion()
- self.light_extended._state_observer._value = 20
- tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # to auto_off by timeout (pre off NOT configured)
- self.light_extended.to_auto_motion()
- with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
- self.light_extended.motion_timeout()
- self.assertEqual("auto_off", self.light_extended.state)
-
- # to auto_preoff by timeout (pre off configured)
- self.light_extended.to_auto_motion()
- self.light_extended.motion_timeout()
- self.assertEqual("auto_preoff", self.light_extended.state)
-
- # to auto_off by motion off (pre off NOT configured)
- self.light_extended.to_auto_motion()
- with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Motion", "OFF", "ON")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # to auto_preoff by motion off (pre off configured)
- self.light_extended.to_auto_motion()
- tests.helper.oh_item.send_command("Unittest_Motion", "OFF", "ON")
- self.assertEqual("auto_preoff", self.light_extended.state)
-
- # from auto_off to auto_motion (motion configured) | _motion_door_allowed = True
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_motion", self.light_extended.state)
-
- # from auto_off NOT to auto_motion (motion configured) | _motion_door_allowed = False
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # from auto_off to auto_motion (motion NOT configured)
- self.light_extended.to_auto_off()
- with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # from auto_preoff to auto_motion (motion configured)
- self.light_extended.to_auto_preoff()
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_motion", self.light_extended.state)
-
- # from auto_preoff to auto_motion (motion NOT configured)
- self.light_extended.to_auto_preoff()
- with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_preoff", self.light_extended.state)
-
- # from auto_motion to auto_leaving (leaving configured)
- self.light_extended.to_auto_motion()
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_leaving", self.light_extended.state)
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
- self.assertEqual("auto_motion", self.light_extended.state)
-
- # auto_motion no change at leaving (leaving NOT configured)
- self.light_extended.to_auto_motion()
- with unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_motion", self.light_extended.state)
-
- # from auto_motion to auto_presleep (pre sleep configured)
- self.light_extended.to_auto_motion()
- with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("auto_presleep", self.light_extended.state)
-
- # auto_motion no change at leaving (pre sleep NOT configured)
- self.light_extended.to_auto_motion()
- with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("auto_motion", self.light_extended.state)
-
- def test_auto_door(self):
- """Test transitions of auto_door."""
- # to auto_off by hand_off
- self.light_extended.to_auto_door()
- self.light_extended._state_observer._value = 20
- tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # to auto_preoff by timeout (pre off configured)
- self.light_extended.to_auto_door()
- self.light_extended.door_timeout()
- self.assertEqual("auto_preoff", self.light_extended.state)
-
- # to auto_off by timeout (pre off NOT configured)
- self.light_extended.to_auto_door()
- with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
- self.light_extended.door_timeout()
- self.assertEqual("auto_off", self.light_extended.state)
-
- # to auto_motion by motion (motion configured)
- self.light_extended.to_auto_door()
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_motion", self.light_extended.state)
-
- # no change by motion (motion NOT configured)
- self.light_extended.to_auto_door()
- with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
- self.assertEqual("auto_door", self.light_extended.state)
-
- # auto_off to auto_door by first door (door configured) | _motion_door_allowed = True
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
- self.assertEqual("auto_door", self.light_extended.state)
-
- # auto_off NOT to auto_door by first door (door configured) | _motion_door_allowed = False
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # auto_off to auto_door by second door (door configured) | _motion_door_allowed = True
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Door_2", "OPEN", "CLOSED")
- self.assertEqual("auto_door", self.light_extended.state)
-
- # auto_off NOT to auto_door by second door (door configured) | _motion_door_allowed = False
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
- self.light_extended.to_auto_off()
- tests.helper.oh_item.send_command("Unittest_Door_2", "OPEN", "CLOSED")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # auto_off NOT to auto_door first door (door NOT configured)
- self.light_extended.to_auto_off()
- with unittest.mock.patch.object(self.light_extended, "_door_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # from auto_door to auto_leaving (leaving configured)
- self.light_extended.to_auto_door()
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_leaving", self.light_extended.state)
-
- # auto_door no change at leaving (leaving NOT configured)
- self.light_extended.to_auto_door()
- with unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
- self.assertEqual("auto_door", self.light_extended.state)
-
- # from auto_door to auto_presleep (pre sleep configured)
- self.light_extended.to_auto_door()
- with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("auto_presleep", self.light_extended.state)
-
- # auto_door no change at leaving (pre sleep NOT configured)
- self.light_extended.to_auto_door()
- with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("auto_door", self.light_extended.state)
-
- # auto_preoff to auto_door when door opens
- self.light_extended.to_auto_preoff()
- with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
- self.assertEqual("auto_door", self.light_extended.state)
-
- def test_leaving(self):
- """Test new extended transitions of auto_leaving."""
- # auto_leaving to auto_off by last door (door_off_leaving_configured configured)
- self.light_extended.to_auto_leaving()
- with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", True):
- tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
- self.assertEqual("auto_off", self.light_extended.state)
-
- # auto_leaving no change by last door (off_at_door_closed_during_leaving NOT configured)
- self.light_extended.to_auto_leaving()
- with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", False):
- tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
- self.assertEqual("auto_leaving", self.light_extended.state)
-
- # auto_leaving no change by door closed, but other door open (off_at_door_closed_during_leaving configured)
- self.light_extended.to_auto_leaving()
- tests.helper.oh_item.set_state("Unittest_Door_2", "OPEN")
- with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", True):
- tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
- self.assertEqual("auto_leaving", self.light_extended.state)
+ """Tests cases for testing LightExtended rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Light_Switch", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "ON")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Light_2_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", "ON")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_2_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", "ON")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_1", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door_2", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion", "OFF")
+
+ self.light_parameter = LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=BrightnessTimeout(20, 6)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(50, 7), night=BrightnessTimeout(40, 6), sleeping=BrightnessTimeout(10, 5)),
+ leaving=FunctionConfig(day=BrightnessTimeout(False, 4), night=BrightnessTimeout(50, 10), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(30, 7), sleeping=None),
+ motion=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=BrightnessTimeout(20, 6)),
+ door=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 8), sleeping=None),
+ )
+
+ self.config_full = LightConfig(
+ items=LightItems(
+ light="Unittest_Light",
+ light_control=["Unittest_Light_ctr"],
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ motion="Unittest_Motion",
+ doors=["Unittest_Door_1", "Unittest_Door_2"],
+ state="CustomState",
+ ),
+ parameter=self.light_parameter,
+ )
+
+ self.config_without_door_motion = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_2",
+ light_control=["Unittest_Light_2_ctr"],
+ manual="Unittest_Manual_2",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ state="H_Unittest_Light_2_state",
+ ),
+ parameter=self.light_parameter,
+ )
+
+ self.light_extended = habapp_rules.actors.light.LightDimmerExtended(self.config_full)
+ self.light_extended_2 = habapp_rules.actors.light.LightDimmerExtended(self.config_without_door_motion)
+
+ def test__init__min_config(self) -> None:
+ """Test __init__ with minimum config."""
+ config_min = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_2",
+ light_control=["Unittest_Light_2_ctr"],
+ manual="Unittest_Manual_2",
+ day="Unittest_Day",
+ state="H_Unittest_Light_2_state",
+ ),
+ parameter=self.light_parameter,
+ )
+
+ habapp_rules.actors.light.LightDimmerExtended(config_min)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "manual"},
+ {
+ "name": "auto",
+ "initial": "init",
+ "children": [
+ {"name": "init"},
+ {"name": "on", "timeout": 10, "on_timeout": "auto_on_timeout"},
+ {"name": "preoff", "timeout": 4, "on_timeout": "preoff_timeout"},
+ {"name": "off"},
+ {"name": "leaving", "timeout": 5, "on_timeout": "leaving_timeout"},
+ {"name": "presleep", "timeout": 5, "on_timeout": "presleep_timeout"},
+ {"name": "restoreState"},
+ {"name": "door", "timeout": 999, "on_timeout": "door_timeout"},
+ {"name": "motion", "timeout": 999, "on_timeout": "motion_timeout"},
+ ],
+ },
+ ]
+ self.assertEqual(expected_states, self.light_extended.states)
+
+ expected_trans = [
+ {"trigger": "manual_on", "source": "auto", "dest": "manual"},
+ {"trigger": "manual_off", "source": "manual", "dest": "auto"},
+ {"trigger": "hand_on", "source": ["auto_off", "auto_preoff"], "dest": "auto_on"},
+ {"trigger": "hand_off", "source": ["auto_on", "auto_leaving", "auto_presleep"], "dest": "auto_off"},
+ {"trigger": "hand_off", "source": "auto_preoff", "dest": "auto_on"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "auto_on_timeout", "source": "auto_on", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "preoff_timeout", "source": "auto_preoff", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "leaving_aborted", "source": "auto_leaving", "dest": "auto_restoreState"},
+ {"trigger": "leaving_timeout", "source": "auto_leaving", "dest": "auto_off"},
+ {"trigger": "sleep_started", "source": ["auto_on", "auto_off", "auto_preoff"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ {"trigger": "sleep_aborted", "source": "auto_presleep", "dest": "auto_restoreState"},
+ {"trigger": "presleep_timeout", "source": "auto_presleep", "dest": "auto_off"},
+ {"trigger": "hand_changed", "source": "auto_on", "dest": "auto_on"},
+ {"trigger": "motion_on", "source": "auto_door", "dest": "auto_motion", "conditions": "_motion_configured"},
+ {"trigger": "motion_on", "source": "auto_off", "dest": "auto_motion", "conditions": ["_motion_configured", "_motion_door_allowed"]},
+ {"trigger": "motion_on", "source": "auto_preoff", "dest": "auto_motion", "conditions": "_motion_configured"},
+ {"trigger": "motion_off", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "motion_off", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_preoff", "conditions": "_pre_off_configured", "before": "_log_motion_timeout_warning"},
+ {"trigger": "motion_timeout", "source": "auto_motion", "dest": "auto_off", "unless": "_pre_off_configured", "before": "_log_motion_timeout_warning"},
+ {"trigger": "hand_off", "source": "auto_motion", "dest": "auto_off"},
+ {"trigger": "door_opened", "source": ["auto_off", "auto_preoff", "auto_door"], "dest": "auto_door", "conditions": ["_door_configured", "_motion_door_allowed"]},
+ {"trigger": "door_timeout", "source": "auto_door", "dest": "auto_preoff", "conditions": "_pre_off_configured"},
+ {"trigger": "door_timeout", "source": "auto_door", "dest": "auto_off", "unless": "_pre_off_configured"},
+ {"trigger": "door_closed", "source": "auto_leaving", "dest": "auto_off", "conditions": "_door_off_leaving_configured"},
+ {"trigger": "hand_off", "source": "auto_door", "dest": "auto_off"},
+ {"trigger": "leaving_started", "source": ["auto_motion", "auto_door"], "dest": "auto_leaving", "conditions": "_leaving_configured"},
+ {"trigger": "sleep_started", "source": ["auto_motion", "auto_door"], "dest": "auto_presleep", "conditions": "_pre_sleep_configured"},
+ ]
+
+ self.assertEqual(expected_trans, self.light_extended.trans)
+ self.assertEqual(expected_trans, self.light_extended_2.trans)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Light", None)
+ tests.helper.oh_item.set_state("Unittest_Light_ctr", None)
+ tests.helper.oh_item.set_state("Unittest_Manual", None)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", None)
+ tests.helper.oh_item.set_state("Unittest_Day", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
+ tests.helper.oh_item.set_state("Unittest_Motion", None)
+ tests.helper.oh_item.set_state("Unittest_Door_1", None)
+ tests.helper.oh_item.set_state("Unittest_Door_2", None)
+
+ habapp_rules.actors.light.LightDimmerExtended(self.config_full)
+
+ def test__init_switch(self) -> None:
+ """Test init of switch."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Light_Switch_state", "")
+ config = LightConfig(
+ items=LightItems(
+ light="Unittest_Light_Switch",
+ light_control=["Unittest_Light_ctr"],
+ manual="Unittest_Manual",
+ presence_state="Unittest_Presence_state",
+ day="Unittest_Day",
+ sleeping_state="Unittest_Sleep_state",
+ motion="Unittest_Motion",
+ doors=["Unittest_Door_1", "Unittest_Door_2"],
+ state="H_Unittest_Light_Switch_state",
+ ),
+ parameter=self.light_parameter,
+ )
+
+ light_extended_switch = habapp_rules.actors.light.LightSwitchExtended(config)
+
+ self.assertEqual("Unittest_Light_Switch", light_extended_switch._config.items.light.name)
+ self.assertEqual("Unittest_Manual", light_extended_switch._config.items.manual.name)
+ self.assertEqual("Unittest_Presence_state", light_extended_switch._config.items.presence_state.name)
+ self.assertEqual("Unittest_Day", light_extended_switch._config.items.day.name)
+ self.assertEqual("Unittest_Sleep_state", light_extended_switch._config.items.sleeping_state.name)
+ self.assertEqual("Unittest_Motion", light_extended_switch._config.items.motion.name)
+ self.assertEqual(["Unittest_Door_1", "Unittest_Door_2"], [item.name for item in light_extended_switch._config.items.doors])
+ self.assertEqual(config, light_extended_switch._config)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "LightExtended"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ light_extended_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=self.light_extended, states=self.light_extended.states, transitions=self.light_extended.trans, initial=self.light_extended.state, show_conditions=False)
+
+ light_extended_graph.get_graph().draw(picture_dir / "LightExtended.png", format="png", prog="dot")
+
+ for state_name in ["auto_door", "auto_motion", "auto_leaving"]:
+ light_extended_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self.light_extended.states, transitions=self.light_extended.trans, initial=self.light_extended.state, show_conditions=True
+ )
+
+ light_extended_graph.set_state(state_name)
+ light_extended_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"LightExtended_{state_name}.png", format="png", prog="dot")
+
+ def test_get_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ test_cases = TestLightBase.get_initial_state_test_cases()
+
+ # no motion
+ with (
+ unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended_2, "_pre_sleep_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended_2, "_leaving_configured", return_value=True),
+ ):
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
+
+ self.assertEqual(test_case.expected_state, self.light_extended._get_initial_state("default"))
+ self.assertEqual(test_case.expected_state, self.light_extended_2._get_initial_state("default"))
+
+ # motion active
+ TestCase = collections.namedtuple("TestCase", "light_value, manual_value, sleep_value, presence_value, expected_state")
+ additional_test_cases = [
+ TestCase(42, "OFF", habapp_rules.system.SleepState.AWAKE.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ TestCase(42, "OFF", habapp_rules.system.SleepState.LOCKED.value, habapp_rules.system.PresenceState.PRESENCE.value, "auto_on"),
+ ]
+
+ tests.helper.oh_item.set_state("Unittest_Motion", "ON")
+ with (
+ unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended_2, "_pre_sleep_configured", return_value=True),
+ unittest.mock.patch.object(self.light_extended_2, "_leaving_configured", return_value=True),
+ ):
+ for test_case in additional_test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Light", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Light_2", test_case.light_value)
+ tests.helper.oh_item.set_state("Unittest_Manual_2", test_case.manual_value)
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_value)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_value)
+
+ self.assertEqual("auto_motion", self.light_extended._get_initial_state("default"))
+ self.assertEqual("auto_on", self.light_extended_2._get_initial_state("default"))
+
+ def test_set_timeouts(self) -> None:
+ """Test _set_timeouts."""
+ TestCase = collections.namedtuple("TestCase", "config, day, sleeping, timeout_on, timeout_pre_off, timeout_leaving, timeout_pre_sleep, timeout_motion, timeout_door")
+
+ light_config_max = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(40, 1), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
+ motion=FunctionConfig(day=None, night=BrightnessTimeout(40, 20), sleeping=BrightnessTimeout(40, 9)),
+ door=FunctionConfig(day=None, night=BrightnessTimeout(10, 21), sleeping=BrightnessTimeout(40, 8)),
+ off_at_door_closed_during_leaving=True,
+ ),
+ )
+
+ light_config_min = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=None,
+ leaving=FunctionConfig(day=None, night=None, sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=None, sleeping=None),
+ motion=FunctionConfig(day=None, night=None, sleeping=None),
+ door=FunctionConfig(day=None, night=None, sleeping=None),
+ ),
+ )
+
+ test_cases = [
+ TestCase(light_config_max, False, False, 5, 1, 15, 7, 20, 21),
+ TestCase(light_config_max, False, True, 2, 0, 0, 0, 9, 8),
+ TestCase(light_config_max, True, False, 10, 4, 0, 0, 0, 0),
+ TestCase(light_config_max, True, True, 2, 0, 0, 0, 9, 8),
+ TestCase(light_config_min, False, False, 5, 0, 0, 0, 0, 0),
+ TestCase(light_config_min, False, True, 2, 0, 0, 0, 0, 0),
+ TestCase(light_config_min, True, False, 10, 0, 0, 0, 0, 0),
+ TestCase(light_config_min, True, True, 2, 0, 0, 0, 0, 0),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_extended._config.items.day = HABApp.openhab.items.SwitchItem("day", "ON" if test_case.day else "OFF")
+ self.light_extended._config.items.sleeping_state = HABApp.openhab.items.SwitchItem("sleeping", "sleeping" if test_case.sleeping else "awake")
+ self.light_extended._config = test_case.config
+
+ self.light_extended._set_timeouts()
+
+ self.assertEqual(test_case.timeout_on, self.light_extended.state_machine.states["auto"].states["on"].timeout)
+ self.assertEqual(test_case.timeout_pre_off, self.light_extended.state_machine.states["auto"].states["preoff"].timeout)
+ self.assertEqual(test_case.timeout_leaving, self.light_extended.state_machine.states["auto"].states["leaving"].timeout)
+ self.assertEqual(test_case.timeout_pre_sleep, self.light_extended.state_machine.states["auto"].states["presleep"].timeout)
+ self.assertEqual(test_case.timeout_motion, self.light_extended.state_machine.states["auto"].states["motion"].timeout)
+ self.assertEqual(test_case.timeout_door, self.light_extended.state_machine.states["auto"].states["door"].timeout)
+
+ def test_get_target_brightness(self) -> None:
+ """Test _get_target_brightness."""
+ light_config = LightConfig(
+ items=self.config_full.items,
+ parameter=LightParameter(
+ on=FunctionConfig(day=BrightnessTimeout(True, 10), night=BrightnessTimeout(80, 5), sleeping=BrightnessTimeout(40, 2)),
+ pre_off=FunctionConfig(day=BrightnessTimeout(40, 4), night=BrightnessTimeout(32, 1), sleeping=None),
+ leaving=FunctionConfig(day=None, night=BrightnessTimeout(40, 15), sleeping=None),
+ pre_sleep=FunctionConfig(day=None, night=BrightnessTimeout(10, 7), sleeping=None),
+ motion=FunctionConfig(day=None, night=BrightnessTimeout(40, 20), sleeping=BrightnessTimeout(30, 9)),
+ door=FunctionConfig(day=None, night=BrightnessTimeout(20, 21), sleeping=BrightnessTimeout(10, 8)),
+ ),
+ )
+
+ self.light_extended._config = light_config
+ self.light_extended._brightness_before = 42
+ self.light_extended._state_observer._value = 100
+ self.light_extended._state_observer._last_manual_event = HABApp.openhab.events.ItemCommandEvent("Item_name", "ON")
+
+ # tests for motion and door
+ TestCase = collections.namedtuple("TestCase", "state, previous_state, day, sleeping, expected_value")
+ test_cases = [
+ # ============================== auto motion ==============================
+ TestCase("auto_motion", previous_state="auto_off", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_motion", previous_state="auto_off", day=False, sleeping=True, expected_value=30),
+ TestCase("auto_motion", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_motion", previous_state="auto_off", day=True, sleeping=True, expected_value=30),
+ TestCase("auto_motion", previous_state="auto_door", day=False, sleeping=False, expected_value=40),
+ TestCase("auto_motion", previous_state="auto_door", day=False, sleeping=True, expected_value=30),
+ TestCase("auto_motion", previous_state="auto_door", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_motion", previous_state="auto_door", day=True, sleeping=True, expected_value=30),
+ # ============================== auto door ==============================
+ TestCase("auto_door", previous_state="auto_off", day=False, sleeping=False, expected_value=20),
+ TestCase("auto_door", previous_state="auto_off", day=False, sleeping=True, expected_value=10),
+ TestCase("auto_door", previous_state="auto_off", day=True, sleeping=False, expected_value=None),
+ TestCase("auto_door", previous_state="auto_off", day=True, sleeping=True, expected_value=10),
+ ]
+
+ # add test cases from normal light
+ test_cases += TestLightBase.get_target_brightness_test_cases()
+
+ # No motion and no door
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_extended._config.items.sleeping_state.value = habapp_rules.system.SleepState.SLEEPING.value if test_case.sleeping else habapp_rules.system.SleepState.AWAKE.value
+ self.light_extended._config.items.day.value = "ON" if test_case.day else "OFF"
+ self.light_extended.state = test_case.state
+ self.light_extended._previous_state = test_case.previous_state
+
+ self.assertEqual(test_case.expected_value, self.light_extended._get_target_brightness(), test_case)
+
+ def test_motion_configured(self) -> None:
+ """Test _moving_configured."""
+ TestCase = collections.namedtuple("TestCase", "motion_item, timeout, result")
+ item_motion = HABApp.openhab.items.SwitchItem.get_item("Unittest_Motion")
+
+ test_cases = [
+ TestCase(None, None, False),
+ TestCase(None, 0, False),
+ TestCase(None, 1, False),
+ TestCase(None, 42, False),
+ TestCase(item_motion, None, False),
+ TestCase(item_motion, 0, False),
+ TestCase(item_motion, 1, True),
+ TestCase(item_motion, 42, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_extended._config.items.motion = test_case.motion_item
+ self.light_extended._timeout_motion = test_case.timeout
+ self.assertEqual(test_case.result, self.light_extended._motion_configured())
+
+ def test_door_configured(self) -> None:
+ """Test _door_configured."""
+ TestCase = collections.namedtuple("TestCase", "door_items, timeout, result")
+ door_items = [HABApp.openhab.items.ContactItem.get_item("Unittest_Door_1")]
+
+ test_cases = [
+ TestCase([], None, False),
+ TestCase([], 0, False),
+ TestCase([], 1, False),
+ TestCase([], 42, False),
+ TestCase(door_items, None, False),
+ TestCase(door_items, 0, False),
+ TestCase(door_items, 1, True),
+ TestCase(door_items, 42, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.light_extended._config.items.doors = test_case.door_items
+ self.light_extended._timeout_door = test_case.timeout
+ self.assertEqual(test_case.result, self.light_extended._door_configured())
+
+ def test_door_off_leaving_configured(self) -> None:
+ """Test _door_off_leaving_configured."""
+ self.light_extended._config.parameter.off_at_door_closed_during_leaving = True
+ self.assertTrue(self.light_extended._door_off_leaving_configured())
+
+ self.light_extended._config.parameter.off_at_door_closed_during_leaving = False
+ self.assertFalse(self.light_extended._door_off_leaving_configured())
+
+ def test_motion_door_allowed(self) -> None:
+ """Test _motion_door_allowed."""
+ with unittest.mock.patch("time.time", return_value=1000), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
+ self.assertTrue(self.light_extended._motion_door_allowed())
+
+ with unittest.mock.patch("time.time", return_value=121), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
+ self.assertTrue(self.light_extended._motion_door_allowed())
+
+ with unittest.mock.patch("time.time", return_value=120), unittest.mock.patch.object(self.light_extended, "_hand_off_timestamp", 100):
+ self.assertFalse(self.light_extended._motion_door_allowed())
+
+ def test_auto_motion(self) -> None:
+ """Test transitions of auto_motion."""
+ # to auto_off by hand_off
+ self.light_extended.to_auto_motion()
+ self.light_extended._state_observer._value = 20
+ tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # to auto_off by timeout (pre off NOT configured)
+ self.light_extended.to_auto_motion()
+ with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
+ self.light_extended.motion_timeout()
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # to auto_preoff by timeout (pre off configured)
+ self.light_extended.to_auto_motion()
+ self.light_extended.motion_timeout()
+ self.assertEqual("auto_preoff", self.light_extended.state)
+
+ # to auto_off by motion off (pre off NOT configured)
+ self.light_extended.to_auto_motion()
+ with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Motion", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # to auto_preoff by motion off (pre off configured)
+ self.light_extended.to_auto_motion()
+ tests.helper.oh_item.send_command("Unittest_Motion", "OFF", "ON")
+ self.assertEqual("auto_preoff", self.light_extended.state)
+
+ # from auto_off to auto_motion (motion configured) | _motion_door_allowed = True
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ # from auto_off NOT to auto_motion (motion configured) | _motion_door_allowed = False
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # from auto_off to auto_motion (motion NOT configured)
+ self.light_extended.to_auto_off()
+ with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # from auto_preoff to auto_motion (motion configured)
+ self.light_extended.to_auto_preoff()
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ # from auto_preoff to auto_motion (motion NOT configured)
+ self.light_extended.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_preoff", self.light_extended.state)
+
+ # from auto_motion to auto_leaving (leaving configured)
+ self.light_extended.to_auto_motion()
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_leaving", self.light_extended.state)
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value, habapp_rules.system.PresenceState.LEAVING.value)
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ # auto_motion no change at leaving (leaving NOT configured)
+ self.light_extended.to_auto_motion()
+ with unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ # from auto_motion to auto_presleep (pre sleep configured)
+ self.light_extended.to_auto_motion()
+ with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("auto_presleep", self.light_extended.state)
+
+ # auto_motion no change at leaving (pre sleep NOT configured)
+ self.light_extended.to_auto_motion()
+ with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ def test_auto_door(self) -> None:
+ """Test transitions of auto_door."""
+ # to auto_off by hand_off
+ self.light_extended.to_auto_door()
+ self.light_extended._state_observer._value = 20
+ tests.helper.oh_item.send_command("Unittest_Light", "OFF", "ON")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # to auto_preoff by timeout (pre off configured)
+ self.light_extended.to_auto_door()
+ self.light_extended.door_timeout()
+ self.assertEqual("auto_preoff", self.light_extended.state)
+
+ # to auto_off by timeout (pre off NOT configured)
+ self.light_extended.to_auto_door()
+ with unittest.mock.patch.object(self.light_extended, "_pre_off_configured", return_value=False):
+ self.light_extended.door_timeout()
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # to auto_motion by motion (motion configured)
+ self.light_extended.to_auto_door()
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_motion", self.light_extended.state)
+
+ # no change by motion (motion NOT configured)
+ self.light_extended.to_auto_door()
+ with unittest.mock.patch.object(self.light_extended, "_motion_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Motion", "ON", "OFF")
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ # auto_off to auto_door by first door (door configured) | _motion_door_allowed = True
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ # auto_off NOT to auto_door by first door (door configured) | _motion_door_allowed = False
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # auto_off to auto_door by second door (door configured) | _motion_door_allowed = True
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Door_2", "OPEN", "CLOSED")
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ # auto_off NOT to auto_door by second door (door configured) | _motion_door_allowed = False
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=False):
+ self.light_extended.to_auto_off()
+ tests.helper.oh_item.send_command("Unittest_Door_2", "OPEN", "CLOSED")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # auto_off NOT to auto_door first door (door NOT configured)
+ self.light_extended.to_auto_off()
+ with unittest.mock.patch.object(self.light_extended, "_door_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # from auto_door to auto_leaving (leaving configured)
+ self.light_extended.to_auto_door()
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_leaving", self.light_extended.state)
+
+ # auto_door no change at leaving (leaving NOT configured)
+ self.light_extended.to_auto_door()
+ with unittest.mock.patch.object(self.light_extended, "_leaving_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Presence_state", habapp_rules.system.PresenceState.LEAVING.value, habapp_rules.system.PresenceState.PRESENCE.value)
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ # from auto_door to auto_presleep (pre sleep configured)
+ self.light_extended.to_auto_door()
+ with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("auto_presleep", self.light_extended.state)
+
+ # auto_door no change at leaving (pre sleep NOT configured)
+ self.light_extended.to_auto_door()
+ with unittest.mock.patch.object(self.light_extended, "_pre_sleep_configured", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ # auto_preoff to auto_door when door opens
+ self.light_extended.to_auto_preoff()
+ with unittest.mock.patch.object(self.light_extended, "_motion_door_allowed", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Door_1", "OPEN", "CLOSED")
+ self.assertEqual("auto_door", self.light_extended.state)
+
+ def test_leaving(self) -> None:
+ """Test new extended transitions of auto_leaving."""
+ # auto_leaving to auto_off by last door (door_off_leaving_configured configured)
+ self.light_extended.to_auto_leaving()
+ with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", True):
+ tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
+ self.assertEqual("auto_off", self.light_extended.state)
+
+ # auto_leaving no change by last door (off_at_door_closed_during_leaving NOT configured)
+ self.light_extended.to_auto_leaving()
+ with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", False):
+ tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
+ self.assertEqual("auto_leaving", self.light_extended.state)
+
+ # auto_leaving no change by door closed, but other door open (off_at_door_closed_during_leaving configured)
+ self.light_extended.to_auto_leaving()
+ tests.helper.oh_item.set_state("Unittest_Door_2", "OPEN")
+ with unittest.mock.patch.object(self.light_extended._config.parameter, "off_at_door_closed_during_leaving", True):
+ tests.helper.oh_item.send_command("Unittest_Door_1", "CLOSED", "OPEN")
+ self.assertEqual("auto_leaving", self.light_extended.state)
diff --git a/tests/actors/light_hcl.py b/tests/actors/light_hcl.py
index 0b66b7a..a015b05 100644
--- a/tests/actors/light_hcl.py
+++ b/tests/actors/light_hcl.py
@@ -1,7 +1,7 @@
"""Test light HCL rules."""
+
import collections
import datetime
-import os
import pathlib
import sys
import unittest.mock
@@ -17,409 +17,374 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access, no-member
class TestHclElevation(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests for elevation-based HCL."""
-
- def setUp(self):
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_min", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Color_min_state", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_max", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_max", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Focus_max", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_on_max", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_State_max", None)
-
- self._config_min = habapp_rules.actors.config.light_hcl.HclElevationConfig(
- items=habapp_rules.actors.config.light_hcl.HclElevationItems(
- color="Unittest_Color_min",
- manual="Unittest_Manual_min",
- elevation="Unittest_Elevation",
- state="H_Unittest_Color_min_state"
- ),
- parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(
- color_map=[
- (-10, 3000),
- (-2, 3800),
- (0, 4200.0),
- (10, 5000)
- ]
- )
-
- )
-
- self._config_max = habapp_rules.actors.config.light_hcl.HclElevationConfig(
- items=habapp_rules.actors.config.light_hcl.HclElevationItems(
- color="Unittest_Color_max",
- manual="Unittest_Manual_max",
- elevation="Unittest_Elevation",
- state="H_State_max",
- sleep_state="Unittest_Sleep_state",
- focus="Unittest_Focus_max",
- switch_on="Unittest_Switch_on_max",
- ),
- parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(
- color_map=[
- (-10, 3000),
- (-2, 3800),
- (0, 4200.0),
- (10, 5000)],
-
- hand_timeout=30 * 60,
- sleep_color=3000,
- post_sleep_timeout=500,
- focus_color=7000
- )
- )
-
- self._hcl_elevation_min = habapp_rules.actors.light_hcl.HclElevation(self._config_min)
- self._hcl_elevation_max = habapp_rules.actors.light_hcl.HclElevation(self._config_max)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Light_HCL_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self._hcl_elevation_min.states,
- transitions=self._hcl_elevation_min.trans,
- initial=self._hcl_elevation_min.state,
- show_conditions=True)
-
- graph.get_graph().draw(picture_dir / "HCL_Base.png", format="png", prog="dot")
-
- def test_set_timeouts(self):
- """Test _set_timeouts."""
- # min
- self.assertEqual(18000, self._hcl_elevation_min.state_machine.get_state("Hand").timeout)
- self.assertEqual(1, self._hcl_elevation_min.state_machine.get_state("Auto_Sleep_Post").timeout)
-
- # max
- self.assertEqual(1800, self._hcl_elevation_max.state_machine.get_state("Hand").timeout)
- self.assertEqual(500, self._hcl_elevation_max.state_machine.get_state("Auto_Sleep_Post").timeout)
-
- def test_get_initial_state(self):
- """Test _get_initial_state."""
- TestCase = collections.namedtuple("TestCase", "manual, focus, sleep_state, result_min, result_max")
-
- test_cases = [
- TestCase("OFF", "OFF", habapp_rules.system.SleepState.AWAKE, "Auto_HCL", "Auto_HCL"),
- TestCase("OFF", "OFF", habapp_rules.system.SleepState.PRE_SLEEPING, "Auto_HCL", "Auto_Sleep"),
- TestCase("OFF", "OFF", habapp_rules.system.SleepState.SLEEPING, "Auto_HCL", "Auto_Sleep"),
- TestCase("OFF", "OFF", habapp_rules.system.SleepState.POST_SLEEPING, "Auto_HCL", "Auto_HCL"),
-
- TestCase("OFF", "ON", habapp_rules.system.SleepState.AWAKE, "Auto_HCL", "Auto_Focus"),
- TestCase("OFF", "ON", habapp_rules.system.SleepState.PRE_SLEEPING, "Auto_HCL", "Auto_Sleep"),
- TestCase("OFF", "ON", habapp_rules.system.SleepState.SLEEPING, "Auto_HCL", "Auto_Sleep"),
- TestCase("OFF", "ON", habapp_rules.system.SleepState.POST_SLEEPING, "Auto_HCL", "Auto_Focus"),
-
- TestCase("ON", "OFF", habapp_rules.system.SleepState.AWAKE, "Manual", "Manual"),
- TestCase("ON", "OFF", habapp_rules.system.SleepState.PRE_SLEEPING, "Manual", "Manual"),
- TestCase("ON", "OFF", habapp_rules.system.SleepState.SLEEPING, "Manual", "Manual"),
- TestCase("ON", "OFF", habapp_rules.system.SleepState.POST_SLEEPING, "Manual", "Manual"),
-
- TestCase("ON", "ON", habapp_rules.system.SleepState.AWAKE, "Manual", "Manual"),
- TestCase("ON", "ON", habapp_rules.system.SleepState.PRE_SLEEPING, "Manual", "Manual"),
- TestCase("ON", "ON", habapp_rules.system.SleepState.SLEEPING, "Manual", "Manual"),
- TestCase("ON", "ON", habapp_rules.system.SleepState.POST_SLEEPING, "Manual", "Manual"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Manual_min", test_case.manual)
- tests.helper.oh_item.set_state("Unittest_Manual_max", test_case.manual)
- tests.helper.oh_item.set_state("Unittest_Focus_max", test_case.focus)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_state.value)
-
- self.assertEqual(test_case.result_min, self._hcl_elevation_min._get_initial_state())
- self.assertEqual(test_case.result_max, self._hcl_elevation_max._get_initial_state())
-
- def test_get_hcl_color(self):
- """Test _get_hcl_color."""
-
- TestCase = collections.namedtuple("TestCase", "input, output")
-
- test_cases = [
- TestCase(-20, 3000),
- TestCase(-10.5, 3000),
- TestCase(-10, 3000),
- TestCase(-6, 3400),
- TestCase(-2, 3800),
- TestCase(-1, 4000),
- TestCase(0, 4200),
- TestCase(5, 4600),
- TestCase(10, 5000),
- TestCase(12, 5000),
- TestCase(0.5, 4240),
- TestCase(0.2556, 4220)
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self._hcl_elevation_min._config.items.elevation.value = test_case.input
- self.assertEqual(test_case.output, self._hcl_elevation_min._get_hcl_color())
-
- def test_end_to_end(self):
- """Test end to end behavior."""
- tests.helper.oh_item.assert_value("Unittest_Color_min", None)
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 0)
- tests.helper.oh_item.assert_value("Unittest_Color_min", 4200)
-
- def test_manual(self):
- """Test manual"""
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "ON")
- self.assertEqual("Manual", self._hcl_elevation_min.state)
- self.assertEqual("Manual", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "OFF")
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- def test_hand(self):
- """Test hand detection."""
- tests.helper.oh_item.item_state_change_event("Unittest_Color_min", 1000)
- tests.helper.oh_item.item_state_change_event("Unittest_Color_max", 1000)
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Color_min", 42)
- tests.helper.oh_item.item_state_change_event("Unittest_Color_max", 42)
-
- self.assertEqual("Hand", self._hcl_elevation_min.state)
- self.assertEqual("Hand", self._hcl_elevation_max.state)
-
- def test_focus(self):
- """Test focus."""
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "ON")
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_Focus", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "OFF")
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- def test_sleep(self):
- """Test sleep."""
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value)
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value)
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
- self.assertEqual("Auto_Sleep_Post", self._hcl_elevation_max.state)
-
- self._hcl_elevation_max.post_sleep_timeout()
- self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
-
- # with focus on
- tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "ON")
- self.assertEqual("Auto_Focus", self._hcl_elevation_max.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value)
- self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
- tests.helper.oh_item.assert_value("Unittest_Focus_max", "OFF")
-
- def test_switch_on(self):
- """Test switch on."""
- self._hcl_elevation_max.state = "Manual"
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Color_dimmer_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Switch_on_dimmer", None)
-
- hcl_color_dimmer = habapp_rules.actors.light_hcl.HclElevation(habapp_rules.actors.config.light_hcl.HclElevationConfig(
- items=habapp_rules.actors.config.light_hcl.HclElevationItems(
- color="Unittest_Color_dimmer",
- manual="Unittest_Manual_dimmer",
- elevation="Unittest_Elevation",
- state="Unittest_Color_dimmer_state",
- switch_on="Unittest_Switch_on_dimmer",
- )
- ))
-
- # event value == OFF
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.at") as run_at_mock:
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 0)
- run_at_mock.assert_not_called()
-
- # state is not Auto_HCL
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.at") as run_at_mock:
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 42)
- run_at_mock.assert_not_called()
-
- # target_color is None
- self._hcl_elevation_max.state = "Auto_HCL"
- with (unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.at") as run_at_mock,
- unittest.mock.patch.object(self._hcl_elevation_max, "_get_hcl_color", return_value=None),
- unittest.mock.patch.object(hcl_color_dimmer, "_get_hcl_color", return_value=None)):
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 43)
- run_at_mock.assert_not_called()
-
- # target_color is a valid value
- self._hcl_elevation_max.state = "Auto_HCL"
- with (unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.at") as run_at_mock,
- unittest.mock.patch.object(self._hcl_elevation_max, "_get_hcl_color", return_value=42),
- unittest.mock.patch.object(hcl_color_dimmer, "_get_hcl_color", return_value=44)):
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 80)
-
- run_at_mock.assert_has_calls([
- unittest.mock.call(1, self._hcl_elevation_max._state_observer.send_command, 42),
- unittest.mock.call(1, hcl_color_dimmer._state_observer.send_command, 44),
- ])
-
-
-# pylint: disable=protected-access
+ """Tests for elevation-based HCL."""
+
+ def setUp(self) -> None:
+ """Setup tests."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_min", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Color_min_state", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_max", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_max", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Focus_max", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_on_max", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_State_max", None)
+
+ self._config_min = habapp_rules.actors.config.light_hcl.HclElevationConfig(
+ items=habapp_rules.actors.config.light_hcl.HclElevationItems(color="Unittest_Color_min", manual="Unittest_Manual_min", elevation="Unittest_Elevation", state="H_Unittest_Color_min_state"),
+ parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(color_map=[(-10, 3000), (-2, 3800), (0, 4200.0), (10, 5000)]),
+ )
+
+ self._config_max = habapp_rules.actors.config.light_hcl.HclElevationConfig(
+ items=habapp_rules.actors.config.light_hcl.HclElevationItems(
+ color="Unittest_Color_max",
+ manual="Unittest_Manual_max",
+ elevation="Unittest_Elevation",
+ state="H_State_max",
+ sleep_state="Unittest_Sleep_state",
+ focus="Unittest_Focus_max",
+ switch_on="Unittest_Switch_on_max",
+ ),
+ parameter=habapp_rules.actors.config.light_hcl.HclElevationParameter(color_map=[(-10, 3000), (-2, 3800), (0, 4200.0), (10, 5000)], hand_timeout=30 * 60, sleep_color=3000, post_sleep_timeout=500, focus_color=7000),
+ )
+
+ self._hcl_elevation_min = habapp_rules.actors.light_hcl.HclElevation(self._config_min)
+ self._hcl_elevation_max = habapp_rules.actors.light_hcl.HclElevation(self._config_max)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Light_HCL"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self._hcl_elevation_min.states, transitions=self._hcl_elevation_min.trans, initial=self._hcl_elevation_min.state, show_conditions=True
+ )
+
+ graph.get_graph().draw(picture_dir / "HCL_Base.png", format="png", prog="dot")
+
+ def test_set_timeouts(self) -> None:
+ """Test _set_timeouts."""
+ # min
+ self.assertEqual(18000, self._hcl_elevation_min.state_machine.get_state("Hand").timeout)
+ self.assertEqual(1, self._hcl_elevation_min.state_machine.get_state("Auto_Sleep_Post").timeout)
+
+ # max
+ self.assertEqual(1800, self._hcl_elevation_max.state_machine.get_state("Hand").timeout)
+ self.assertEqual(500, self._hcl_elevation_max.state_machine.get_state("Auto_Sleep_Post").timeout)
+
+ def test_get_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ TestCase = collections.namedtuple("TestCase", "manual, focus, sleep_state, result_min, result_max")
+
+ test_cases = [
+ TestCase("OFF", "OFF", habapp_rules.system.SleepState.AWAKE, "Auto_HCL", "Auto_HCL"),
+ TestCase("OFF", "OFF", habapp_rules.system.SleepState.PRE_SLEEPING, "Auto_HCL", "Auto_Sleep"),
+ TestCase("OFF", "OFF", habapp_rules.system.SleepState.SLEEPING, "Auto_HCL", "Auto_Sleep"),
+ TestCase("OFF", "OFF", habapp_rules.system.SleepState.POST_SLEEPING, "Auto_HCL", "Auto_HCL"),
+ TestCase("OFF", "ON", habapp_rules.system.SleepState.AWAKE, "Auto_HCL", "Auto_Focus"),
+ TestCase("OFF", "ON", habapp_rules.system.SleepState.PRE_SLEEPING, "Auto_HCL", "Auto_Sleep"),
+ TestCase("OFF", "ON", habapp_rules.system.SleepState.SLEEPING, "Auto_HCL", "Auto_Sleep"),
+ TestCase("OFF", "ON", habapp_rules.system.SleepState.POST_SLEEPING, "Auto_HCL", "Auto_Focus"),
+ TestCase("ON", "OFF", habapp_rules.system.SleepState.AWAKE, "Manual", "Manual"),
+ TestCase("ON", "OFF", habapp_rules.system.SleepState.PRE_SLEEPING, "Manual", "Manual"),
+ TestCase("ON", "OFF", habapp_rules.system.SleepState.SLEEPING, "Manual", "Manual"),
+ TestCase("ON", "OFF", habapp_rules.system.SleepState.POST_SLEEPING, "Manual", "Manual"),
+ TestCase("ON", "ON", habapp_rules.system.SleepState.AWAKE, "Manual", "Manual"),
+ TestCase("ON", "ON", habapp_rules.system.SleepState.PRE_SLEEPING, "Manual", "Manual"),
+ TestCase("ON", "ON", habapp_rules.system.SleepState.SLEEPING, "Manual", "Manual"),
+ TestCase("ON", "ON", habapp_rules.system.SleepState.POST_SLEEPING, "Manual", "Manual"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Manual_min", test_case.manual)
+ tests.helper.oh_item.set_state("Unittest_Manual_max", test_case.manual)
+ tests.helper.oh_item.set_state("Unittest_Focus_max", test_case.focus)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_state.value)
+
+ self.assertEqual(test_case.result_min, self._hcl_elevation_min._get_initial_state())
+ self.assertEqual(test_case.result_max, self._hcl_elevation_max._get_initial_state())
+
+ def test_get_hcl_color(self) -> None:
+ """Test _get_hcl_color."""
+ TestCase = collections.namedtuple("TestCase", "input, output")
+
+ test_cases = [
+ TestCase(-20, 3000),
+ TestCase(-10.5, 3000),
+ TestCase(-10, 3000),
+ TestCase(-6, 3400),
+ TestCase(-2, 3800),
+ TestCase(-1, 4000),
+ TestCase(0, 4200),
+ TestCase(5, 4600),
+ TestCase(10, 5000),
+ TestCase(12, 5000),
+ TestCase(0.5, 4240),
+ TestCase(0.2556, 4220),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self._hcl_elevation_min._config.items.elevation.value = test_case.input
+ self.assertEqual(test_case.output, self._hcl_elevation_min._get_hcl_color())
+
+ def test_end_to_end(self) -> None:
+ """Test end to end behavior."""
+ tests.helper.oh_item.assert_value("Unittest_Color_min", None)
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 0)
+ tests.helper.oh_item.assert_value("Unittest_Color_min", 4200)
+
+ def test_manual(self) -> None:
+ """Test manual."""
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "ON")
+ self.assertEqual("Manual", self._hcl_elevation_min.state)
+ self.assertEqual("Manual", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "OFF")
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ def test_hand(self) -> None:
+ """Test hand detection."""
+ tests.helper.oh_item.item_state_change_event("Unittest_Color_min", 1000)
+ tests.helper.oh_item.item_state_change_event("Unittest_Color_max", 1000)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Color_min", 42)
+ tests.helper.oh_item.item_state_change_event("Unittest_Color_max", 42)
+
+ self.assertEqual("Hand", self._hcl_elevation_min.state)
+ self.assertEqual("Hand", self._hcl_elevation_max.state)
+
+ def test_focus(self) -> None:
+ """Test focus."""
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "ON")
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_Focus", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "OFF")
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ def test_sleep(self) -> None:
+ """Test sleep."""
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_HCL", self._hcl_elevation_min.state)
+ self.assertEqual("Auto_Sleep_Post", self._hcl_elevation_max.state)
+
+ self._hcl_elevation_max.post_sleep_timeout()
+ self.assertEqual("Auto_HCL", self._hcl_elevation_max.state)
+
+ # with focus on
+ tests.helper.oh_item.item_state_change_event("Unittest_Focus_max", "ON")
+ self.assertEqual("Auto_Focus", self._hcl_elevation_max.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value)
+ self.assertEqual("Auto_Sleep_Active", self._hcl_elevation_max.state)
+ tests.helper.oh_item.assert_value("Unittest_Focus_max", "OFF")
+
+ def test_switch_on(self) -> None:
+ """Test switch on."""
+ self._hcl_elevation_max.state = "Manual"
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Color_dimmer_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Switch_on_dimmer", None)
+
+ hcl_color_dimmer = habapp_rules.actors.light_hcl.HclElevation(
+ habapp_rules.actors.config.light_hcl.HclElevationConfig(
+ items=habapp_rules.actors.config.light_hcl.HclElevationItems(
+ color="Unittest_Color_dimmer",
+ manual="Unittest_Manual_dimmer",
+ elevation="Unittest_Elevation",
+ state="Unittest_Color_dimmer_state",
+ switch_on="Unittest_Switch_on_dimmer",
+ )
+ )
+ )
+
+ # event value == OFF
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.once") as run_soon_mock:
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 0)
+ run_soon_mock.assert_not_called()
+
+ # state is not Auto_HCL
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.once") as run_soon_mock:
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 42)
+ run_soon_mock.assert_not_called()
+
+ # target_color is None
+ self._hcl_elevation_max.state = "Auto_HCL"
+ with (
+ unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.once") as run_soon_mock,
+ unittest.mock.patch.object(self._hcl_elevation_max, "_get_hcl_color", return_value=None),
+ unittest.mock.patch.object(hcl_color_dimmer, "_get_hcl_color", return_value=None),
+ ):
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 43)
+ run_soon_mock.assert_not_called()
+
+ # target_color is a valid value
+ self._hcl_elevation_max.state = "Auto_HCL"
+ with (
+ unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.once") as run_soon_mock,
+ unittest.mock.patch.object(self._hcl_elevation_max, "_get_hcl_color", return_value=42),
+ unittest.mock.patch.object(hcl_color_dimmer, "_get_hcl_color", return_value=44),
+ ):
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_max", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch_on_dimmer", 80)
+
+ run_soon_mock.assert_has_calls([
+ unittest.mock.call(1, self._hcl_elevation_max._state_observer.send_command, 42),
+ unittest.mock.call(1, hcl_color_dimmer._state_observer.send_command, 44),
+ ])
+
+
class TestHclTime(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests for time-based HCL."""
-
- def setUp(self):
- tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_min", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Color_min_state", None)
-
- self._config = habapp_rules.actors.config.light_hcl.HclTimeConfig(
- items=habapp_rules.actors.config.light_hcl.HclTimeItems(
- color=HABApp.openhab.items.NumberItem("Unittest_Color_min"),
- manual=HABApp.openhab.items.SwitchItem("Unittest_Manual_min"),
- state=HABApp.openhab.items.StringItem("H_Unittest_Color_min_state"),
- ),
- parameter=habapp_rules.actors.config.light_hcl.HclTimeParameter(
- color_map=[
- (2, 3000),
- (8, 4000),
- (12, 9000),
- (17, 9000),
- (20, 4000)
- ],
- )
- )
-
- self._rule = habapp_rules.actors.light_hcl.HclTime(self._config)
-
- def test_one_hour_later(self):
- """Test _one_hour_later."""
- TestCase = collections.namedtuple("TestCase", "configured, time, today_weekend_holiday, tomorrow_weekend_holiday, expected_result")
-
- test_cases = [
- # not configured -> always false
- TestCase(False, datetime.datetime(2023, 12, 19, 12), False, False, False),
-
- # 12:00 -> always false
- TestCase(True, datetime.datetime(2023, 12, 19, 12), False, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 12), False, True, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 12), True, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 12), True, True, False),
-
- # 13:00 -> true if next day is a free day
- TestCase(True, datetime.datetime(2023, 12, 19, 13), False, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 13), False, True, True),
- TestCase(True, datetime.datetime(2023, 12, 19, 13), True, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 13), True, True, True),
-
- # 4:00 -> true if today is a free day
- TestCase(True, datetime.datetime(2023, 12, 19, 4), False, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 4), False, True, True),
- TestCase(True, datetime.datetime(2023, 12, 19, 4), True, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 4), True, True, True),
-
- # 5:00 -> always false
- TestCase(True, datetime.datetime(2023, 12, 19, 5), False, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 5), False, True, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 5), True, False, False),
- TestCase(True, datetime.datetime(2023, 12, 19, 5), True, True, False),
- ]
-
- with unittest.mock.patch("habapp_rules.core.type_of_day.is_holiday") as is_holiday_mock, unittest.mock.patch("habapp_rules.core.type_of_day.is_weekend") as is_weekend_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- # test holiday
- is_holiday_mock.side_effect = [test_case.tomorrow_weekend_holiday, test_case.today_weekend_holiday]
- is_weekend_mock.side_effect = [False, False]
- self._rule._config.parameter.shift_weekend_holiday = test_case.configured
-
- self.assertEqual(test_case.expected_result, self._rule._one_hour_later(test_case.time))
-
- # test weekend
- is_holiday_mock.side_effect = [False, False]
- is_weekend_mock.side_effect = [test_case.tomorrow_weekend_holiday, test_case.today_weekend_holiday]
- self._rule._config.parameter.shift_weekend_holiday = test_case.configured
-
- self.assertEqual(test_case.expected_result, self._rule._one_hour_later(test_case.time))
-
- def test_get_hcl_color(self):
- """Test _get_hcl_color."""
- # test without color value as attribute
- TestCase = collections.namedtuple("TestCase", "test_time, output")
-
- test_cases = [
- TestCase(datetime.datetime(2023, 1, 1, 0, 0), 3333),
- TestCase(datetime.datetime(2023, 1, 1, 1, 0), 3167),
- TestCase(datetime.datetime(2023, 1, 1, 2, 0), 3000),
- TestCase(datetime.datetime(2023, 1, 1, 3, 30), 3250),
- TestCase(datetime.datetime(2023, 1, 1, 5, 0), 3500),
- TestCase(datetime.datetime(2023, 1, 1, 8, 0), 4000),
- TestCase(datetime.datetime(2023, 1, 1, 9, 0), 5250),
- TestCase(datetime.datetime(2023, 1, 1, 12, 0), 9000),
- TestCase(datetime.datetime(2023, 1, 1, 12, 10), 9000),
- TestCase(datetime.datetime(2023, 1, 1, 20, 0), 4000),
- TestCase(datetime.datetime(2023, 1, 1, 22, 0), 3667),
- TestCase(datetime.datetime(2023, 1, 1, 22, 12), 3633),
- ]
-
- with unittest.mock.patch("datetime.datetime") as datetime_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- datetime_mock.now.return_value = test_case.test_time
- self.assertEqual(test_case.output, round(self._rule._get_hcl_color()))
-
- # test one hour later
- test_time = datetime.datetime(2023, 1, 1, 21, 0)
- with unittest.mock.patch.object(self._rule, "_one_hour_later", return_value=True), unittest.mock.patch("datetime.datetime") as datetime_mock:
- datetime_mock.now.return_value = test_time
- self.assertEqual(4000, round(self._rule._get_hcl_color()))
-
- def test_update_color(self):
- """Test _update_color."""
- with unittest.mock.patch.object(self._rule, "_get_hcl_color", return_value=42):
- self._rule._update_color()
- tests.helper.oh_item.assert_value("Unittest_Color_min", 42)
-
- # state is not Auto_HCL:
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON")
- with unittest.mock.patch.object(self._rule, "_get_hcl_color", return_value=123):
- self._rule._update_color()
- tests.helper.oh_item.assert_value("Unittest_Color_min", 42)
+ """Tests for time-based HCL."""
+
+ def setUp(self) -> None:
+ """Set up tests."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Color_min", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Color_min_state", None)
+
+ self._config = habapp_rules.actors.config.light_hcl.HclTimeConfig(
+ items=habapp_rules.actors.config.light_hcl.HclTimeItems(
+ color=HABApp.openhab.items.NumberItem("Unittest_Color_min"),
+ manual=HABApp.openhab.items.SwitchItem("Unittest_Manual_min"),
+ state=HABApp.openhab.items.StringItem("H_Unittest_Color_min_state"),
+ ),
+ parameter=habapp_rules.actors.config.light_hcl.HclTimeParameter(
+ color_map=[(2, 3000), (8, 4000), (12, 9000), (17, 9000), (20, 4000)],
+ ),
+ )
+
+ self._rule = habapp_rules.actors.light_hcl.HclTime(self._config)
+
+ def test_one_hour_later(self) -> None:
+ """Test _one_hour_later."""
+ TestCase = collections.namedtuple("TestCase", "configured, time, today_weekend_holiday, tomorrow_weekend_holiday, expected_result")
+
+ test_cases = [
+ # not configured -> always false
+ TestCase(False, datetime.datetime(2023, 12, 19, 12), False, False, False),
+ # 12:00 -> always false
+ TestCase(True, datetime.datetime(2023, 12, 19, 12), False, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 12), False, True, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 12), True, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 12), True, True, False),
+ # 13:00 -> true if next day is a free day
+ TestCase(True, datetime.datetime(2023, 12, 19, 13), False, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 13), False, True, True),
+ TestCase(True, datetime.datetime(2023, 12, 19, 13), True, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 13), True, True, True),
+ # 4:00 -> true if today is a free day
+ TestCase(True, datetime.datetime(2023, 12, 19, 4), False, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 4), False, True, True),
+ TestCase(True, datetime.datetime(2023, 12, 19, 4), True, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 4), True, True, True),
+ # 5:00 -> always false
+ TestCase(True, datetime.datetime(2023, 12, 19, 5), False, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 5), False, True, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 5), True, False, False),
+ TestCase(True, datetime.datetime(2023, 12, 19, 5), True, True, False),
+ ]
+
+ with unittest.mock.patch("habapp_rules.core.type_of_day.is_holiday") as is_holiday_mock, unittest.mock.patch("habapp_rules.core.type_of_day.is_weekend") as is_weekend_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ # test holiday
+ is_holiday_mock.side_effect = [test_case.tomorrow_weekend_holiday, test_case.today_weekend_holiday]
+ is_weekend_mock.side_effect = [False, False]
+ self._rule._config.parameter.shift_weekend_holiday = test_case.configured
+
+ self.assertEqual(test_case.expected_result, self._rule._one_hour_later(test_case.time))
+
+ # test weekend
+ is_holiday_mock.side_effect = [False, False]
+ is_weekend_mock.side_effect = [test_case.tomorrow_weekend_holiday, test_case.today_weekend_holiday]
+ self._rule._config.parameter.shift_weekend_holiday = test_case.configured
+
+ self.assertEqual(test_case.expected_result, self._rule._one_hour_later(test_case.time))
+
+ def test_get_hcl_color(self) -> None:
+ """Test _get_hcl_color."""
+ # test without color value as attribute
+ TestCase = collections.namedtuple("TestCase", "test_time, output")
+
+ test_cases = [
+ TestCase(datetime.datetime(2023, 1, 1, 0, 0), 3333),
+ TestCase(datetime.datetime(2023, 1, 1, 1, 0), 3167),
+ TestCase(datetime.datetime(2023, 1, 1, 2, 0), 3000),
+ TestCase(datetime.datetime(2023, 1, 1, 3, 30), 3250),
+ TestCase(datetime.datetime(2023, 1, 1, 5, 0), 3500),
+ TestCase(datetime.datetime(2023, 1, 1, 8, 0), 4000),
+ TestCase(datetime.datetime(2023, 1, 1, 9, 0), 5250),
+ TestCase(datetime.datetime(2023, 1, 1, 12, 0), 9000),
+ TestCase(datetime.datetime(2023, 1, 1, 12, 10), 9000),
+ TestCase(datetime.datetime(2023, 1, 1, 20, 0), 4000),
+ TestCase(datetime.datetime(2023, 1, 1, 22, 0), 3667),
+ TestCase(datetime.datetime(2023, 1, 1, 22, 12), 3633),
+ ]
+
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ datetime_mock.now.return_value = test_case.test_time
+ self.assertEqual(test_case.output, round(self._rule._get_hcl_color()))
+
+ # test one hour later
+ test_time = datetime.datetime(2023, 1, 1, 21, 0)
+ with unittest.mock.patch.object(self._rule, "_one_hour_later", return_value=True), unittest.mock.patch("datetime.datetime") as datetime_mock:
+ datetime_mock.now.return_value = test_time
+ self.assertEqual(4000, round(self._rule._get_hcl_color()))
+
+ def test_update_color(self) -> None:
+ """Test _update_color."""
+ with unittest.mock.patch.object(self._rule, "_get_hcl_color", return_value=42):
+ self._rule._update_color()
+ tests.helper.oh_item.assert_value("Unittest_Color_min", 42)
+
+ # state is not Auto_HCL:
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON")
+ with unittest.mock.patch.object(self._rule, "_get_hcl_color", return_value=123):
+ self._rule._update_color()
+ tests.helper.oh_item.assert_value("Unittest_Color_min", 42)
diff --git a/tests/actors/shading.py b/tests/actors/shading.py
index 3c08509..fcbc450 100644
--- a/tests/actors/shading.py
+++ b/tests/actors/shading.py
@@ -1,7 +1,7 @@
"""Test shading rules."""
+
import collections
import copy
-import os
import pathlib
import sys
import time
@@ -25,1195 +25,1110 @@
import tests.helper.timer
-# pylint: disable=protected-access,no-member,too-many-public-methods, too-many-lines
class TestShadingBase(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing _ShadingBase."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading_min", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Shading_max", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_max", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_min_state", "")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
-
- config_min = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading_min",
- manual="Unittest_Manual_min",
- state="H_Unittest_Shading_min_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- config_max = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading_max",
- manual="Unittest_Manual_max",
- wind_alarm="Unittest_WindAlarm",
- sun_protection="Unittest_SunProtection",
- sleeping_state="Unittest_Sleep_state",
- night="Unittest_Night",
- door="Unittest_Door",
- summer="Unittest_Summer",
- hand_manual_is_active_feedback="Unittest_Hand_Manual_active",
- state="CustomState"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- self.shading_min = habapp_rules.actors.shading._ShadingBase(config_min)
- self.shading_max = habapp_rules.actors.shading._ShadingBase(config_max)
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "WindAlarm"},
- {"name": "Manual"},
- {"name": "Hand", "on_timeout": "_auto_hand_timeout", "timeout": 72000},
- {"name": "Auto", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "Open"},
- {"name": "DoorOpen", "initial": "Open", "children": [
- {"name": "Open"},
- {"name": "PostOpen", "on_timeout": "_timeout_post_door_open", "timeout": 300}
- ]},
- {"name": "NightClose"},
- {"name": "SleepingClose"},
- {"name": "SunProtection"}
- ]}
- ]
- self.assertEqual(expected_states, self.shading_max.states)
-
- expected_trans = [
- {"trigger": "_wind_alarm_on", "source": ["Auto", "Hand", "Manual"], "dest": "WindAlarm"},
- {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Manual", "conditions": "_manual_active"},
- {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Auto", "unless": "_manual_active"},
-
- # manual
- {"trigger": "_manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
- {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
-
- # hand
- {"trigger": "_hand_command", "source": ["Auto"], "dest": "Hand"},
- {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
-
- # sun
- {"trigger": "_sun_on", "source": "Auto_Open", "dest": "Auto_SunProtection"},
- {"trigger": "_sun_off", "source": "Auto_SunProtection", "dest": "Auto_Open"},
-
- # sleep
- {"trigger": "_sleep_started", "source": ["Auto_Open", "Auto_NightClose", "Auto_SunProtection", "Auto_DoorOpen"], "dest": "Auto_SleepingClose"},
- {"trigger": "_sleep_started", "source": "Hand", "dest": "Auto"},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_NightClose", "conditions": ["_night_active_and_configured"]},
- {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_Open", "unless": ["_night_active_and_configured", "_sun_protection_active_and_configured"]},
-
- # door
- {"trigger": "_door_open", "source": ["Auto_NightClose", "Auto_SunProtection", "Auto_SleepingClose", "Auto_Open"], "dest": "Auto_DoorOpen"},
- {"trigger": "_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_DoorOpen_Open"},
- {"trigger": "_door_closed", "source": "Auto_DoorOpen_Open", "dest": "Auto_DoorOpen_PostOpen"},
- {"trigger": "_timeout_post_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_Init"},
-
- # night close
- {"trigger": "_night_started", "source": ["Auto_Open", "Auto_SunProtection"], "dest": "Auto_NightClose", "conditions": "_night_active_and_configured"},
- {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
- {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_Open", "unless": ["_sun_protection_active_and_configured"]}
- ]
-
- self.assertEqual(expected_trans, self.shading_max.trans)
-
- self.assertEqual(300, self.shading_max.state_machine.states["Auto"].states["DoorOpen"].states["PostOpen"].timeout)
- self.assertEqual(86400, self.shading_max.state_machine.states["Manual"].timeout)
-
- def test_init_exceptions(self):
- """Test exceptions of __init__."""
- TestCase = collections.namedtuple("TestCase", "item_type, raises_exc")
-
- test_cases = [
- TestCase(HABApp.openhab.items.RollershutterItem, False),
- TestCase(HABApp.openhab.items.DimmerItem, False),
- TestCase(HABApp.openhab.items.SwitchItem, True),
- TestCase(HABApp.openhab.items.ContactItem, True),
- TestCase(HABApp.openhab.items.NumberItem, True)
- ]
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Temp_state", "")
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.add_mock_item(test_case.item_type, "Unittest_Temp", None)
-
- if test_case.raises_exc:
- with self.assertRaises(pydantic_core.ValidationError):
- habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Temp",
- manual="Unittest_Manual_min",
- state="H_Unittest_Shading_min_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- else:
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Temp",
- manual="Unittest_Manual_min",
- state="H_Unittest_Shading_min_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- habapp_rules.actors.shading._ShadingBase(config)
- tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Temp")
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Shading_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- jal_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.shading_min.states,
- transitions=self.shading_min.trans,
- initial=self.shading_min.state,
- show_conditions=False)
-
- jal_graph.get_graph().draw(picture_dir / "Shading.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self.shading_min.states) if state not in ["auto_init"]]:
- jal_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.shading_min.states,
- transitions=self.shading_min.trans,
- initial=state_name,
- show_conditions=True)
- jal_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Shading_{state_name}.png", format="png", prog="dot")
-
- @staticmethod
- def get_initial_state_test_cases() -> list[collections.namedtuple]:
- """Get test cases for initial state tests
-
- :return: tests cases
- """
- TestCase = collections.namedtuple("TestCase", "wind_alarm, manual, sleeping_state, door, night, sun_protection, expected_state")
-
- return [
- # wind_alarm = OFF | manual = OFF
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Auto_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Auto_SunProtection"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Auto_NightClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="Auto_NightClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
-
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Auto_SleepingClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Auto_SleepingClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Auto_SleepingClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="Auto_SleepingClose"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
- TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
-
- # wind_alarm = OFF | manual = ON
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="Manual"),
-
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="Manual"),
- TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="Manual"),
-
- # wind_alarm = ON | manual = OFF
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
-
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
-
- # wind_alarm = ON | manual = ON
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
-
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
- TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
- ]
-
- def test_get_initial_state(self):
- """Test _get_initial_state."""
- for test_case in self.get_initial_state_test_cases():
- tests.helper.oh_item.set_state("Unittest_WindAlarm", test_case.wind_alarm)
- tests.helper.oh_item.set_state("Unittest_Manual_max", test_case.manual)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleeping_state.value)
- tests.helper.oh_item.set_state("Unittest_Door", test_case.door)
- tests.helper.oh_item.set_state("Unittest_Night", test_case.night)
- tests.helper.oh_item.set_state("Unittest_SunProtection", test_case.sun_protection)
-
- self.assertEqual(test_case.expected_state, self.shading_max._get_initial_state())
-
- @staticmethod
- def get_target_positions_test_cases() -> list[collections.namedtuple]:
- """Get test cases for target position
-
- :return: tests cases
- """
- TestCase = collections.namedtuple("TestCase", "state, target_pos")
-
- return [
- TestCase("Hand", None),
- TestCase("Manual", None),
- TestCase("WindAlarm", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
- TestCase("Auto_Open", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
- TestCase("Auto_SunProtection", habapp_rules.actors.config.shading.ShadingPosition(100, None)),
- TestCase("Auto_SleepingClose", habapp_rules.actors.config.shading.ShadingPosition(100, 100)),
- TestCase("Auto_NightClose", habapp_rules.actors.config.shading.ShadingPosition(100, 100)),
- TestCase("Auto_DoorOpen_Open", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
- TestCase("Auto_DoorOpen_PostOpen", None),
- ]
-
- def test_get_target_position(self):
- """Test _get_target_position."""
- for test_case in self.get_target_positions_test_cases():
- with self.subTest(test_case=test_case):
- self.shading_max._set_state(test_case.state)
- self.assertEqual(test_case.target_pos, self.shading_max._get_target_position())
-
- tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
- self.shading_max._set_state("Auto_NightClose")
- self.assertEqual(None, self.shading_max._get_target_position())
-
- def test_get_target_position_sleeping(self):
- """Test get_target_position if sleeping is active"""
- config_night = habapp_rules.actors.config.shading.ShadingPosition(20, 30)
- config_day = habapp_rules.actors.config.shading.ShadingPosition(40, 50)
-
- # item night is not None
- with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day):
- self.shading_max.state = "Auto_SleepingClose"
- tests.helper.oh_item.set_state("Unittest_Night", "ON")
- self.assertEqual(config_night, self.shading_max._get_target_position())
-
- tests.helper.oh_item.set_state("Unittest_Night", "OFF")
- self.assertEqual(config_day, self.shading_max._get_target_position())
-
- # item night is None
- with (unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night),
- unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day),
- unittest.mock.patch.object(self.shading_max._config.items, "night", None)):
- self.shading_max.state = "Auto_SleepingClose"
- tests.helper.oh_item.set_state("Unittest_Night", "ON")
- self.assertEqual(config_night, self.shading_max._get_target_position())
-
- tests.helper.oh_item.set_state("Unittest_Night", "OFF")
- self.assertEqual(config_night, self.shading_max._get_target_position())
-
- def test_cb_sleep_state(self):
- """Test _cb_sleep_state"""
- TestCase = collections.namedtuple("TestCase", "sleep_state, started_triggered, stopped_triggered")
-
- test_cases = [
- TestCase(habapp_rules.system.SleepState.AWAKE, False, False),
- TestCase(habapp_rules.system.SleepState.PRE_SLEEPING, True, False),
- TestCase(habapp_rules.system.SleepState.SLEEPING, False, False),
- TestCase(habapp_rules.system.SleepState.POST_SLEEPING, False, True),
- TestCase(habapp_rules.system.SleepState.LOCKED, False, False),
- ]
-
- with unittest.mock.patch.object(self.shading_max, "_sleep_started") as started_mock, unittest.mock.patch.object(self.shading_max, "_sleep_stopped") as stopped_mock:
- for test_case in test_cases:
- started_mock.reset_mock()
- stopped_mock.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleep_state.value)
-
- if test_case.started_triggered:
- started_mock.assert_called_once()
- else:
- started_mock.assert_not_called()
-
- if test_case.stopped_triggered:
- stopped_mock.assert_called_once()
- else:
- stopped_mock.assert_not_called()
-
- def test_cb_night(self):
- """Test _cb_night"""
- config_night = habapp_rules.actors.config.shading.ShadingPosition(20, 30)
- config_day = habapp_rules.actors.config.shading.ShadingPosition(40, 50)
-
- self.shading_max.state = "Auto_SleepingClose"
- with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day):
- with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
- tests.helper.oh_item.item_state_change_event("Unittest_Night", "OFF")
- apply_pos_mock.assert_called_once_with(config_day)
-
- with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
- tests.helper.oh_item.item_state_change_event("Unittest_Night", "ON")
- apply_pos_mock.assert_called_once_with(config_night)
-
- self.shading_max.state = "Manual"
- with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
- tests.helper.oh_item.item_state_change_event("Unittest_Night", "OFF")
- apply_pos_mock.assert_not_called()
-
- def test_cb_hand(self):
- """Test _cb_hand."""
- self.shading_min.to_Auto()
- self.shading_max.to_Auto()
-
- # last rule command was less than a second ago
- self.shading_min._set_shading_state_timestamp = time.time() - 0.1
- self.shading_max._set_shading_state_timestamp = time.time() - 0.1
-
- self.shading_min._cb_hand(unittest.mock.MagicMock())
- self.shading_max._cb_hand(unittest.mock.MagicMock())
-
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # last rule command was longer ago
- self.shading_min._set_shading_state_timestamp = time.time() - 1.6
- self.shading_max._set_shading_state_timestamp = time.time() - 1.6
-
- self.shading_min._cb_hand(unittest.mock.MagicMock())
- self.shading_max._cb_hand(unittest.mock.MagicMock())
-
- self.assertEqual("Hand", self.shading_min.state)
- self.assertEqual("Hand", self.shading_max.state)
-
- def test_night_active_and_configured(self):
- """Test _night_active_and_configured."""
- TestCase = collections.namedtuple("TestCase", "night, summer, config_summer, config_winter, expected_result")
-
- shading_pos = habapp_rules.actors.config.shading.ShadingPosition(42, 0)
-
- test_cases = [
- # night off
- TestCase("OFF", None, None, None, False),
- TestCase("OFF", None, None, shading_pos, False),
- TestCase("OFF", None, shading_pos, None, False),
- TestCase("OFF", None, shading_pos, shading_pos, False),
-
- TestCase("OFF", "OFF", None, None, False),
- TestCase("OFF", "OFF", None, shading_pos, False),
- TestCase("OFF", "OFF", shading_pos, None, False),
- TestCase("OFF", "OFF", shading_pos, shading_pos, False),
-
- TestCase("OFF", "ON", None, None, False),
- TestCase("OFF", "ON", None, shading_pos, False),
- TestCase("OFF", "ON", shading_pos, None, False),
- TestCase("OFF", "ON", shading_pos, shading_pos, False),
-
- # night on
- TestCase("ON", None, None, None, False),
- TestCase("ON", None, None, shading_pos, True),
- TestCase("ON", None, shading_pos, None, False),
- TestCase("ON", None, shading_pos, shading_pos, True),
-
- TestCase("ON", "OFF", None, None, False),
- TestCase("ON", "OFF", None, shading_pos, True),
- TestCase("ON", "OFF", shading_pos, None, False),
- TestCase("ON", "OFF", shading_pos, shading_pos, True),
-
- TestCase("ON", "ON", None, None, False),
- TestCase("ON", "ON", None, shading_pos, False),
- TestCase("ON", "ON", shading_pos, None, True),
- TestCase("ON", "ON", shading_pos, shading_pos, True),
- ]
-
- for test_case in test_cases:
- with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_night_close_summer", test_case.config_summer), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_night_close_winter", test_case.config_winter):
- tests.helper.oh_item.set_state("Unittest_Summer", test_case.summer)
- tests.helper.oh_item.set_state("Unittest_Night", test_case.night)
-
- self.assertEqual(test_case.expected_result, self.shading_max._night_active_and_configured())
-
- def test_manual_transitions(self):
- """Test transitions of state manual"""
-
- for initial_state in ("Hand", "Auto"):
- self.shading_min.state_machine.set_state(initial_state)
- self.shading_max.state_machine.set_state(initial_state)
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "ON", "OFF")
- self.assertEqual("Manual", self.shading_min.state)
- self.assertEqual("Manual", self.shading_max.state)
-
- # to WindAlarm | without wind_alarm_item
- self.shading_min.to_Manual()
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
- self.assertEqual("Manual", self.shading_min.state)
-
- # to WindAlarm | with wind_alarm_item (and back to manual -> states must be the same as before)
- self.shading_max.to_Manual()
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
- self.assertEqual("WindAlarm", self.shading_max.state)
-
- # to Auto (manual off)
- self.shading_min.to_Manual()
- self.shading_max.to_Manual()
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_Manual_min", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_Manual_max", "OFF", "ON")
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- def test_hand_transitions(self):
- """Test transitions of state hand"""
- # from auto to hand
- self.shading_min.to_Auto()
- self.shading_max.to_Auto()
- self.shading_min._hand_command()
- self.shading_max._hand_command()
- self.assertEqual("Hand", self.shading_min.state)
- self.assertEqual("Hand", self.shading_max.state)
-
- # from hand to auto
- self.shading_min.to_Hand()
- self.shading_max.to_Hand()
- self.shading_min._auto_hand_timeout()
- self.shading_max._auto_hand_timeout()
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # from hand to manual
- self.shading_min.to_Hand()
- self.shading_max.to_Hand()
- tests.helper.oh_item.send_command("Unittest_Manual_min", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Manual_max", "ON", "OFF")
- self.assertEqual("Manual", self.shading_min.state)
- self.assertEqual("Manual", self.shading_max.state)
-
- # from hand to wind_alarm
- self.shading_min.to_Hand()
- self.shading_max.to_Hand()
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
- self.assertEqual("Hand", self.shading_min.state)
- self.assertEqual("WindAlarm", self.shading_max.state)
-
- def test_wind_alarm_transitions(self):
- """Test transitions of state WindAlarm"""
- # from wind_alarm to manual
- self.shading_max.to_WindAlarm()
- tests.helper.oh_item.send_command("Unittest_Manual_max", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
- self.assertEqual("Manual", self.shading_max.state)
-
- # from wind_alarm to Auto
- self.shading_max.to_WindAlarm()
- tests.helper.oh_item.send_command("Unittest_Manual_max", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- def test_auto_open_transitions(self):
- """Test transitions of state Auto_Open"""
- # to door_open
- self.shading_min.to_Auto_Open()
- self.shading_max.to_Auto_Open()
- tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
-
- # to night_close | configured
- self.shading_min.to_Auto_Open()
- self.shading_max.to_Auto_Open()
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_NightClose", self.shading_max.state)
-
- # to night_close | NOT configured
- self.shading_min.to_Auto_Open()
- self.shading_max.to_Auto_Open()
- tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to sleeping_close
- self.shading_min.to_Auto_Open()
- self.shading_max.to_Auto_Open()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_SleepingClose", self.shading_max.state)
-
- # to sun_protection
- self.shading_min.to_Auto_Open()
- self.shading_max.to_Auto_Open()
- tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
- self.assertEqual("Auto_Open", self.shading_min.state)
- self.assertEqual("Auto_SunProtection", self.shading_max.state)
-
- def test_auto_night_close_transitions(self):
- """Test transitions of state Auto_NightClose"""
- # to open
- self.shading_max.to_Auto_NightClose()
- tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to door_open
- self.shading_max.to_Auto_NightClose()
- tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
- self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
-
- # to sleeping_close
- self.shading_max.to_Auto_NightClose()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_SleepingClose", self.shading_max.state)
-
- # to sun_protection
- self.shading_max.to_Auto_NightClose()
- tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
- self.assertEqual("Auto_SunProtection", self.shading_max.state)
-
- def test_auto_sleeping_close_transitions(self):
- """Test transitions of state Auto_SleepingClose"""
- # to open
- self.shading_max.to_Auto_SleepingClose()
- tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "OFF")
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to night_close | configured
- self.shading_max.to_Auto_SleepingClose()
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("Auto_NightClose", self.shading_max.state)
-
- # to night_close | NOT configured
- self.shading_max.to_Auto_SleepingClose()
- tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to door_open
- self.shading_max.to_Auto_SleepingClose()
- tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
- self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
-
- # to sun_protection
- self.shading_max.to_Auto_SleepingClose()
- tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("Auto_SunProtection", self.shading_max.state)
-
- def test_auto_sun_protection_transitions(self):
- """Test transitions of state Auto_SunProtection"""
- # to open
- self.shading_max.to_Auto_SunProtection()
- tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "ON")
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to night_close | configured
- self.shading_max.to_Auto_SunProtection()
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- self.assertEqual("Auto_NightClose", self.shading_max.state)
-
- # to night_close | NOT configured
- self.shading_max.to_Auto_SunProtection()
- tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
- self.assertEqual("Auto_SunProtection", self.shading_max.state)
-
- # to sleeping_protection
- self.shading_max.to_Auto_SunProtection()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_SleepingClose", self.shading_max.state)
-
- # to door_open
- self.shading_max.to_Auto_SunProtection()
- tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
- self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
-
- def test_auto_door_open_transitions(self):
- """Test transitions of state Auto_DoorOpen"""
- # door closed -> PostOpen
- self.shading_max.to_Auto_DoorOpen_Open()
- tests.helper.oh_item.send_command("Unittest_Door", "CLOSED", "OPEN")
- self.assertEqual("Auto_DoorOpen_PostOpen", self.shading_max.state)
-
- # door opened again -> Open
- tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
- self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
-
- # door closed + timeout -> AutoInit
- tests.helper.oh_item.send_command("Unittest_Door", "CLOSED", "OPEN")
- self.shading_max._timeout_post_door_open()
- self.assertEqual("Auto_Open", self.shading_max.state)
-
- # to sleeping from open
- self.shading_max.to_Auto_DoorOpen_Open()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_SleepingClose", self.shading_max.state)
-
- # to sleeping from post open
- self.shading_max.to_Auto_DoorOpen_PostOpen()
- tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
- self.assertEqual("Auto_SleepingClose", self.shading_max.state)
+ """Tests cases for testing _ShadingBase."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading_min", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_min", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Shading_max", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_max", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_min_state", "")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
+
+ config_min = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading_min", manual="Unittest_Manual_min", state="H_Unittest_Shading_min_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+
+ config_max = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading_max",
+ manual="Unittest_Manual_max",
+ wind_alarm="Unittest_WindAlarm",
+ sun_protection="Unittest_SunProtection",
+ sleeping_state="Unittest_Sleep_state",
+ night="Unittest_Night",
+ door="Unittest_Door",
+ summer="Unittest_Summer",
+ hand_manual_is_active_feedback="Unittest_Hand_Manual_active",
+ state="CustomState",
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+
+ self.shading_min = habapp_rules.actors.shading._ShadingBase(config_min)
+ self.shading_max = habapp_rules.actors.shading._ShadingBase(config_max)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "WindAlarm"},
+ {"name": "Manual"},
+ {"name": "Hand", "on_timeout": "_auto_hand_timeout", "timeout": 72000},
+ {
+ "name": "Auto",
+ "initial": "Init",
+ "children": [
+ {"name": "Init"},
+ {"name": "Open"},
+ {"name": "DoorOpen", "initial": "Open", "children": [{"name": "Open"}, {"name": "PostOpen", "on_timeout": "_timeout_post_door_open", "timeout": 300}]},
+ {"name": "NightClose"},
+ {"name": "SleepingClose"},
+ {"name": "SunProtection"},
+ ],
+ },
+ ]
+ self.assertEqual(expected_states, self.shading_max.states)
+
+ expected_trans = [
+ {"trigger": "_wind_alarm_on", "source": ["Auto", "Hand", "Manual"], "dest": "WindAlarm"},
+ {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Manual", "conditions": "_manual_active"},
+ {"trigger": "_wind_alarm_off", "source": "WindAlarm", "dest": "Auto", "unless": "_manual_active"},
+ # manual
+ {"trigger": "_manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
+ {"trigger": "_manual_off", "source": "Manual", "dest": "Auto"},
+ # hand
+ {"trigger": "_hand_command", "source": ["Auto"], "dest": "Hand"},
+ {"trigger": "_auto_hand_timeout", "source": "Hand", "dest": "Auto"},
+ # sun
+ {"trigger": "_sun_on", "source": "Auto_Open", "dest": "Auto_SunProtection"},
+ {"trigger": "_sun_off", "source": "Auto_SunProtection", "dest": "Auto_Open"},
+ # sleep
+ {"trigger": "_sleep_started", "source": ["Auto_Open", "Auto_NightClose", "Auto_SunProtection", "Auto_DoorOpen"], "dest": "Auto_SleepingClose"},
+ {"trigger": "_sleep_started", "source": "Hand", "dest": "Auto"},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_NightClose", "conditions": ["_night_active_and_configured"]},
+ {"trigger": "_sleep_stopped", "source": "Auto_SleepingClose", "dest": "Auto_Open", "unless": ["_night_active_and_configured", "_sun_protection_active_and_configured"]},
+ # door
+ {"trigger": "_door_open", "source": ["Auto_NightClose", "Auto_SunProtection", "Auto_SleepingClose", "Auto_Open"], "dest": "Auto_DoorOpen"},
+ {"trigger": "_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_DoorOpen_Open"},
+ {"trigger": "_door_closed", "source": "Auto_DoorOpen_Open", "dest": "Auto_DoorOpen_PostOpen"},
+ {"trigger": "_timeout_post_door_open", "source": "Auto_DoorOpen_PostOpen", "dest": "Auto_Init"},
+ # night close
+ {"trigger": "_night_started", "source": ["Auto_Open", "Auto_SunProtection"], "dest": "Auto_NightClose", "conditions": "_night_active_and_configured"},
+ {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_SunProtection", "conditions": "_sun_protection_active_and_configured"},
+ {"trigger": "_night_stopped", "source": "Auto_NightClose", "dest": "Auto_Open", "unless": ["_sun_protection_active_and_configured"]},
+ ]
+
+ self.assertEqual(expected_trans, self.shading_max.trans)
+
+ self.assertEqual(300, self.shading_max.state_machine.states["Auto"].states["DoorOpen"].states["PostOpen"].timeout)
+ self.assertEqual(86400, self.shading_max.state_machine.states["Manual"].timeout)
+
+ def test_init_exceptions(self) -> None:
+ """Test exceptions of __init__."""
+ TestCase = collections.namedtuple("TestCase", "item_type, raises_exc")
+
+ test_cases = [
+ TestCase(HABApp.openhab.items.RollershutterItem, False),
+ TestCase(HABApp.openhab.items.DimmerItem, False),
+ TestCase(HABApp.openhab.items.SwitchItem, True),
+ TestCase(HABApp.openhab.items.ContactItem, True),
+ TestCase(HABApp.openhab.items.NumberItem, True),
+ ]
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Temp_state", "")
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.add_mock_item(test_case.item_type, "Unittest_Temp", None)
+
+ if test_case.raises_exc:
+ with self.assertRaises(pydantic_core.ValidationError):
+ habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Temp", manual="Unittest_Manual_min", state="H_Unittest_Shading_min_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+ else:
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Temp", manual="Unittest_Manual_min", state="H_Unittest_Shading_min_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+ habapp_rules.actors.shading._ShadingBase(config)
+ tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Temp")
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Shading"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ jal_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.shading_min.states, transitions=self.shading_min.trans, initial=self.shading_min.state, show_conditions=False)
+
+ jal_graph.get_graph().draw(picture_dir / "Shading.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self.shading_min.states) if state != "auto_init"]:
+ jal_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.shading_min.states, transitions=self.shading_min.trans, initial=state_name, show_conditions=True)
+ jal_graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Shading_{state_name}.png", format="png", prog="dot")
+
+ @staticmethod
+ def get_initial_state_test_cases() -> list[collections.namedtuple]:
+ """Get test cases for initial state tests.
+
+ Returns:
+ tests cases
+ """
+ TestCase = collections.namedtuple("TestCase", "wind_alarm, manual, sleeping_state, door, night, sun_protection, expected_state")
+
+ return [
+ # wind_alarm = OFF | manual = OFF
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Auto_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Auto_SunProtection"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Auto_NightClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="Auto_NightClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Auto_SleepingClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Auto_SleepingClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Auto_SleepingClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="Auto_SleepingClose"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="Auto_DoorOpen_Open"),
+ TestCase(wind_alarm="OFF", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="Auto_DoorOpen_Open"),
+ # wind_alarm = OFF | manual = ON
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="Manual"),
+ TestCase(wind_alarm="OFF", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="Manual"),
+ # wind_alarm = ON | manual = OFF
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="OFF", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ # wind_alarm = ON | manual = ON
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.AWAKE, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="CLOSED", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="OFF", sun_protection="ON", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="OFF", expected_state="WindAlarm"),
+ TestCase(wind_alarm="ON", manual="ON", sleeping_state=habapp_rules.system.SleepState.SLEEPING, door="OPEN", night="ON", sun_protection="ON", expected_state="WindAlarm"),
+ ]
+
+ def test_get_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ for test_case in self.get_initial_state_test_cases():
+ tests.helper.oh_item.set_state("Unittest_WindAlarm", test_case.wind_alarm)
+ tests.helper.oh_item.set_state("Unittest_Manual_max", test_case.manual)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleeping_state.value)
+ tests.helper.oh_item.set_state("Unittest_Door", test_case.door)
+ tests.helper.oh_item.set_state("Unittest_Night", test_case.night)
+ tests.helper.oh_item.set_state("Unittest_SunProtection", test_case.sun_protection)
+
+ self.assertEqual(test_case.expected_state, self.shading_max._get_initial_state())
+
+ @staticmethod
+ def get_target_positions_test_cases() -> list[collections.namedtuple]:
+ """Get test cases for target position.
+
+ Returns:
+ tests cases
+ """
+ TestCase = collections.namedtuple("TestCase", "state, target_pos")
+
+ return [
+ TestCase("Hand", None),
+ TestCase("Manual", None),
+ TestCase("WindAlarm", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
+ TestCase("Auto_Open", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
+ TestCase("Auto_SunProtection", habapp_rules.actors.config.shading.ShadingPosition(100, None)),
+ TestCase("Auto_SleepingClose", habapp_rules.actors.config.shading.ShadingPosition(100, 100)),
+ TestCase("Auto_NightClose", habapp_rules.actors.config.shading.ShadingPosition(100, 100)),
+ TestCase("Auto_DoorOpen_Open", habapp_rules.actors.config.shading.ShadingPosition(0, 0)),
+ TestCase("Auto_DoorOpen_PostOpen", None),
+ ]
+
+ def test_get_target_position(self) -> None:
+ """Test _get_target_position."""
+ for test_case in self.get_target_positions_test_cases():
+ with self.subTest(test_case=test_case):
+ self.shading_max._set_state(test_case.state)
+ self.assertEqual(test_case.target_pos, self.shading_max._get_target_position())
+
+ tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
+ self.shading_max._set_state("Auto_NightClose")
+ self.assertEqual(None, self.shading_max._get_target_position())
+
+ def test_get_target_position_sleeping(self) -> None:
+ """Test get_target_position if sleeping is active."""
+ config_night = habapp_rules.actors.config.shading.ShadingPosition(20, 30)
+ config_day = habapp_rules.actors.config.shading.ShadingPosition(40, 50)
+
+ # item night is not None
+ with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day):
+ self.shading_max.state = "Auto_SleepingClose"
+ tests.helper.oh_item.set_state("Unittest_Night", "ON")
+ self.assertEqual(config_night, self.shading_max._get_target_position())
+
+ tests.helper.oh_item.set_state("Unittest_Night", "OFF")
+ self.assertEqual(config_day, self.shading_max._get_target_position())
+
+ # item night is None
+ with (
+ unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night),
+ unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day),
+ unittest.mock.patch.object(self.shading_max._config.items, "night", None),
+ ):
+ self.shading_max.state = "Auto_SleepingClose"
+ tests.helper.oh_item.set_state("Unittest_Night", "ON")
+ self.assertEqual(config_night, self.shading_max._get_target_position())
+
+ tests.helper.oh_item.set_state("Unittest_Night", "OFF")
+ self.assertEqual(config_night, self.shading_max._get_target_position())
+
+ def test_cb_sleep_state(self) -> None:
+ """Test _cb_sleep_state."""
+ TestCase = collections.namedtuple("TestCase", "sleep_state, started_triggered, stopped_triggered")
+
+ test_cases = [
+ TestCase(habapp_rules.system.SleepState.AWAKE, False, False),
+ TestCase(habapp_rules.system.SleepState.PRE_SLEEPING, True, False),
+ TestCase(habapp_rules.system.SleepState.SLEEPING, False, False),
+ TestCase(habapp_rules.system.SleepState.POST_SLEEPING, False, True),
+ TestCase(habapp_rules.system.SleepState.LOCKED, False, False),
+ ]
+
+ with unittest.mock.patch.object(self.shading_max, "_sleep_started") as started_mock, unittest.mock.patch.object(self.shading_max, "_sleep_stopped") as stopped_mock:
+ for test_case in test_cases:
+ started_mock.reset_mock()
+ stopped_mock.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", test_case.sleep_state.value)
+
+ if test_case.started_triggered:
+ started_mock.assert_called_once()
+ else:
+ started_mock.assert_not_called()
+
+ if test_case.stopped_triggered:
+ stopped_mock.assert_called_once()
+ else:
+ stopped_mock.assert_not_called()
+
+ def test_cb_night(self) -> None:
+ """Test _cb_night."""
+ config_night = habapp_rules.actors.config.shading.ShadingPosition(20, 30)
+ config_day = habapp_rules.actors.config.shading.ShadingPosition(40, 50)
+
+ self.shading_max.state = "Auto_SleepingClose"
+ with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_night", config_night), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_sleeping_day", config_day):
+ with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
+ tests.helper.oh_item.item_state_change_event("Unittest_Night", "OFF")
+ apply_pos_mock.assert_called_once_with(config_day)
+
+ with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
+ tests.helper.oh_item.item_state_change_event("Unittest_Night", "ON")
+ apply_pos_mock.assert_called_once_with(config_night)
+
+ self.shading_max.state = "Manual"
+ with unittest.mock.patch.object(self.shading_max, "_apply_target_position") as apply_pos_mock:
+ tests.helper.oh_item.item_state_change_event("Unittest_Night", "OFF")
+ apply_pos_mock.assert_not_called()
+
+ def test_cb_hand(self) -> None:
+ """Test _cb_hand."""
+ self.shading_min.to_Auto()
+ self.shading_max.to_Auto()
+
+ # last rule command was less than a second ago
+ self.shading_min._set_shading_state_timestamp = time.time() - 0.1
+ self.shading_max._set_shading_state_timestamp = time.time() - 0.1
+
+ self.shading_min._cb_hand(unittest.mock.MagicMock())
+ self.shading_max._cb_hand(unittest.mock.MagicMock())
+
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # last rule command was longer ago
+ self.shading_min._set_shading_state_timestamp = time.time() - 1.6
+ self.shading_max._set_shading_state_timestamp = time.time() - 1.6
+
+ self.shading_min._cb_hand(unittest.mock.MagicMock())
+ self.shading_max._cb_hand(unittest.mock.MagicMock())
+
+ self.assertEqual("Hand", self.shading_min.state)
+ self.assertEqual("Hand", self.shading_max.state)
+
+ def test_night_active_and_configured(self) -> None:
+ """Test _night_active_and_configured."""
+ TestCase = collections.namedtuple("TestCase", "night, summer, config_summer, config_winter, expected_result")
+
+ shading_pos = habapp_rules.actors.config.shading.ShadingPosition(42, 0)
+
+ test_cases = [
+ # night off
+ TestCase("OFF", None, None, None, False),
+ TestCase("OFF", None, None, shading_pos, False),
+ TestCase("OFF", None, shading_pos, None, False),
+ TestCase("OFF", None, shading_pos, shading_pos, False),
+ TestCase("OFF", "OFF", None, None, False),
+ TestCase("OFF", "OFF", None, shading_pos, False),
+ TestCase("OFF", "OFF", shading_pos, None, False),
+ TestCase("OFF", "OFF", shading_pos, shading_pos, False),
+ TestCase("OFF", "ON", None, None, False),
+ TestCase("OFF", "ON", None, shading_pos, False),
+ TestCase("OFF", "ON", shading_pos, None, False),
+ TestCase("OFF", "ON", shading_pos, shading_pos, False),
+ # night on
+ TestCase("ON", None, None, None, False),
+ TestCase("ON", None, None, shading_pos, True),
+ TestCase("ON", None, shading_pos, None, False),
+ TestCase("ON", None, shading_pos, shading_pos, True),
+ TestCase("ON", "OFF", None, None, False),
+ TestCase("ON", "OFF", None, shading_pos, True),
+ TestCase("ON", "OFF", shading_pos, None, False),
+ TestCase("ON", "OFF", shading_pos, shading_pos, True),
+ TestCase("ON", "ON", None, None, False),
+ TestCase("ON", "ON", None, shading_pos, False),
+ TestCase("ON", "ON", shading_pos, None, True),
+ TestCase("ON", "ON", shading_pos, shading_pos, True),
+ ]
+
+ for test_case in test_cases:
+ with unittest.mock.patch.object(self.shading_max._config.parameter, "pos_night_close_summer", test_case.config_summer), unittest.mock.patch.object(self.shading_max._config.parameter, "pos_night_close_winter", test_case.config_winter):
+ tests.helper.oh_item.set_state("Unittest_Summer", test_case.summer)
+ tests.helper.oh_item.set_state("Unittest_Night", test_case.night)
+
+ self.assertEqual(test_case.expected_result, self.shading_max._night_active_and_configured())
+
+ def test_manual_transitions(self) -> None:
+ """Test transitions of state manual."""
+ for initial_state in ("Hand", "Auto"):
+ self.shading_min.state_machine.set_state(initial_state)
+ self.shading_max.state_machine.set_state(initial_state)
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_min", "ON", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_max", "ON", "OFF")
+ self.assertEqual("Manual", self.shading_min.state)
+ self.assertEqual("Manual", self.shading_max.state)
+
+ # to WindAlarm | without wind_alarm_item
+ self.shading_min.to_Manual()
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
+ self.assertEqual("Manual", self.shading_min.state)
+
+ # to WindAlarm | with wind_alarm_item (and back to manual -> states must be the same as before)
+ self.shading_max.to_Manual()
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
+ self.assertEqual("WindAlarm", self.shading_max.state)
+
+ # to Auto (manual off)
+ self.shading_min.to_Manual()
+ self.shading_max.to_Manual()
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_Manual_min", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_Manual_max", "OFF", "ON")
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ def test_hand_transitions(self) -> None:
+ """Test transitions of state hand."""
+ # from auto to hand
+ self.shading_min.to_Auto()
+ self.shading_max.to_Auto()
+ self.shading_min._hand_command()
+ self.shading_max._hand_command()
+ self.assertEqual("Hand", self.shading_min.state)
+ self.assertEqual("Hand", self.shading_max.state)
+
+ # from hand to auto
+ self.shading_min.to_Hand()
+ self.shading_max.to_Hand()
+ self.shading_min._auto_hand_timeout()
+ self.shading_max._auto_hand_timeout()
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # from hand to manual
+ self.shading_min.to_Hand()
+ self.shading_max.to_Hand()
+ tests.helper.oh_item.send_command("Unittest_Manual_min", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Manual_max", "ON", "OFF")
+ self.assertEqual("Manual", self.shading_min.state)
+ self.assertEqual("Manual", self.shading_max.state)
+
+ # from hand to wind_alarm
+ self.shading_min.to_Hand()
+ self.shading_max.to_Hand()
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
+ self.assertEqual("Hand", self.shading_min.state)
+ self.assertEqual("WindAlarm", self.shading_max.state)
+
+ def test_wind_alarm_transitions(self) -> None:
+ """Test transitions of state WindAlarm."""
+ # from wind_alarm to manual
+ self.shading_max.to_WindAlarm()
+ tests.helper.oh_item.send_command("Unittest_Manual_max", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
+ self.assertEqual("Manual", self.shading_max.state)
+
+ # from wind_alarm to Auto
+ self.shading_max.to_WindAlarm()
+ tests.helper.oh_item.send_command("Unittest_Manual_max", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ def test_auto_open_transitions(self) -> None:
+ """Test transitions of state Auto_Open."""
+ # to door_open
+ self.shading_min.to_Auto_Open()
+ self.shading_max.to_Auto_Open()
+ tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
+
+ # to night_close | configured
+ self.shading_min.to_Auto_Open()
+ self.shading_max.to_Auto_Open()
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_NightClose", self.shading_max.state)
+
+ # to night_close | NOT configured
+ self.shading_min.to_Auto_Open()
+ self.shading_max.to_Auto_Open()
+ tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to sleeping_close
+ self.shading_min.to_Auto_Open()
+ self.shading_max.to_Auto_Open()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_SleepingClose", self.shading_max.state)
+
+ # to sun_protection
+ self.shading_min.to_Auto_Open()
+ self.shading_max.to_Auto_Open()
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
+ self.assertEqual("Auto_Open", self.shading_min.state)
+ self.assertEqual("Auto_SunProtection", self.shading_max.state)
+
+ def test_auto_night_close_transitions(self) -> None:
+ """Test transitions of state Auto_NightClose."""
+ # to open
+ self.shading_max.to_Auto_NightClose()
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to door_open
+ self.shading_max.to_Auto_NightClose()
+ tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
+ self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
+
+ # to sleeping_close
+ self.shading_max.to_Auto_NightClose()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_SleepingClose", self.shading_max.state)
+
+ # to sun_protection
+ self.shading_max.to_Auto_NightClose()
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
+ self.assertEqual("Auto_SunProtection", self.shading_max.state)
+
+ def test_auto_sleeping_close_transitions(self) -> None:
+ """Test transitions of state Auto_SleepingClose."""
+ # to open
+ self.shading_max.to_Auto_SleepingClose()
+ tests.helper.oh_item.send_command("Unittest_Night", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to night_close | configured
+ self.shading_max.to_Auto_SleepingClose()
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("Auto_NightClose", self.shading_max.state)
+
+ # to night_close | NOT configured
+ self.shading_max.to_Auto_SleepingClose()
+ tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to door_open
+ self.shading_max.to_Auto_SleepingClose()
+ tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
+ self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
+
+ # to sun_protection
+ self.shading_max.to_Auto_SleepingClose()
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.POST_SLEEPING.value, habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("Auto_SunProtection", self.shading_max.state)
+
+ def test_auto_sun_protection_transitions(self) -> None:
+ """Test transitions of state Auto_SunProtection."""
+ # to open
+ self.shading_max.to_Auto_SunProtection()
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "OFF", "ON")
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to night_close | configured
+ self.shading_max.to_Auto_SunProtection()
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ self.assertEqual("Auto_NightClose", self.shading_max.state)
+
+ # to night_close | NOT configured
+ self.shading_max.to_Auto_SunProtection()
+ tests.helper.oh_item.send_command("Unittest_Summer", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Night", "ON", "OFF")
+ self.assertEqual("Auto_SunProtection", self.shading_max.state)
+
+ # to sleeping_protection
+ self.shading_max.to_Auto_SunProtection()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_SleepingClose", self.shading_max.state)
+
+ # to door_open
+ self.shading_max.to_Auto_SunProtection()
+ tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
+ self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
+
+ def test_auto_door_open_transitions(self) -> None:
+ """Test transitions of state Auto_DoorOpen."""
+ # door closed -> PostOpen
+ self.shading_max.to_Auto_DoorOpen_Open()
+ tests.helper.oh_item.send_command("Unittest_Door", "CLOSED", "OPEN")
+ self.assertEqual("Auto_DoorOpen_PostOpen", self.shading_max.state)
+
+ # door opened again -> Open
+ tests.helper.oh_item.send_command("Unittest_Door", "OPEN", "CLOSED")
+ self.assertEqual("Auto_DoorOpen_Open", self.shading_max.state)
+
+ # door closed + timeout -> AutoInit
+ tests.helper.oh_item.send_command("Unittest_Door", "CLOSED", "OPEN")
+ self.shading_max._timeout_post_door_open()
+ self.assertEqual("Auto_Open", self.shading_max.state)
+
+ # to sleeping from open
+ self.shading_max.to_Auto_DoorOpen_Open()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_SleepingClose", self.shading_max.state)
+
+ # to sleeping from post open
+ self.shading_max.to_Auto_DoorOpen_PostOpen()
+ tests.helper.oh_item.send_command("Unittest_Sleep_state", habapp_rules.system.SleepState.PRE_SLEEPING.value, habapp_rules.system.SleepState.AWAKE.value)
+ self.assertEqual("Auto_SleepingClose", self.shading_max.state)
class TestShadingShutter(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Raffstore class."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Shading", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
-
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- wind_alarm="Unittest_WindAlarm",
- sun_protection="Unittest_SunProtection",
- sleep_state="Unittest_Sleep_state",
- night="Unittest_Night",
- door="Unittest_Door",
- summer="Unittest_Summer",
- hand_manual_active="Unittest_Hand_Manual_active",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- self.shutter = habapp_rules.actors.shading.Shutter(config)
-
- def test_set_shading_state(self):
- """Test _set_shading_state."""
- TestCase = collections.namedtuple("TestCase", "target_pos, sent_pos")
- self.shutter._set_shading_state_timestamp = None
-
- test_cases = [
- TestCase(None, None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, None), None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, 25), None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, None), 50),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80),
-
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 0), 0),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 25), 0),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, 0), 50),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80),
- ]
-
- with unittest.mock.patch.object(self.shutter, "_get_target_position") as get_pos_mock, unittest.mock.patch.object(self.shutter._state_observer_pos, "send_command") as send_pos_mock:
- for test_case in test_cases:
- get_pos_mock.return_value = test_case.target_pos
- send_pos_mock.reset_mock()
-
- self.shutter._set_shading_state()
-
- if test_case.sent_pos is None:
- send_pos_mock.assert_not_called()
- else:
- send_pos_mock.assert_called_once_with(test_case.sent_pos)
-
- self.assertTrue(self.shutter._set_shading_state_timestamp > 1000)
+ """Tests cases for testing Raffstore class."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Shading", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
+
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading",
+ manual="Unittest_Manual",
+ wind_alarm="Unittest_WindAlarm",
+ sun_protection="Unittest_SunProtection",
+ sleep_state="Unittest_Sleep_state",
+ night="Unittest_Night",
+ door="Unittest_Door",
+ summer="Unittest_Summer",
+ hand_manual_active="Unittest_Hand_Manual_active",
+ state="H_Unittest_Shading_state",
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+
+ self.shutter = habapp_rules.actors.shading.Shutter(config)
+
+ def test_set_shading_state(self) -> None:
+ """Test _set_shading_state."""
+ TestCase = collections.namedtuple("TestCase", "target_pos, sent_pos")
+ self.shutter._set_shading_state_timestamp = None
+
+ test_cases = [
+ TestCase(None, None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, None), None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, 25), None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, None), 50),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 0), 0),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 25), 0),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, 0), 50),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80),
+ ]
+
+ with unittest.mock.patch.object(self.shutter, "_get_target_position") as get_pos_mock, unittest.mock.patch.object(self.shutter._state_observer_pos, "send_command") as send_pos_mock:
+ for test_case in test_cases:
+ get_pos_mock.return_value = test_case.target_pos
+ send_pos_mock.reset_mock()
+
+ self.shutter._set_shading_state()
+
+ if test_case.sent_pos is None:
+ send_pos_mock.assert_not_called()
+ else:
+ send_pos_mock.assert_called_once_with(test_case.sent_pos)
+
+ self.assertTrue(self.shutter._set_shading_state_timestamp > 1000)
class TestShadingRaffstore(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Raffstore class."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Slat", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "OFF")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", "")
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_SunProtection_Slat", 83)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
-
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- wind_alarm="Unittest_WindAlarm",
- sun_protection="Unittest_SunProtection",
- sun_protection_slat="Unittest_SunProtection_Slat",
- sleeping_state="Unittest_Sleep_state",
- night="Unittest_Night",
- door="Unittest_Door",
- summer="Unittest_Summer",
- hand_manual_active="Unittest_Hand_Manual_active",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- self.raffstore = habapp_rules.actors.shading.Raffstore(config)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Shading", None)
- tests.helper.oh_item.set_state("Unittest_Slat", None)
- tests.helper.oh_item.set_state("Unittest_Manual", None)
- tests.helper.oh_item.set_state("Unittest_WindAlarm", None)
- tests.helper.oh_item.set_state("Unittest_SunProtection", None)
- tests.helper.oh_item.set_state("Unittest_SunProtection_Slat", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
- tests.helper.oh_item.set_state("Unittest_Night", None)
- tests.helper.oh_item.set_state("Unittest_Door", None)
- tests.helper.oh_item.set_state("Unittest_Summer", None)
- tests.helper.oh_item.set_state("Unittest_Hand_Manual_active", None)
-
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- wind_alarm="Unittest_WindAlarm",
- sun_protection="Unittest_SunProtection",
- sun_protection_slat="Unittest_SunProtection_Slat",
- sleeping_state="Unittest_Sleep_state",
- night="Unittest_Night",
- door="Unittest_Door",
- summer="Unittest_Summer",
- hand_manual_active="Unittest_Hand_Manual_active",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- habapp_rules.actors.shading.Raffstore(config)
-
- def test_init_min(self):
- """Test init of raffstore with minimal attributes."""
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
-
- habapp_rules.actors.shading.Raffstore(config)
-
- def test_init_missing_items(self):
- """Test init with missing items."""
- # sun_protection item NOT given | item sun_protection_slat NOT given
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- habapp_rules.actors.shading.Raffstore(config)
-
- # sun_protection item NOT given | item sun_protection_slat given
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state",
- sun_protection_slat="Unittest_SunProtection_Slat"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.shading.Raffstore(config)
-
- # sun_protection item given | item sun_protection_slat NOT given
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state",
- sun_protection="Unittest_SunProtection",
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.shading.Raffstore(config)
-
- # sun_protection item given | item sun_protection_slat given
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- slat="Unittest_Slat",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state",
- sun_protection="Unittest_SunProtection",
- sun_protection_slat="Unittest_SunProtection_Slat"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- habapp_rules.actors.shading.Raffstore(config)
-
- # slat is missing
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- config = habapp_rules.actors.config.shading.ShadingConfig(
- items=habapp_rules.actors.config.shading.ShadingItems(
- shading_position="Unittest_Shading",
- manual="Unittest_Manual",
- state="H_Unittest_Shading_state"
- ),
- parameter=habapp_rules.actors.config.shading.ShadingParameter()
- )
- habapp_rules.actors.shading.Raffstore(config)
-
- def test_verify_items(self):
- """Test __verify_items"""
- # test shading position type
- TestCase = collections.namedtuple("TestCase", "item_type, raises_exc")
-
- test_cases = [
- TestCase(HABApp.openhab.items.RollershutterItem, False),
- TestCase(HABApp.openhab.items.DimmerItem, True),
- TestCase(HABApp.openhab.items.SwitchItem, True),
- TestCase(HABApp.openhab.items.ContactItem, True),
- TestCase(HABApp.openhab.items.NumberItem, True)
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.raffstore._config.items.shading_position = test_case.item_type("Name")
- if test_case.raises_exc:
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- self.raffstore._Raffstore__verify_items()
- else:
- self.raffstore._Raffstore__verify_items()
-
- def test_get_target_position(self):
- """Test _get_target_position."""
- TestCase = collections.namedtuple("TestCase", "state, target_pos")
-
- test_cases = TestShadingBase.get_target_positions_test_cases()
- test_cases[4] = TestCase("Auto_SunProtection", habapp_rules.actors.config.shading.ShadingPosition(100, 83))
-
- for test_case in test_cases:
- self.raffstore._set_state(test_case.state)
- self.assertEqual(test_case.target_pos, self.raffstore._get_target_position())
-
- def test_set_shading_state(self):
- """Test _set_shading_state"""
- TestCase = collections.namedtuple("TestCase", "target_pos, sent_pos, sent_slat")
-
- test_cases = [
- TestCase(None, None, None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, None), None, None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, 25), None, 25),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, None), 50, None),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80, 90),
-
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 0), 0, 0),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 25), 0, 25),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, 0), 50, 0),
- TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80, 90),
- ]
-
- with (unittest.mock.patch.object(self.raffstore, "_get_target_position") as get_pos_mock,
- unittest.mock.patch.object(self.raffstore._state_observer_pos, "send_command") as send_pos_mock,
- unittest.mock.patch.object(self.raffstore._state_observer_slat, "send_command") as send_slat_mock):
- for test_case in test_cases:
- get_pos_mock.return_value = test_case.target_pos
- send_pos_mock.reset_mock()
- send_slat_mock.reset_mock()
-
- self.raffstore._set_shading_state()
-
- if test_case.sent_pos is None:
- send_pos_mock.assert_not_called()
- else:
- send_pos_mock.assert_called_once_with(test_case.sent_pos)
-
- if test_case.sent_slat is None:
- send_slat_mock.assert_not_called()
- else:
- send_slat_mock.assert_called_once_with(test_case.sent_slat)
-
- def test_cb_slat(self):
- """Test _cb_slat"""
- with unittest.mock.patch.object(self.raffstore._state_observer_slat, "send_command") as send_command_mock:
- tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_SunProtection_Slat", 15, 99)
- send_command_mock.assert_not_called()
-
- tests.helper.oh_item.send_command("Unittest_Manual", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
- self.assertEqual("Auto_SunProtection", self.raffstore.state)
- tests.helper.oh_item.send_command("Unittest_SunProtection_Slat", 22, 15)
- send_command_mock.assert_has_calls([
- unittest.mock.call(0),
- unittest.mock.call(15),
- unittest.mock.call(22)
- ])
-
- def test_manual_position_restore(self):
- """Test if manual position is restored correctly"""
- tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
-
- tests.helper.oh_item.send_command("Unittest_Shading", 12)
- tests.helper.oh_item.send_command("Unittest_Slat", 34)
-
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Shading", 0)
- tests.helper.oh_item.assert_value("Unittest_Slat", 0)
-
- tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
- tests.helper.oh_item.assert_value("Unittest_Shading", 12)
- tests.helper.oh_item.assert_value("Unittest_Slat", 34)
+ """Tests cases for testing Raffstore class."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_Shading", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Slat", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", "OFF")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Shading_state", "")
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_WindAlarm", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_SunProtection", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_SunProtection_Slat", 83)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Hand_Manual_active", "OFF")
+
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading",
+ slat="Unittest_Slat",
+ manual="Unittest_Manual",
+ wind_alarm="Unittest_WindAlarm",
+ sun_protection="Unittest_SunProtection",
+ sun_protection_slat="Unittest_SunProtection_Slat",
+ sleeping_state="Unittest_Sleep_state",
+ night="Unittest_Night",
+ door="Unittest_Door",
+ summer="Unittest_Summer",
+ hand_manual_active="Unittest_Hand_Manual_active",
+ state="H_Unittest_Shading_state",
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+
+ self.raffstore = habapp_rules.actors.shading.Raffstore(config)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Shading", None)
+ tests.helper.oh_item.set_state("Unittest_Slat", None)
+ tests.helper.oh_item.set_state("Unittest_Manual", None)
+ tests.helper.oh_item.set_state("Unittest_WindAlarm", None)
+ tests.helper.oh_item.set_state("Unittest_SunProtection", None)
+ tests.helper.oh_item.set_state("Unittest_SunProtection_Slat", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", None)
+ tests.helper.oh_item.set_state("Unittest_Night", None)
+ tests.helper.oh_item.set_state("Unittest_Door", None)
+ tests.helper.oh_item.set_state("Unittest_Summer", None)
+ tests.helper.oh_item.set_state("Unittest_Hand_Manual_active", None)
+
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading",
+ slat="Unittest_Slat",
+ manual="Unittest_Manual",
+ wind_alarm="Unittest_WindAlarm",
+ sun_protection="Unittest_SunProtection",
+ sun_protection_slat="Unittest_SunProtection_Slat",
+ sleeping_state="Unittest_Sleep_state",
+ night="Unittest_Night",
+ door="Unittest_Door",
+ summer="Unittest_Summer",
+ hand_manual_active="Unittest_Hand_Manual_active",
+ state="H_Unittest_Shading_state",
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+
+ habapp_rules.actors.shading.Raffstore(config)
+
+ def test_init_min(self) -> None:
+ """Test init of raffstore with minimal attributes."""
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", slat="Unittest_Slat", manual="Unittest_Manual", state="H_Unittest_Shading_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+
+ habapp_rules.actors.shading.Raffstore(config)
+
+ def test_init_missing_items(self) -> None:
+ """Test init with missing items."""
+ # sun_protection item NOT given | item sun_protection_slat NOT given
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", slat="Unittest_Slat", manual="Unittest_Manual", state="H_Unittest_Shading_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+ habapp_rules.actors.shading.Raffstore(config)
+
+ # sun_protection item NOT given | item sun_protection_slat given
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", slat="Unittest_Slat", manual="Unittest_Manual", state="H_Unittest_Shading_state", sun_protection_slat="Unittest_SunProtection_Slat"),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.shading.Raffstore(config)
+
+ # sun_protection item given | item sun_protection_slat NOT given
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading",
+ slat="Unittest_Slat",
+ manual="Unittest_Manual",
+ state="H_Unittest_Shading_state",
+ sun_protection="Unittest_SunProtection",
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.shading.Raffstore(config)
+
+ # sun_protection item given | item sun_protection_slat given
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(
+ shading_position="Unittest_Shading", slat="Unittest_Slat", manual="Unittest_Manual", state="H_Unittest_Shading_state", sun_protection="Unittest_SunProtection", sun_protection_slat="Unittest_SunProtection_Slat"
+ ),
+ parameter=habapp_rules.actors.config.shading.ShadingParameter(),
+ )
+ habapp_rules.actors.shading.Raffstore(config)
+
+ # slat is missing
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ config = habapp_rules.actors.config.shading.ShadingConfig(
+ items=habapp_rules.actors.config.shading.ShadingItems(shading_position="Unittest_Shading", manual="Unittest_Manual", state="H_Unittest_Shading_state"), parameter=habapp_rules.actors.config.shading.ShadingParameter()
+ )
+ habapp_rules.actors.shading.Raffstore(config)
+
+ def test_verify_items(self) -> None:
+ """Test __verify_items."""
+ # test shading position type
+ TestCase = collections.namedtuple("TestCase", "item_type, raises_exc")
+
+ test_cases = [
+ TestCase(HABApp.openhab.items.RollershutterItem, False),
+ TestCase(HABApp.openhab.items.DimmerItem, True),
+ TestCase(HABApp.openhab.items.SwitchItem, True),
+ TestCase(HABApp.openhab.items.ContactItem, True),
+ TestCase(HABApp.openhab.items.NumberItem, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.raffstore._config.items.shading_position = test_case.item_type("Name")
+ if test_case.raises_exc:
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ self.raffstore._Raffstore__verify_items()
+ else:
+ self.raffstore._Raffstore__verify_items()
+
+ def test_get_target_position(self) -> None:
+ """Test _get_target_position."""
+ TestCase = collections.namedtuple("TestCase", "state, target_pos")
+
+ test_cases = TestShadingBase.get_target_positions_test_cases()
+ test_cases[4] = TestCase("Auto_SunProtection", habapp_rules.actors.config.shading.ShadingPosition(100, 83))
+
+ for test_case in test_cases:
+ self.raffstore._set_state(test_case.state)
+ self.assertEqual(test_case.target_pos, self.raffstore._get_target_position())
+
+ def test_set_shading_state(self) -> None:
+ """Test _set_shading_state."""
+ TestCase = collections.namedtuple("TestCase", "target_pos, sent_pos, sent_slat")
+
+ test_cases = [
+ TestCase(None, None, None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, None), None, None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(None, 25), None, 25),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, None), 50, None),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80, 90),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 0), 0, 0),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(0, 25), 0, 25),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(50, 0), 50, 0),
+ TestCase(habapp_rules.actors.config.shading.ShadingPosition(80, 90), 80, 90),
+ ]
+
+ with (
+ unittest.mock.patch.object(self.raffstore, "_get_target_position") as get_pos_mock,
+ unittest.mock.patch.object(self.raffstore._state_observer_pos, "send_command") as send_pos_mock,
+ unittest.mock.patch.object(self.raffstore._state_observer_slat, "send_command") as send_slat_mock,
+ ):
+ for test_case in test_cases:
+ get_pos_mock.return_value = test_case.target_pos
+ send_pos_mock.reset_mock()
+ send_slat_mock.reset_mock()
+
+ self.raffstore._set_shading_state()
+
+ if test_case.sent_pos is None:
+ send_pos_mock.assert_not_called()
+ else:
+ send_pos_mock.assert_called_once_with(test_case.sent_pos)
+
+ if test_case.sent_slat is None:
+ send_slat_mock.assert_not_called()
+ else:
+ send_slat_mock.assert_called_once_with(test_case.sent_slat)
+
+ def test_cb_slat(self) -> None:
+ """Test _cb_slat."""
+ with unittest.mock.patch.object(self.raffstore._state_observer_slat, "send_command") as send_command_mock:
+ tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_SunProtection_Slat", 15, 99)
+ send_command_mock.assert_not_called()
+
+ tests.helper.oh_item.send_command("Unittest_Manual", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_SunProtection", "ON", "OFF")
+ self.assertEqual("Auto_SunProtection", self.raffstore.state)
+ tests.helper.oh_item.send_command("Unittest_SunProtection_Slat", 22, 15)
+ send_command_mock.assert_has_calls([unittest.mock.call(0), unittest.mock.call(15), unittest.mock.call(22)])
+
+ def test_manual_position_restore(self) -> None:
+ """Test if manual position is restored correctly."""
+ tests.helper.oh_item.send_command("Unittest_Manual", "ON", "OFF")
+
+ tests.helper.oh_item.send_command("Unittest_Shading", 12)
+ tests.helper.oh_item.send_command("Unittest_Slat", 34)
+
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "ON", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Shading", 0)
+ tests.helper.oh_item.assert_value("Unittest_Slat", 0)
+
+ tests.helper.oh_item.send_command("Unittest_WindAlarm", "OFF", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Shading", 12)
+ tests.helper.oh_item.assert_value("Unittest_Slat", 34)
class TestResetAllManualHand(tests.helper.test_case_base.TestCaseBase):
- """Tests for ResetAllManualHand"""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
+ """Tests for ResetAllManualHand."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Reset", None)
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
- config = habapp_rules.actors.config.shading.ResetAllManualHandConfig(
- items=habapp_rules.actors.config.shading.ResetAllManualHandItems(
- reset_manual_hand="Unittest_Reset"
- )
- )
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Reset", None)
- self.reset_shading_rule = habapp_rules.actors.shading.ResetAllManualHand(config)
+ config = habapp_rules.actors.config.shading.ResetAllManualHandConfig(items=habapp_rules.actors.config.shading.ResetAllManualHandItems(reset_manual_hand="Unittest_Reset"))
- def test__get_shading_objects(self):
- """Test __get_shading_objects."""
- # shading objects where given
- shading_object_1 = unittest.mock.MagicMock()
- shading_object_2 = unittest.mock.MagicMock()
+ self.reset_shading_rule = habapp_rules.actors.shading.ResetAllManualHand(config)
- config_2 = habapp_rules.actors.config.shading.ResetAllManualHandConfig(
- items=habapp_rules.actors.config.shading.ResetAllManualHandItems(
- reset_manual_hand="Unittest_Reset",
- ),
- parameter=habapp_rules.actors.config.shading.ResetAllManualHandParameter(
- shading_objects=[shading_object_1, shading_object_2]
- )
- )
+ def test__get_shading_objects(self) -> None:
+ """Test __get_shading_objects."""
+ # shading objects where given
+ shading_object_1 = unittest.mock.MagicMock()
+ shading_object_2 = unittest.mock.MagicMock()
- reset_shading_rule_2 = habapp_rules.actors.shading.ResetAllManualHand(config_2)
- self.assertEqual([shading_object_1, shading_object_2], reset_shading_rule_2._ResetAllManualHand__get_shading_objects())
+ config_2 = habapp_rules.actors.config.shading.ResetAllManualHandConfig(
+ items=habapp_rules.actors.config.shading.ResetAllManualHandItems(
+ reset_manual_hand="Unittest_Reset",
+ ),
+ parameter=habapp_rules.actors.config.shading.ResetAllManualHandParameter(shading_objects=[shading_object_1, shading_object_2]),
+ )
- # shading objects are not set via __init__
- with unittest.mock.patch.object(self.reset_shading_rule, "get_rule") as get_rule_mock:
- self.reset_shading_rule._ResetAllManualHand__get_shading_objects()
+ reset_shading_rule_2 = habapp_rules.actors.shading.ResetAllManualHand(config_2)
+ self.assertEqual([shading_object_1, shading_object_2], reset_shading_rule_2._ResetAllManualHand__get_shading_objects())
- get_rule_mock.assert_called_once_with(None)
+ # shading objects are not set via __init__
+ with unittest.mock.patch.object(self.reset_shading_rule, "get_rule") as get_rule_mock:
+ self.reset_shading_rule._ResetAllManualHand__get_shading_objects()
- def test_cb_reset_all(self):
- """Test _cb_reset_all."""
- TestCase = collections.namedtuple("TestCase", "event, state, manual_commands")
+ get_rule_mock.assert_called_once_with(None)
- test_cases = [
- TestCase("OFF", "Auto", []),
- TestCase("OFF", "Hand", []),
- TestCase("OFF", "Manual", []),
+ def test_cb_reset_all(self) -> None:
+ """Test _cb_reset_all."""
+ TestCase = collections.namedtuple("TestCase", "event, state, manual_commands")
- TestCase("ON", "Auto", []),
- TestCase("ON", "Hand", [unittest.mock.call("ON"), unittest.mock.call("OFF")]),
- TestCase("ON", "Manual", [unittest.mock.call("OFF")])
- ]
+ test_cases = [
+ TestCase("OFF", "Auto", []),
+ TestCase("OFF", "Hand", []),
+ TestCase("OFF", "Manual", []),
+ TestCase("ON", "Auto", []),
+ TestCase("ON", "Hand", [unittest.mock.call("ON"), unittest.mock.call("OFF")]),
+ TestCase("ON", "Manual", [unittest.mock.call("OFF")]),
+ ]
- shading_rule_mock = unittest.mock.MagicMock()
- shading_rule_mock._config.items.manual = unittest.mock.MagicMock(spec=HABApp.openhab.items.SwitchItem)
+ shading_rule_mock = unittest.mock.MagicMock()
+ shading_rule_mock._config.items.manual = unittest.mock.MagicMock(spec=HABApp.openhab.items.SwitchItem)
- with unittest.mock.patch.object(self.reset_shading_rule, "_ResetAllManualHand__get_shading_objects", return_value=[shading_rule_mock]):
- for test_case in test_cases:
- shading_rule_mock.state = test_case.state
- shading_rule_mock._config.items.manual.oh_send_command.reset_mock()
+ with unittest.mock.patch.object(self.reset_shading_rule, "_ResetAllManualHand__get_shading_objects", return_value=[shading_rule_mock]):
+ for test_case in test_cases:
+ shading_rule_mock.state = test_case.state
+ shading_rule_mock._config.items.manual.oh_send_command.reset_mock()
- self.reset_shading_rule._cb_reset_all(HABApp.openhab.events.ItemCommandEvent("name", test_case.event))
+ self.reset_shading_rule._cb_reset_all(HABApp.openhab.events.ItemCommandEvent("name", test_case.event))
- self.assertEqual(len(test_case.manual_commands), shading_rule_mock._config.items.manual.oh_send_command.call_count)
- shading_rule_mock._config.items.manual.oh_send_command.assert_has_calls(test_case.manual_commands)
+ self.assertEqual(len(test_case.manual_commands), shading_rule_mock._config.items.manual.oh_send_command.call_count)
+ shading_rule_mock._config.items.manual.oh_send_command.assert_has_calls(test_case.manual_commands)
class TestSlatValueSun(tests.helper.test_case_base.TestCaseBase):
- """Tests for SlatValueSun."""
-
- def setUp(self):
- """Setup test case."""
-
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_SlatValueSingle", None) # NumberItem
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_SlatValueDual", None) # DimmerItem
-
- self.config_single = habapp_rules.actors.config.shading.SlatValueConfig(
- items=habapp_rules.actors.config.shading.SlatValueItems(
- sun_elevation="Unittest_Elevation",
- slat_value="Unittest_SlatValueSingle",
- ),
- parameter=habapp_rules.actors.config.shading.SlatValueParameter(
- elevation_slat_characteristic=[
- habapp_rules.actors.config.shading.ElevationSlatMapping(0, 100),
- habapp_rules.actors.config.shading.ElevationSlatMapping(10, 70),
- habapp_rules.actors.config.shading.ElevationSlatMapping(20, 60),
- habapp_rules.actors.config.shading.ElevationSlatMapping(30, 50),
- habapp_rules.actors.config.shading.ElevationSlatMapping(40, 50),
- ]
- )
- )
-
- self.config_dual = habapp_rules.actors.config.shading.SlatValueConfig(
- items=habapp_rules.actors.config.shading.SlatValueItems(
- sun_elevation="Unittest_Elevation",
- summer="Unittest_Summer",
- slat_value="Unittest_SlatValueDual",
- ),
- parameter=habapp_rules.actors.config.shading.SlatValueParameter(
- elevation_slat_characteristic=copy.deepcopy(self.config_single.parameter.elevation_slat_characteristic),
- elevation_slat_characteristic_summer=[
- habapp_rules.actors.config.shading.ElevationSlatMapping(0, 100),
- habapp_rules.actors.config.shading.ElevationSlatMapping(10, 80),
- habapp_rules.actors.config.shading.ElevationSlatMapping(20, 70),
- habapp_rules.actors.config.shading.ElevationSlatMapping(30, 60),
- habapp_rules.actors.config.shading.ElevationSlatMapping(40, 50),
- habapp_rules.actors.config.shading.ElevationSlatMapping(50, 50),
- ]
- )
- )
-
- self.slat_value_sun_single = habapp_rules.actors.shading.SlatValueSun(self.config_single)
- self.slat_value_sun_dual = habapp_rules.actors.shading.SlatValueSun(self.config_dual)
-
- def test__get_slat_value(self):
- """Test __get_slat_value."""
- tests.helper.oh_item.item_state_change_event("Unittest_Summer", "ON")
- TestCase = collections.namedtuple("TestCase", "elevation, expected_result_single, , expected_result_dual")
-
- test_cases = [
- TestCase(-10, 100, 100),
-
- TestCase(-1, 100, 100),
- TestCase(0, 100, 100),
- TestCase(1, 100, 100),
-
- TestCase(9, 100, 100),
- TestCase(10, 70, 80),
- TestCase(11, 70, 80),
-
- TestCase(29, 60, 70),
- TestCase(30, 50, 60),
- TestCase(31, 50, 60),
-
- TestCase(39, 50, 60),
- TestCase(40, 50, 50),
- TestCase(41, 50, 50),
-
- TestCase(60, 50, 50),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.assertEqual(test_case.expected_result_single, self.slat_value_sun_single._SlatValueSun__get_slat_value(test_case.elevation))
- self.assertEqual(test_case.expected_result_dual, self.slat_value_sun_dual._SlatValueSun__get_slat_value(test_case.elevation))
-
- def test_cb_elevation(self):
- """Test _cb_elevation."""
- with unittest.mock.patch.object(self.slat_value_sun_single, "_SlatValueSun__get_slat_value", side_effect=[42, 43]):
- tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", None)
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 10)
- tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", 42)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 11)
- tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", 43)
-
- def test_cb_summer_winter(self):
- """Test _cb_summer_winter."""
- # initial -> None == Winter
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 30)
- self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic, self.slat_value_sun_dual._slat_characteristic_active)
- tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 50)
-
- # change to summer
- tests.helper.oh_item.item_state_change_event("Unittest_Summer", "ON")
- self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic_summer, self.slat_value_sun_dual._slat_characteristic_active)
- tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 60)
-
- # change to winter
- tests.helper.oh_item.item_state_change_event("Unittest_Summer", "OFF")
- self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic, self.slat_value_sun_dual._slat_characteristic_active)
- tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 50)
+ """Tests for SlatValueSun."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_SlatValueSingle", None) # NumberItem
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_SlatValueDual", None) # DimmerItem
+
+ self.config_single = habapp_rules.actors.config.shading.SlatValueConfig(
+ items=habapp_rules.actors.config.shading.SlatValueItems(
+ sun_elevation="Unittest_Elevation",
+ slat_value="Unittest_SlatValueSingle",
+ ),
+ parameter=habapp_rules.actors.config.shading.SlatValueParameter(
+ elevation_slat_characteristic=[
+ habapp_rules.actors.config.shading.ElevationSlatMapping(0, 100),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(10, 70),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(20, 60),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(30, 50),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(40, 50),
+ ]
+ ),
+ )
+
+ self.config_dual = habapp_rules.actors.config.shading.SlatValueConfig(
+ items=habapp_rules.actors.config.shading.SlatValueItems(
+ sun_elevation="Unittest_Elevation",
+ summer="Unittest_Summer",
+ slat_value="Unittest_SlatValueDual",
+ ),
+ parameter=habapp_rules.actors.config.shading.SlatValueParameter(
+ elevation_slat_characteristic=copy.deepcopy(self.config_single.parameter.elevation_slat_characteristic),
+ elevation_slat_characteristic_summer=[
+ habapp_rules.actors.config.shading.ElevationSlatMapping(0, 100),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(10, 80),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(20, 70),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(30, 60),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(40, 50),
+ habapp_rules.actors.config.shading.ElevationSlatMapping(50, 50),
+ ],
+ ),
+ )
+
+ self.slat_value_sun_single = habapp_rules.actors.shading.SlatValueSun(self.config_single)
+ self.slat_value_sun_dual = habapp_rules.actors.shading.SlatValueSun(self.config_dual)
+
+ def test__get_slat_value(self) -> None:
+ """Test __get_slat_value."""
+ tests.helper.oh_item.item_state_change_event("Unittest_Summer", "ON")
+ TestCase = collections.namedtuple("TestCase", "elevation, expected_result_single, , expected_result_dual")
+
+ test_cases = [
+ TestCase(-10, 100, 100),
+ TestCase(-1, 100, 100),
+ TestCase(0, 100, 100),
+ TestCase(1, 100, 100),
+ TestCase(9, 100, 100),
+ TestCase(10, 70, 80),
+ TestCase(11, 70, 80),
+ TestCase(29, 60, 70),
+ TestCase(30, 50, 60),
+ TestCase(31, 50, 60),
+ TestCase(39, 50, 60),
+ TestCase(40, 50, 50),
+ TestCase(41, 50, 50),
+ TestCase(60, 50, 50),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.assertEqual(test_case.expected_result_single, self.slat_value_sun_single._SlatValueSun__get_slat_value(test_case.elevation))
+ self.assertEqual(test_case.expected_result_dual, self.slat_value_sun_dual._SlatValueSun__get_slat_value(test_case.elevation))
+
+ def test_cb_elevation(self) -> None:
+ """Test _cb_elevation."""
+ with unittest.mock.patch.object(self.slat_value_sun_single, "_SlatValueSun__get_slat_value", side_effect=[42, 43]):
+ tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", None)
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 10)
+ tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", 42)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 11)
+ tests.helper.oh_item.assert_value("Unittest_SlatValueSingle", 43)
+
+ def test_cb_summer_winter(self) -> None:
+ """Test _cb_summer_winter."""
+ # initial -> None == Winter
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", 30)
+ self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic, self.slat_value_sun_dual._slat_characteristic_active)
+ tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 50)
+
+ # change to summer
+ tests.helper.oh_item.item_state_change_event("Unittest_Summer", "ON")
+ self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic_summer, self.slat_value_sun_dual._slat_characteristic_active)
+ tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 60)
+
+ # change to winter
+ tests.helper.oh_item.item_state_change_event("Unittest_Summer", "OFF")
+ self.assertEqual(self.config_dual.parameter.elevation_slat_characteristic, self.slat_value_sun_dual._slat_characteristic_active)
+ tests.helper.oh_item.assert_value("Unittest_SlatValueDual", 50)
diff --git a/tests/actors/state_observer.py b/tests/actors/state_observer.py
index 93cfa71..3eeb2e4 100644
--- a/tests/actors/state_observer.py
+++ b/tests/actors/state_observer.py
@@ -1,4 +1,5 @@
"""Test Presence rule."""
+
import collections
import unittest
import unittest.mock
@@ -12,573 +13,559 @@
import tests.helper.timer
-# pylint: disable=protected-access
class TestStateObserverSwitch(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing StateObserver for switch item."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
-
- self._cb_on = unittest.mock.MagicMock()
- self._cb_off = unittest.mock.MagicMock()
- self._observer_switch = habapp_rules.actors.state_observer.StateObserverSwitch("Unittest_Switch", cb_on=self._cb_on, cb_off=self._cb_off)
-
- def test_command_from_habapp(self):
- """Test HABApp rule triggers a command -> no manual should be detected."""
-
- for value in ["OFF", "OFF", "ON", "ON", "OFF"]:
- self._observer_switch.send_command(value)
- tests.helper.oh_item.item_command_event("Unittest_Switch", value)
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", value)
- self._cb_on.assert_not_called()
- self._cb_off.assert_not_called()
-
- def test_manu_from_openhab(self):
- """Test manual detection from openHAB."""
- TestCase = collections.namedtuple("TestCase", "command, cb_on_called, cb_off_called")
-
- test_cases = [
- TestCase("ON", True, False),
- TestCase("ON", False, False),
- TestCase("OFF", False, True),
- TestCase("OFF", False, False),
- TestCase("ON", True, False),
- ]
-
- for test_case in test_cases:
- self._cb_on.reset_mock()
- self._cb_off.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
-
- self.assertEqual(test_case.cb_on_called, self._cb_on.called)
- self.assertEqual(test_case.cb_off_called, self._cb_off.called)
-
- if test_case.cb_on_called:
- self._cb_on.assert_called_with(unittest.mock.ANY)
- if test_case.cb_off_called:
- self._cb_off.assert_called_with(unittest.mock.ANY)
-
- def test_manu_from_extern(self):
- """Test manual detection from extern."""
- TestCase = collections.namedtuple("TestCase", "command, cb_on_called, cb_off_called")
-
- test_cases = [
- TestCase("ON", True, False),
- TestCase("ON", False, False),
- TestCase("OFF", False, True),
- TestCase("OFF", False, False),
- TestCase("ON", True, False),
- ]
-
- for test_case in test_cases:
- self._cb_on.reset_mock()
- self._cb_off.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
-
- self.assertEqual(test_case.cb_on_called, self._cb_on.called)
- self.assertEqual(test_case.cb_off_called, self._cb_off.called)
- if test_case.cb_on_called:
- self._cb_on.assert_called_with(unittest.mock.ANY)
- if test_case.cb_off_called:
- self._cb_off.assert_called_with(unittest.mock.ANY)
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
- self.assertEqual(test_case.command == "ON", self._observer_switch.value)
-
- def test_send_command_exception(self):
- """Test if correct exceptions is raised."""
- with self.assertRaises(ValueError):
- self._observer_switch.send_command(2)
-
-
-# pylint: disable=protected-access, no-member
+ """Tests cases for testing StateObserver for switch item."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
+
+ self._cb_on = unittest.mock.MagicMock()
+ self._cb_off = unittest.mock.MagicMock()
+ self._observer_switch = habapp_rules.actors.state_observer.StateObserverSwitch("Unittest_Switch", cb_on=self._cb_on, cb_off=self._cb_off)
+
+ def test_command_from_habapp(self) -> None:
+ """Test HABApp rule triggers a command -> no manual should be detected."""
+ for value in ["OFF", "OFF", "ON", "ON", "OFF"]:
+ self._observer_switch.send_command(value)
+ tests.helper.oh_item.item_command_event("Unittest_Switch", value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", value)
+ self._cb_on.assert_not_called()
+ self._cb_off.assert_not_called()
+
+ def test_manu_from_openhab(self) -> None:
+ """Test manual detection from openHAB."""
+ TestCase = collections.namedtuple("TestCase", "command, cb_on_called, cb_off_called")
+
+ test_cases = [
+ TestCase("ON", True, False),
+ TestCase("ON", False, False),
+ TestCase("OFF", False, True),
+ TestCase("OFF", False, False),
+ TestCase("ON", True, False),
+ ]
+
+ for test_case in test_cases:
+ self._cb_on.reset_mock()
+ self._cb_off.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
+
+ self.assertEqual(test_case.cb_on_called, self._cb_on.called)
+ self.assertEqual(test_case.cb_off_called, self._cb_off.called)
+
+ if test_case.cb_on_called:
+ self._cb_on.assert_called_with(unittest.mock.ANY)
+ if test_case.cb_off_called:
+ self._cb_off.assert_called_with(unittest.mock.ANY)
+
+ def test_manu_from_extern(self) -> None:
+ """Test manual detection from extern."""
+ TestCase = collections.namedtuple("TestCase", "command, cb_on_called, cb_off_called")
+
+ test_cases = [
+ TestCase("ON", True, False),
+ TestCase("ON", False, False),
+ TestCase("OFF", False, True),
+ TestCase("OFF", False, False),
+ TestCase("ON", True, False),
+ ]
+
+ for test_case in test_cases:
+ self._cb_on.reset_mock()
+ self._cb_off.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
+
+ self.assertEqual(test_case.cb_on_called, self._cb_on.called)
+ self.assertEqual(test_case.cb_off_called, self._cb_off.called)
+ if test_case.cb_on_called:
+ self._cb_on.assert_called_with(unittest.mock.ANY)
+ if test_case.cb_off_called:
+ self._cb_off.assert_called_with(unittest.mock.ANY)
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", test_case.command)
+ self.assertEqual(test_case.command == "ON", self._observer_switch.value)
+
+ def test_send_command_exception(self) -> None:
+ """Test if correct exceptions is raised."""
+ with self.assertRaises(ValueError):
+ self._observer_switch.send_command(2)
+
+
class TestStateObserverDimmer(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing StateObserver for dimmer item."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_ctr", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_ctr", None)
-
- self._cb_on = unittest.mock.MagicMock()
- self._cb_off = unittest.mock.MagicMock()
- self._cb_changed = unittest.mock.MagicMock()
- self._observer_dimmer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, cb_brightness_change=self._cb_changed, control_names=["Unittest_Dimmer_ctr"])
-
- def test_init(self):
- """Test init of StateObserverDimmer."""
- self.assertEqual([], self._observer_dimmer._StateObserverBase__group_items)
- self.assertEqual(1, len(self._observer_dimmer._StateObserverBase__control_items))
- self.assertEqual("Unittest_Dimmer_ctr", self._observer_dimmer._StateObserverBase__control_items[0].name)
-
- observer_dimmer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, cb_brightness_change=self._cb_changed, group_names=["Unittest_Dimmer_ctr"])
- self.assertEqual(1, len(observer_dimmer._StateObserverBase__group_items))
- self.assertEqual("Unittest_Dimmer_ctr", observer_dimmer._StateObserverBase__group_items[0].name)
- self.assertEqual([], observer_dimmer._StateObserverBase__control_items)
-
- def test__check_item_types(self):
- """Test if wrong item types are detected correctly."""
- with self.assertRaises(TypeError) as context:
- habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, control_names=["Unittest_Dimmer_ctr", "Unittest_Switch_ctr"])
- self.assertEqual("Found items with wrong item type. Expected: DimmerItem. Wrong: Unittest_Switch_ctr ", str(context.exception))
-
- def test_command_from_habapp(self):
- """Test HABApp rule triggers a command -> no manual should be detected."""
- for value in [100, 0, 30, 100, 0, "ON", "OFF", 0, 80]:
- self._observer_dimmer.send_command(value)
- tests.helper.oh_item.item_command_event("Unittest_Dimmer", value)
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", value)
- self._cb_on.assert_not_called()
- self._cb_off.assert_not_called()
- self._cb_changed.assert_not_called()
-
- def test_manu_from_ctr(self):
- """Test manual detection from control item."""
- TestCase = collections.namedtuple("TestCase", "command, state, cb_on_called")
-
- test_cases = [
- TestCase("INCREASE", 30, True),
- TestCase("INCREASE", 40, False),
- TestCase("DECREASE", 20, False)
- ]
-
- for test_case in test_cases:
- self._cb_on.reset_mock()
- self._cb_off.reset_mock()
-
- tests.helper.oh_item.item_command_event("Unittest_Dimmer_ctr", test_case.command)
-
- # cb_on called
- self.assertEqual(test_case.cb_on_called, self._cb_on.called)
- if test_case.cb_on_called:
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
-
- # cb_off not called
- self._cb_off.assert_not_called()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.state)
- self.assertEqual(test_case.state, self._observer_dimmer.value)
-
- def test_basic_behavior_on_knx(self):
- """Test basic behavior. Switch ON via KNX"""
- # === Switch ON via KNX button ===
- # set initial state
- self._cb_on.reset_mock()
- self._observer_dimmer._value = 0
- self._observer_dimmer._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 100)
- self.assertEqual(100, self._observer_dimmer.value)
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
-
- # === Switch ON via KNX value ===
- # set initial state
- self._cb_on.reset_mock()
- self._observer_dimmer._value = 0
- self._observer_dimmer._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 42)
- self.assertEqual(42, self._observer_dimmer.value)
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
-
- # === Switch ON via KNX from group ===
- # set initial state
- self._cb_on.reset_mock()
- self._observer_dimmer._value = 0
- self._observer_dimmer._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 80)
- self.assertEqual(80, self._observer_dimmer.value)
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
-
- # === Value via KNX from group ===
- # set initial state
- self._cb_on.reset_mock()
- self._observer_dimmer._value = 0
- self._observer_dimmer._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 60)
- self.assertEqual(60, self._observer_dimmer.value)
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
-
- def test_manu_from_openhab(self):
- """Test manual detection from control item."""
- TestCase = collections.namedtuple("TestCase", "command, state, cb_on_called, cb_off_called")
- self._observer_dimmer._StateObserverDimmer__last_received_value = 0
-
- test_cases = [
- TestCase(100, 100, True, False),
- TestCase(100, 100, False, False),
- TestCase(0, 0, False, True),
- TestCase("ON", 100.0, True, False),
- TestCase("OFF", 0.0, False, True),
- ]
-
- for test_case in test_cases:
- self._cb_on.reset_mock()
- self._cb_off.reset_mock()
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.command)
-
- self.assertEqual(test_case.cb_on_called, self._cb_on.called)
- self.assertEqual(test_case.cb_off_called, self._cb_off.called)
- if test_case.cb_on_called:
- self._cb_on.assert_called_once_with(unittest.mock.ANY)
- if test_case.cb_off_called:
- self._cb_off.assert_called_once_with(unittest.mock.ANY)
- tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.state)
- self.assertEqual(test_case.state, self._observer_dimmer.value)
-
- def test_check_manual(self):
- """Test method _check_manual."""
-
- TestCase = collections.namedtuple("TestCase", "event, current_value, on_called, off_called, change_called")
-
- test_cases = [
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", "ON"), 0, True, False, False),
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", "OFF"), 42, False, True, False),
-
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", 0), 0, False, False, False),
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 0, True, False, False),
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", 0), 42, False, True, False),
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 17, False, False, True),
- TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 80, False, False, True)
- ]
-
- with unittest.mock.patch.object(self._observer_dimmer, "_cb_on") as cb_on_mock, \
- unittest.mock.patch.object(self._observer_dimmer, "_cb_off") as cb_off_mock, \
- unittest.mock.patch.object(self._observer_dimmer, "_cb_brightness_change") as cb_change_mock:
- for test_case in test_cases:
- cb_on_mock.reset_mock()
- self._observer_dimmer._value = test_case.current_value
- cb_on_mock.reset_mock()
- cb_off_mock.reset_mock()
- cb_change_mock.reset_mock()
-
- self._observer_dimmer._check_manual(test_case.event)
-
- self.assertEqual(cb_on_mock.called, test_case.on_called)
- self.assertEqual(cb_off_mock.called, test_case.off_called)
- self.assertEqual(cb_change_mock.called, test_case.change_called)
-
- if test_case.on_called:
- cb_on_mock.assert_called_once_with(test_case.event)
- if test_case.off_called:
- cb_off_mock.assert_called_once_with(test_case.event)
- if test_case.change_called:
- cb_change_mock.assert_called_once_with(test_case.event)
-
- def test_cb_group_item(self):
- """Test _cb_group_item."""
- self._observer_dimmer._group_last_event = 0
- with unittest.mock.patch("time.time") as time_mock, unittest.mock.patch.object(self._observer_dimmer, "_check_manual") as check_manual_mock:
- time_mock.return_value = 10
- self._observer_dimmer._cb_group_item(HABApp.openhab.events.ItemStateUpdatedEvent("item_name", "ON"))
- time_mock.return_value = 10.2
- self._observer_dimmer._cb_group_item(HABApp.openhab.events.ItemStateUpdatedEvent("item_name", "ON"))
- check_manual_mock.assert_called_once()
-
- def test_send_command_exception(self):
- """Test if correct exceptions is raised."""
- with self.assertRaises(ValueError):
- self._observer_dimmer.send_command(None)
-
- with self.assertRaises(ValueError):
- self._observer_dimmer.send_command("dimmer")
-
-
-# pylint: disable=protected-access
+ """Tests cases for testing StateObserver for dimmer item."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_ctr", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_ctr", None)
+
+ self._cb_on = unittest.mock.MagicMock()
+ self._cb_off = unittest.mock.MagicMock()
+ self._cb_changed = unittest.mock.MagicMock()
+ self._observer_dimmer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, cb_brightness_change=self._cb_changed, control_names=["Unittest_Dimmer_ctr"])
+
+ def test_init(self) -> None:
+ """Test init of StateObserverDimmer."""
+ self.assertEqual([], self._observer_dimmer._StateObserverBase__group_items)
+ self.assertEqual(1, len(self._observer_dimmer._StateObserverBase__control_items))
+ self.assertEqual("Unittest_Dimmer_ctr", self._observer_dimmer._StateObserverBase__control_items[0].name)
+
+ observer_dimmer = habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, cb_brightness_change=self._cb_changed, group_names=["Unittest_Dimmer_ctr"])
+ self.assertEqual(1, len(observer_dimmer._StateObserverBase__group_items))
+ self.assertEqual("Unittest_Dimmer_ctr", observer_dimmer._StateObserverBase__group_items[0].name)
+ self.assertEqual([], observer_dimmer._StateObserverBase__control_items)
+
+ def test__check_item_types(self) -> None:
+ """Test if wrong item types are detected correctly."""
+ with self.assertRaises(TypeError) as context:
+ habapp_rules.actors.state_observer.StateObserverDimmer("Unittest_Dimmer", cb_on=self._cb_on, cb_off=self._cb_off, control_names=["Unittest_Dimmer_ctr", "Unittest_Switch_ctr"])
+ self.assertEqual("Found items with wrong item type. Expected: DimmerItem. Wrong: Unittest_Switch_ctr ", str(context.exception))
+
+ def test_command_from_habapp(self) -> None:
+ """Test HABApp rule triggers a command -> no manual should be detected."""
+ for value in [100, 0, 30, 100, 0, "ON", "OFF", 0, 80]:
+ self._observer_dimmer.send_command(value)
+ tests.helper.oh_item.item_command_event("Unittest_Dimmer", value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", value)
+ self._cb_on.assert_not_called()
+ self._cb_off.assert_not_called()
+ self._cb_changed.assert_not_called()
+
+ def test_manu_from_ctr(self) -> None:
+ """Test manual detection from control item."""
+ TestCase = collections.namedtuple("TestCase", "command, state, cb_on_called")
+
+ test_cases = [TestCase("INCREASE", 30, True), TestCase("INCREASE", 40, False), TestCase("DECREASE", 20, False)]
+
+ for test_case in test_cases:
+ self._cb_on.reset_mock()
+ self._cb_off.reset_mock()
+
+ tests.helper.oh_item.item_command_event("Unittest_Dimmer_ctr", test_case.command)
+
+ # cb_on called
+ self.assertEqual(test_case.cb_on_called, self._cb_on.called)
+ if test_case.cb_on_called:
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+
+ # cb_off not called
+ self._cb_off.assert_not_called()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.state)
+ self.assertEqual(test_case.state, self._observer_dimmer.value)
+
+ def test_basic_behavior_on_knx(self) -> None:
+ """Test basic behavior. Switch ON via KNX."""
+ # === Switch ON via KNX button ===
+ # set initial state
+ self._cb_on.reset_mock()
+ self._observer_dimmer._value = 0
+ self._observer_dimmer._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 100)
+ self.assertEqual(100, self._observer_dimmer.value)
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+
+ # === Switch ON via KNX value ===
+ # set initial state
+ self._cb_on.reset_mock()
+ self._observer_dimmer._value = 0
+ self._observer_dimmer._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 42)
+ self.assertEqual(42, self._observer_dimmer.value)
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+
+ # === Switch ON via KNX from group ===
+ # set initial state
+ self._cb_on.reset_mock()
+ self._observer_dimmer._value = 0
+ self._observer_dimmer._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 80)
+ self.assertEqual(80, self._observer_dimmer.value)
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+
+ # === Value via KNX from group ===
+ # set initial state
+ self._cb_on.reset_mock()
+ self._observer_dimmer._value = 0
+ self._observer_dimmer._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", 60)
+ self.assertEqual(60, self._observer_dimmer.value)
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+
+ def test_manu_from_openhab(self) -> None:
+ """Test manual detection from control item."""
+ TestCase = collections.namedtuple("TestCase", "command, state, cb_on_called, cb_off_called")
+ self._observer_dimmer._StateObserverDimmer__last_received_value = 0
+
+ test_cases = [
+ TestCase(100, 100, True, False),
+ TestCase(100, 100, False, False),
+ TestCase(0, 0, False, True),
+ TestCase("ON", 100.0, True, False),
+ TestCase("OFF", 0.0, False, True),
+ ]
+
+ for test_case in test_cases:
+ self._cb_on.reset_mock()
+ self._cb_off.reset_mock()
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.command)
+
+ self.assertEqual(test_case.cb_on_called, self._cb_on.called)
+ self.assertEqual(test_case.cb_off_called, self._cb_off.called)
+ if test_case.cb_on_called:
+ self._cb_on.assert_called_once_with(unittest.mock.ANY)
+ if test_case.cb_off_called:
+ self._cb_off.assert_called_once_with(unittest.mock.ANY)
+ tests.helper.oh_item.item_state_change_event("Unittest_Dimmer", test_case.state)
+ self.assertEqual(test_case.state, self._observer_dimmer.value)
+
+ def test_check_manual(self) -> None:
+ """Test method _check_manual."""
+ TestCase = collections.namedtuple("TestCase", "event, current_value, on_called, off_called, change_called")
+
+ test_cases = [
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", "ON"), 0, True, False, False),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", "OFF"), 42, False, True, False),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", 0), 0, False, False, False),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 0, True, False, False),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", 0), 42, False, True, False),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 17, False, False, True),
+ TestCase(HABApp.openhab.events.ItemCommandEvent("any", 42), 80, False, False, True),
+ ]
+
+ with (
+ unittest.mock.patch.object(self._observer_dimmer, "_cb_on") as cb_on_mock,
+ unittest.mock.patch.object(self._observer_dimmer, "_cb_off") as cb_off_mock,
+ unittest.mock.patch.object(self._observer_dimmer, "_cb_brightness_change") as cb_change_mock,
+ ):
+ for test_case in test_cases:
+ cb_on_mock.reset_mock()
+ self._observer_dimmer._value = test_case.current_value
+ cb_on_mock.reset_mock()
+ cb_off_mock.reset_mock()
+ cb_change_mock.reset_mock()
+
+ self._observer_dimmer._check_manual(test_case.event)
+
+ self.assertEqual(cb_on_mock.called, test_case.on_called)
+ self.assertEqual(cb_off_mock.called, test_case.off_called)
+ self.assertEqual(cb_change_mock.called, test_case.change_called)
+
+ if test_case.on_called:
+ cb_on_mock.assert_called_once_with(test_case.event)
+ if test_case.off_called:
+ cb_off_mock.assert_called_once_with(test_case.event)
+ if test_case.change_called:
+ cb_change_mock.assert_called_once_with(test_case.event)
+
+ def test_cb_group_item(self) -> None:
+ """Test _cb_group_item."""
+ self._observer_dimmer._group_last_event = 0
+ with unittest.mock.patch("time.time") as time_mock, unittest.mock.patch.object(self._observer_dimmer, "_check_manual") as check_manual_mock:
+ time_mock.return_value = 10
+ self._observer_dimmer._cb_group_item(HABApp.openhab.events.ItemStateUpdatedEvent("item_name", "ON"))
+ time_mock.return_value = 10.2
+ self._observer_dimmer._cb_group_item(HABApp.openhab.events.ItemStateUpdatedEvent("item_name", "ON"))
+ check_manual_mock.assert_called_once()
+
+ def test_send_command_exception(self) -> None:
+ """Test if correct exceptions is raised."""
+ with self.assertRaises(ValueError):
+ self._observer_dimmer.send_command(None)
+
+ with self.assertRaises(ValueError):
+ self._observer_dimmer.send_command("dimmer")
+
+
class TestStateObserverRollerShutter(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing StateObserver for switch item."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter_ctr", None)
-
- self._cb_manual = unittest.mock.MagicMock()
- self._observer_jalousie = habapp_rules.actors.state_observer.StateObserverRollerShutter("Unittest_RollerShutter", cb_manual=self._cb_manual, control_names=["Unittest_RollerShutter_ctr"])
-
- def test_init(self):
- """Test init of StateObserverDimmer."""
- self.assertEqual([], self._observer_jalousie._StateObserverBase__group_items)
- self.assertEqual(1, len(self._observer_jalousie._StateObserverBase__control_items))
- self.assertEqual("Unittest_RollerShutter_ctr", self._observer_jalousie._StateObserverBase__control_items[0].name)
-
- observer_jalousie = habapp_rules.actors.state_observer.StateObserverRollerShutter("Unittest_RollerShutter", cb_manual=self._cb_manual, group_names=["Unittest_RollerShutter_ctr"])
- self.assertEqual(1, len(observer_jalousie._StateObserverBase__group_items))
- self.assertEqual("Unittest_RollerShutter_ctr", observer_jalousie._StateObserverBase__group_items[0].name)
- self.assertEqual([], observer_jalousie._StateObserverBase__control_items)
-
- def test_command_from_habapp(self):
- """Test HABApp rule triggers a command -> no manual should be detected."""
- for value in [100, 0, 30, 100.0, 0.0]:
- self._observer_jalousie.send_command(value)
- tests.helper.oh_item.item_command_event("Unittest_RollerShutter", value)
- tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", value)
- self._cb_manual.assert_not_called()
-
- def test_command_from_habapp_exception(self):
- """Test HABApp rule triggers a command with wrong type"""
- with self.assertRaises(ValueError):
- self._observer_jalousie.send_command("UP")
- self._cb_manual.assert_not_called()
-
- def test_manu_from_ctr(self):
- """Test manual detection from control item."""
- TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
-
- test_cases = [
- TestCase("DOWN", True),
- TestCase("DOWN", True),
- TestCase("UP", True),
- TestCase("UP", True),
- TestCase(0, False),
- TestCase(100, False),
- ]
-
- for test_case in test_cases:
- self._cb_manual.reset_mock()
-
- tests.helper.oh_item.item_command_event("Unittest_RollerShutter_ctr", test_case.command)
-
- self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
- if test_case.cb_manual_called:
- self._cb_manual.assert_called_once_with(unittest.mock.ANY)
-
- def test_basic_behavior_on_knx(self):
- """Test basic behavior. Switch ON via KNX"""
- # === Switch ON via KNX button ===
- # set initial state
- self._cb_manual.reset_mock()
- self._observer_jalousie._value = 0
- self._observer_jalousie._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 100)
- self.assertEqual(100, self._observer_jalousie.value)
- self._cb_manual.assert_called_once_with(unittest.mock.ANY)
-
- # === Switch ON via KNX value ===
- # set initial state
- self._cb_manual.reset_mock()
- self._observer_jalousie._value = 0
- self._observer_jalousie._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 42)
- self.assertEqual(42, self._observer_jalousie.value)
- self._cb_manual.assert_called_once_with(unittest.mock.ANY)
-
- # === Switch ON via KNX from group ===
- # set initial state
- self._cb_manual.reset_mock()
- self._observer_jalousie._value = 0
- self._observer_jalousie._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 80)
- self.assertEqual(80, self._observer_jalousie.value)
- self._cb_manual.assert_called_once_with(unittest.mock.ANY)
-
- # === Value via KNX from group ===
- # set initial state
- self._cb_manual.reset_mock()
- self._observer_jalousie._value = 0
- self._observer_jalousie._StateObserverDimmer__last_received_value = 0
- # In real system, this command is triggered about 2 sec later
- tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 60)
- self.assertEqual(60, self._observer_jalousie.value)
- self._cb_manual.assert_called_once_with(unittest.mock.ANY)
-
- def test_check_manual(self):
- """Test _check_manual."""
- TestCase = collections.namedtuple("TestCase", "event, value, tolerance, cb_called")
-
- test_cases = [
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", None, None), 0, 0, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 0, None), None, 0, False),
-
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 0, None), 0, 0, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 10, None), 10, 0, False),
-
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 1, None), 0, 0, True),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 10, None), 0, 0, True),
-
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 1, None), 0, 2, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 2, None), 0, 2, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 3, None), 0, 2, True),
-
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 9, None), 10, 2, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 8, None), 10, 2, False),
- TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 7, None), 10, 2, True)
- ]
-
- with unittest.mock.patch.object(self._observer_jalousie, "_trigger_callback") as trigger_callback_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- trigger_callback_mock.reset_mock()
- self._observer_jalousie._value = test_case.value
- self._observer_jalousie._value_tolerance = test_case.tolerance
-
- self._observer_jalousie._check_manual(test_case.event)
-
- self.assertEqual(test_case.cb_called, trigger_callback_mock.called)
+ """Tests cases for testing StateObserver for switch item."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter_ctr", None)
+
+ self._cb_manual = unittest.mock.MagicMock()
+ self._observer_jalousie = habapp_rules.actors.state_observer.StateObserverRollerShutter("Unittest_RollerShutter", cb_manual=self._cb_manual, control_names=["Unittest_RollerShutter_ctr"])
+
+ def test_init(self) -> None:
+ """Test init of StateObserverDimmer."""
+ self.assertEqual([], self._observer_jalousie._StateObserverBase__group_items)
+ self.assertEqual(1, len(self._observer_jalousie._StateObserverBase__control_items))
+ self.assertEqual("Unittest_RollerShutter_ctr", self._observer_jalousie._StateObserverBase__control_items[0].name)
+
+ observer_jalousie = habapp_rules.actors.state_observer.StateObserverRollerShutter("Unittest_RollerShutter", cb_manual=self._cb_manual, group_names=["Unittest_RollerShutter_ctr"])
+ self.assertEqual(1, len(observer_jalousie._StateObserverBase__group_items))
+ self.assertEqual("Unittest_RollerShutter_ctr", observer_jalousie._StateObserverBase__group_items[0].name)
+ self.assertEqual([], observer_jalousie._StateObserverBase__control_items)
+
+ def test_command_from_habapp(self) -> None:
+ """Test HABApp rule triggers a command -> no manual should be detected."""
+ for value in [100, 0, 30, 100.0, 0.0]:
+ self._observer_jalousie.send_command(value)
+ tests.helper.oh_item.item_command_event("Unittest_RollerShutter", value)
+ tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", value)
+ self._cb_manual.assert_not_called()
+
+ def test_command_from_habapp_exception(self) -> None:
+ """Test HABApp rule triggers a command with wrong type."""
+ with self.assertRaises(TypeError):
+ self._observer_jalousie.send_command("UP")
+ self._cb_manual.assert_not_called()
+
+ def test_manu_from_ctr(self) -> None:
+ """Test manual detection from control item."""
+ TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
+
+ test_cases = [
+ TestCase("DOWN", True),
+ TestCase("DOWN", True),
+ TestCase("UP", True),
+ TestCase("UP", True),
+ TestCase(0, False),
+ TestCase(100, False),
+ ]
+
+ for test_case in test_cases:
+ self._cb_manual.reset_mock()
+
+ tests.helper.oh_item.item_command_event("Unittest_RollerShutter_ctr", test_case.command)
+
+ self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
+ if test_case.cb_manual_called:
+ self._cb_manual.assert_called_once_with(unittest.mock.ANY)
+
+ def test_basic_behavior_on_knx(self) -> None:
+ """Test basic behavior. Switch ON via KNX."""
+ # === Switch ON via KNX button ===
+ # set initial state
+ self._cb_manual.reset_mock()
+ self._observer_jalousie._value = 0
+ self._observer_jalousie._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 100)
+ self.assertEqual(100, self._observer_jalousie.value)
+ self._cb_manual.assert_called_once_with(unittest.mock.ANY)
+
+ # === Switch ON via KNX value ===
+ # set initial state
+ self._cb_manual.reset_mock()
+ self._observer_jalousie._value = 0
+ self._observer_jalousie._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 42)
+ self.assertEqual(42, self._observer_jalousie.value)
+ self._cb_manual.assert_called_once_with(unittest.mock.ANY)
+
+ # === Switch ON via KNX from group ===
+ # set initial state
+ self._cb_manual.reset_mock()
+ self._observer_jalousie._value = 0
+ self._observer_jalousie._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 80)
+ self.assertEqual(80, self._observer_jalousie.value)
+ self._cb_manual.assert_called_once_with(unittest.mock.ANY)
+
+ # === Value via KNX from group ===
+ # set initial state
+ self._cb_manual.reset_mock()
+ self._observer_jalousie._value = 0
+ self._observer_jalousie._StateObserverDimmer__last_received_value = 0
+ # In real system, this command is triggered about 2 sec later
+ tests.helper.oh_item.item_state_change_event("Unittest_RollerShutter", 60)
+ self.assertEqual(60, self._observer_jalousie.value)
+ self._cb_manual.assert_called_once_with(unittest.mock.ANY)
+
+ def test_check_manual(self) -> None:
+ """Test _check_manual."""
+ TestCase = collections.namedtuple("TestCase", "event, value, tolerance, cb_called")
+
+ test_cases = [
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", None, None), 0, 0, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 0, None), None, 0, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 0, None), 0, 0, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 10, None), 10, 0, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 1, None), 0, 0, True),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 10, None), 0, 0, True),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 1, None), 0, 2, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 2, None), 0, 2, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 3, None), 0, 2, True),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 9, None), 10, 2, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 8, None), 10, 2, False),
+ TestCase(HABApp.openhab.events.ItemStateChangedEvent("any", 7, None), 10, 2, True),
+ ]
+
+ with unittest.mock.patch.object(self._observer_jalousie, "_trigger_callback") as trigger_callback_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ trigger_callback_mock.reset_mock()
+ self._observer_jalousie._value = test_case.value
+ self._observer_jalousie._value_tolerance = test_case.tolerance
+
+ self._observer_jalousie._check_manual(test_case.event)
+
+ self.assertEqual(test_case.cb_called, trigger_callback_mock.called)
class TestStateObserverNumber(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing StateObserver for number item."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
-
- self._cb_manual = unittest.mock.MagicMock()
- self._observer_number = habapp_rules.actors.state_observer.StateObserverNumber("Unittest_Number", self._cb_manual, value_tolerance=0.1)
-
- def test_command_from_habapp(self):
- """Test HABApp rule triggers a command -> no manual should be detected."""
- for value in [0, 0, 42, 42, 0]:
- self._observer_number.send_command(value)
- tests.helper.oh_item.item_command_event("Unittest_Number", value)
- tests.helper.oh_item.item_state_change_event("Unittest_Number", value)
- self._cb_manual.assert_not_called()
-
- def test_manu_from_openhab(self):
- """Test manual detection from openHAB."""
- TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
-
- test_cases = [
- TestCase(0, False),
- TestCase(42, True),
- TestCase(0, True),
- TestCase(0, False),
- TestCase(1000, True),
- ]
-
- for test_case in test_cases:
- self._cb_manual.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
-
- self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
- if test_case.cb_manual_called:
- self._cb_manual.assert_called_with(unittest.mock.ANY)
-
- def test_manu_from_extern(self):
- """Test manual detection from extern."""
- TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
-
- test_cases = [
- TestCase(0, False),
- TestCase(42, True),
- TestCase(0, True),
- TestCase(0, False),
- TestCase(1000, True),
- ]
-
- for test_case in test_cases:
- self._cb_manual.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
-
- self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
- if test_case.cb_manual_called:
- self._cb_manual.assert_called_with(unittest.mock.ANY)
- tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
- self.assertEqual(test_case.command, self._observer_number.value)
-
- def test_check_manual(self):
- """Test _check_manual."""
- TestCase = collections.namedtuple("TestCase", "last_value, new_value, manual_expected")
-
- test_cases = [
- # same value -> False
- TestCase(0, 0, False),
- TestCase(1, 1, False),
- TestCase(100, 100, False),
-
- # diff < 0.1 -> False
- TestCase(1, 0.9, False),
- TestCase(1, 1.09, False),
- TestCase(0.9, 1, False),
- TestCase(1.09, 1, False),
- TestCase(0, -0.1, False),
- TestCase(0, 0.1, False),
- TestCase(-0.1, 0, False),
- TestCase(0.1, 0, False),
-
- # diff > 0.1 -> True
- TestCase(0, 0.2, True),
- TestCase(0, -0.2, True),
- TestCase(0.2, 0, True),
- TestCase(-0.2, 0, True),
- ]
-
- with unittest.mock.patch.object(self._observer_number, "_trigger_callback") as trigger_cb_mock:
- for test_case in test_cases:
- with self.subTest(test_case = test_case):
- trigger_cb_mock.reset_mock()
- self._observer_number._value = test_case.last_value
- self._observer_number._check_manual(HABApp.openhab.events.ItemStateChangedEvent("some_name", test_case.new_value, None))
- if test_case.manual_expected:
- trigger_cb_mock.assert_called_once()
- else:
- trigger_cb_mock.assert_not_called()
-
- def test_send_command_exception(self):
- """Test if correct exceptions is raised."""
- with self.assertRaises(ValueError):
- self._observer_number.send_command("ON")
+ """Tests cases for testing StateObserver for number item."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
+
+ self._cb_manual = unittest.mock.MagicMock()
+ self._observer_number = habapp_rules.actors.state_observer.StateObserverNumber("Unittest_Number", self._cb_manual, value_tolerance=0.1)
+
+ def test_command_from_habapp(self) -> None:
+ """Test HABApp rule triggers a command -> no manual should be detected."""
+ for value in [0, 0, 42, 42, 0]:
+ self._observer_number.send_command(value)
+ tests.helper.oh_item.item_command_event("Unittest_Number", value)
+ tests.helper.oh_item.item_state_change_event("Unittest_Number", value)
+ self._cb_manual.assert_not_called()
+
+ def test_manu_from_openhab(self) -> None:
+ """Test manual detection from openHAB."""
+ TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
+
+ test_cases = [
+ TestCase(0, False),
+ TestCase(42, True),
+ TestCase(0, True),
+ TestCase(0, False),
+ TestCase(1000, True),
+ ]
+
+ for test_case in test_cases:
+ self._cb_manual.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
+
+ self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
+ if test_case.cb_manual_called:
+ self._cb_manual.assert_called_with(unittest.mock.ANY)
+
+ def test_manu_from_extern(self) -> None:
+ """Test manual detection from extern."""
+ TestCase = collections.namedtuple("TestCase", "command, cb_manual_called")
+
+ test_cases = [
+ TestCase(0, False),
+ TestCase(42, True),
+ TestCase(0, True),
+ TestCase(0, False),
+ TestCase(1000, True),
+ ]
+
+ for test_case in test_cases:
+ self._cb_manual.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
+
+ self.assertEqual(test_case.cb_manual_called, self._cb_manual.called)
+ if test_case.cb_manual_called:
+ self._cb_manual.assert_called_with(unittest.mock.ANY)
+ tests.helper.oh_item.item_state_change_event("Unittest_Number", test_case.command)
+ self.assertEqual(test_case.command, self._observer_number.value)
+
+ def test_check_manual(self) -> None:
+ """Test _check_manual."""
+ TestCase = collections.namedtuple("TestCase", "last_value, new_value, manual_expected")
+
+ test_cases = [
+ # same value -> False
+ TestCase(0, 0, False),
+ TestCase(1, 1, False),
+ TestCase(100, 100, False),
+ # diff < 0.1 -> False
+ TestCase(1, 0.9, False),
+ TestCase(1, 1.09, False),
+ TestCase(0.9, 1, False),
+ TestCase(1.09, 1, False),
+ TestCase(0, -0.1, False),
+ TestCase(0, 0.1, False),
+ TestCase(-0.1, 0, False),
+ TestCase(0.1, 0, False),
+ # diff > 0.1 -> True
+ TestCase(0, 0.2, True),
+ TestCase(0, -0.2, True),
+ TestCase(0.2, 0, True),
+ TestCase(-0.2, 0, True),
+ ]
+
+ with unittest.mock.patch.object(self._observer_number, "_trigger_callback") as trigger_cb_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ trigger_cb_mock.reset_mock()
+ self._observer_number._value = test_case.last_value
+ self._observer_number._check_manual(HABApp.openhab.events.ItemStateChangedEvent("some_name", test_case.new_value, None))
+ if test_case.manual_expected:
+ trigger_cb_mock.assert_called_once()
+ else:
+ trigger_cb_mock.assert_not_called()
+
+ def test_send_command_exception(self) -> None:
+ """Test if correct exceptions is raised."""
+ with self.assertRaises(TypeError):
+ self._observer_number.send_command("ON")
class TestStateObserverSlat(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing StateObserver for number / dimmer item used as slat item."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
- self._cb_manual = unittest.mock.MagicMock()
- self._observer_slat = habapp_rules.actors.state_observer.StateObserverSlat("Unittest_Dimmer", self._cb_manual)
-
- def test_check_manual(self):
- """test _check_manual."""
- # value == 0
- with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
- self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 0, 42))
- timer_mock.assert_called_once_with(3, self._observer_slat._StateObserverSlat__cb_check_manual_delayed, [event])
- base_check_manual_mock.assert_not_called()
-
- # value == 100
- with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
- self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 100, 42))
- timer_mock.assert_called_once_with(3, self._observer_slat._StateObserverSlat__cb_check_manual_delayed, [event])
- base_check_manual_mock.assert_not_called()
-
- # other value | timer not running
- with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
- self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 80, 42))
- timer_mock.assert_not_called()
- base_check_manual_mock.assert_called_once_with(self._observer_slat, event)
-
- def test__cb_check_manual_delayed(self):
- """Test __cb_check_manual_delayed."""
- event_mock = unittest.mock.MagicMock()
- with unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
- self._observer_slat._StateObserverSlat__cb_check_manual_delayed(event_mock)
- base_check_manual_mock.assert_called_once_with(self._observer_slat, event_mock)
-
- def test_on_rule_removed(self):
- """Test on_rule_removed."""
- with unittest.mock.patch.object(self._observer_slat, "_stop_timer_manual") as stop_timer_mock:
- self._observer_slat.on_rule_removed()
- stop_timer_mock.assert_called_once()
+ """Tests cases for testing StateObserver for number / dimmer item used as slat item."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
+ self._cb_manual = unittest.mock.MagicMock()
+ self._observer_slat = habapp_rules.actors.state_observer.StateObserverSlat("Unittest_Dimmer", self._cb_manual)
+
+ def test_check_manual(self) -> None:
+ """Test _check_manual."""
+ # value is 0
+ with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
+ self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 0, 42))
+ timer_mock.assert_called_once_with(3, self._observer_slat._StateObserverSlat__cb_check_manual_delayed, [event])
+ base_check_manual_mock.assert_not_called()
+
+ # value is 100
+ with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
+ self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 100, 42))
+ timer_mock.assert_called_once_with(3, self._observer_slat._StateObserverSlat__cb_check_manual_delayed, [event])
+ base_check_manual_mock.assert_not_called()
+
+ # other value | timer not running
+ with unittest.mock.patch("threading.Timer") as timer_mock, unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
+ self._observer_slat._check_manual(event := HABApp.openhab.events.ItemStateChangedEvent("any", 80, 42))
+ timer_mock.assert_not_called()
+ base_check_manual_mock.assert_called_once_with(self._observer_slat, event)
+
+ def test__cb_check_manual_delayed(self) -> None:
+ """Test __cb_check_manual_delayed."""
+ event_mock = unittest.mock.MagicMock()
+ with unittest.mock.patch("habapp_rules.actors.state_observer.StateObserverNumber._check_manual") as base_check_manual_mock:
+ self._observer_slat._StateObserverSlat__cb_check_manual_delayed(event_mock)
+ base_check_manual_mock.assert_called_once_with(self._observer_slat, event_mock)
+
+ def test_on_rule_removed(self) -> None:
+ """Test on_rule_removed."""
+ with unittest.mock.patch.object(self._observer_slat, "_stop_timer_manual") as stop_timer_mock:
+ self._observer_slat.on_rule_removed()
+ stop_timer_mock.assert_called_once()
diff --git a/tests/actors/ventilation.py b/tests/actors/ventilation.py
index 13d03b8..6f3594d 100644
--- a/tests/actors/ventilation.py
+++ b/tests/actors/ventilation.py
@@ -1,7 +1,7 @@
"""Test ventilation rules."""
+
import collections
import datetime
-import os
import pathlib
import sys
import unittest
@@ -25,914 +25,850 @@
import tests.helper.timer
-# pylint: disable=protected-access,no-member,too-many-public-methods
class TestVentilation(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing Ventilation."""
-
- def setUp(self) -> None:
- """Setup test case."""
- self.run_at_mock_patcher = unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.at")
- self.addCleanup(self.run_at_mock_patcher.stop)
- self.run_at_mock = self.run_at_mock_patcher.start()
-
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_min_level", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_level_state", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_max_level", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
-
- parameter_max = habapp_rules.actors.config.ventilation.VentilationParameter(
- state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
- state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
- state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
- state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
- state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18))
- )
-
- config_max = habapp_rules.actors.config.ventilation.VentilationConfig(
- items=habapp_rules.actors.config.ventilation.VentilationItems(
- ventilation_level="Unittest_Ventilation_max_level",
- manual="Unittest_Ventilation_max_manual",
- hand_request="Unittest_Ventilation_max_hand_request",
- external_request="Unittest_Ventilation_max_external_request",
- feedback_on="Unittest_Ventilation_max_feedback_on",
- feedback_power="Unittest_Ventilation_max_feedback_power",
- display_text="Unittest_Ventilation_max_display_text",
- presence_state="Unittest_Presence_state",
- state="Unittest_Ventilation_max_Custom_State"
- ),
- parameter=parameter_max
- )
-
- config_min = habapp_rules.actors.config.ventilation.VentilationConfig(
- items=habapp_rules.actors.config.ventilation.VentilationItems(
- ventilation_level="Unittest_Ventilation_min_level",
- manual="Unittest_Ventilation_min_manual",
- state="H_Unittest_Ventilation_min_level_state"
- ),
-
- )
-
- self.ventilation_min = habapp_rules.actors.ventilation.Ventilation(config_min)
- self.ventilation_max = habapp_rules.actors.ventilation.Ventilation(config_max)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Ventilation_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=self.ventilation_min.state,
- show_conditions=False)
-
- graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state not in ["Auto_Init"]]:
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=state_name,
- show_conditions=True)
- graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
-
- def test_init(self):
- """Test __init__."""
- # check timeouts
- self.assertEqual(3600, self.ventilation_min.state_machine.get_state("Auto_PowerHand").timeout)
- self.assertEqual(3600, self.ventilation_min.state_machine.get_state("Auto_LongAbsence_On").timeout)
-
- self.assertEqual(42 * 60, self.ventilation_max.state_machine.get_state("Auto_PowerHand").timeout)
- self.assertEqual(1800, self.ventilation_max.state_machine.get_state("Auto_LongAbsence_On").timeout)
-
- def test_get_initial_state(self):
- """Test getting initial state."""
- TestCase = collections.namedtuple("TestCase", "presence_state, manual, hand_request, external_request, expected_state_min, expected_state_max")
-
- test_cases = [
- # present
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, False, False, "Auto_Normal", "Auto_Normal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, True, True, "Manual", "Manual"),
-
- # long absence
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, False, False, "Auto_Normal", "Auto_LongAbsence"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, False, True, "Auto_Normal", "Auto_LongAbsence"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, True, True, "Manual", "Manual"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Ventilation_min_manual", "ON" if test_case.manual else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_manual", "ON" if test_case.manual else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_hand_request", "ON" if test_case.hand_request else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON" if test_case.external_request else "OFF")
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_state)
-
- # self.assertEqual(test_case.expected_state_min, self.ventilation_min._get_initial_state())
- self.assertEqual(test_case.expected_state_max, self.ventilation_max._get_initial_state())
-
- def test_set_level(self):
- """test _set_level."""
- TestCase = collections.namedtuple("TestCase", "state, expected_level")
-
- test_cases = [
- TestCase("Manual", None),
- TestCase("Auto_PowerHand", 102),
- TestCase("Auto_Normal", 101),
- TestCase("Auto_PowerExternal", 103),
- TestCase("Auto_LongAbsence_On", 105),
- TestCase("Auto_LongAbsence_Off", 0),
- TestCase("Auto_Init", None),
- ]
-
- with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- send_mock.reset_mock()
- self.ventilation_max.state = test_case.state
-
- self.ventilation_max._set_level()
-
- if test_case.expected_level is not None:
- send_mock.assert_called_once_with(self.ventilation_max._config.items.ventilation_level, test_case.expected_level)
- else:
- send_mock.assert_not_called()
-
- def test_set_feedback_states(self):
- """test _set_feedback_states."""
- TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
-
- test_cases = [
- TestCase(None, "Auto_PowerHand", False, False, "Hand Custom 42min"),
- TestCase(None, "Auto_Normal", False, False, "Normal Custom"),
- TestCase(None, "Auto_PowerExternal", False, False, "External Custom"),
- TestCase(None, "Auto_LongAbsence_On", False, False, "Absence Custom ON"),
- TestCase(None, "Auto_LongAbsence_Off", False, False, "Absence Custom OFF"),
- TestCase(None, "Auto_Init", False, False, "Absence Custom OFF"),
-
- TestCase(0, "Auto_PowerHand", False, False, "Hand Custom 42min"),
- TestCase(0, "Auto_Normal", False, False, "Normal Custom"),
- TestCase(0, "Auto_PowerExternal", False, False, "External Custom"),
- TestCase(0, "Auto_LongAbsence_On", False, False, "Absence Custom ON"),
- TestCase(0, "Auto_LongAbsence_Off", False, False, "Absence Custom OFF"),
- TestCase(0, "Auto_Init", False, False, "Absence Custom OFF"),
-
- TestCase(1, "Auto_PowerHand", True, False, "Hand Custom 42min"),
- TestCase(1, "Auto_Normal", True, False, "Normal Custom"),
- TestCase(1, "Auto_PowerExternal", True, False, "External Custom"),
- TestCase(1, "Auto_LongAbsence_On", True, False, "Absence Custom ON"),
- TestCase(1, "Auto_LongAbsence_Off", True, False, "Absence Custom OFF"),
- TestCase(1, "Auto_Init", True, False, "Absence Custom OFF"),
-
- TestCase(2, "Auto_PowerHand", True, True, "Hand Custom 42min"),
- TestCase(2, "Auto_Normal", True, True, "Normal Custom"),
- TestCase(2, "Auto_PowerExternal", True, True, "External Custom"),
- TestCase(2, "Auto_LongAbsence_On", True, True, "Absence Custom ON"),
- TestCase(2, "Auto_LongAbsence_Off", True, True, "Absence Custom OFF"),
- TestCase(2, "Auto_Init", True, True, "Absence Custom OFF"),
-
- TestCase(42, "Auto_PowerHand", True, True, "Hand Custom 42min"),
- TestCase(42, "Auto_Normal", True, True, "Normal Custom"),
- TestCase(42, "Auto_PowerExternal", True, True, "External Custom"),
- TestCase(42, "Auto_LongAbsence_On", True, True, "Absence Custom ON"),
- TestCase(42, "Auto_LongAbsence_Off", True, True, "Absence Custom OFF"),
- TestCase(42, "Auto_Init", True, True, "Absence Custom OFF"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.ventilation_min._ventilation_level = test_case.ventilation_level
- self.ventilation_max._ventilation_level = test_case.ventilation_level
- self.ventilation_min.state = test_case.state
- self.ventilation_max.state = test_case.state
-
- self.ventilation_min._set_feedback_states()
- self.ventilation_max._set_feedback_states()
-
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
-
- def test_on_enter_long_absence_off(self):
- """Test on_enter_Auto_LongAbsence_Off."""
- with unittest.mock.patch.object(self.ventilation_max, "_trigger_long_absence_power_on") as trigger_on_mock:
- self.ventilation_max.to_Auto_LongAbsence_Off()
- self.run_at_mock.assert_called_once_with(datetime.time(18), trigger_on_mock)
-
- def test_trigger_long_absence_power_on(self):
- """Test _trigger_long_absence_power_on."""
- with unittest.mock.patch.object(self.ventilation_max, "_long_absence_power_on") as power_on_mock:
- self.ventilation_max._trigger_long_absence_power_on()
- power_on_mock.assert_called_once()
-
- def test__set_hand_display_text(self):
- """test __set_hand_display_text."""
- # wrong state
- for state in ["Manual", "Auto_Normal", "Auto_PowerHumidity", "Auto_PowerDryer", "Auto_LongAbsence_On", "Auto_LongAbsence_Off"]:
- self.ventilation_max.state = state
-
- self.ventilation_max._VentilationBase__set_hand_display_text()
- self.run_at_mock.assert_not_called()
-
- # PowerHand state:
- TestCase = collections.namedtuple("TestCase", "changed_time, now_time, expected_display")
-
- test_cases = [
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0), "Hand Custom 42min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0, 30), "Hand Custom 42min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0, 31), "Hand Custom 41min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 2, 0), "Hand Custom 40min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 2, 31), "Hand Custom 39min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 42, 0), "Hand Custom 0min"),
- TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 45, 0), "Hand Custom 0min"),
- ]
-
- self.ventilation_max.state = "Auto_PowerHand"
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.ventilation_max._state_change_time = test_case.changed_time
- now_value = test_case.now_time
- self.run_at_mock.reset_mock()
- with unittest.mock.patch("datetime.datetime") as datetime_mock:
- datetime_mock.now.return_value = now_value
- self.ventilation_max._VentilationBase__set_hand_display_text()
- self.run_at_mock.assert_called_once()
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display)
-
- def test_external_active_and_configured(self):
- """test _external_active_and_configured."""
- self.assertFalse(self.ventilation_min._external_active_and_configured())
-
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "OFF")
- self.assertFalse(self.ventilation_max._external_active_and_configured())
-
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON")
- self.assertTrue(self.ventilation_max._external_active_and_configured())
-
- def test_auto_normal_transitions(self):
- """Test transitions of state Auto_Normal"""
- # to Auto_PowerHand
- self.ventilation_min.to_Auto_Normal()
- self.ventilation_max.to_Auto_Normal()
-
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
-
- # back to Auto_Normal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
-
- # to Auto_PowerExternal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
-
- # back to Auto_Normal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
-
- # to Auto_LongAbsence
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.LONG_ABSENCE.value)
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_LongAbsence_Off", self.ventilation_max.state)
+ """Tests cases for testing Ventilation."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ self.run_at_mock_patcher = unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.once")
+ self.addCleanup(self.run_at_mock_patcher.stop)
+ self.run_at_mock = self.run_at_mock_patcher.start()
+
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_min_level", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_level_state", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_max_level", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
+
+ parameter_max = habapp_rules.actors.config.ventilation.VentilationParameter(
+ state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
+ state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
+ state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
+ state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
+ state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18)),
+ )
+
+ config_max = habapp_rules.actors.config.ventilation.VentilationConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationItems(
+ ventilation_level="Unittest_Ventilation_max_level",
+ manual="Unittest_Ventilation_max_manual",
+ hand_request="Unittest_Ventilation_max_hand_request",
+ external_request="Unittest_Ventilation_max_external_request",
+ feedback_on="Unittest_Ventilation_max_feedback_on",
+ feedback_power="Unittest_Ventilation_max_feedback_power",
+ display_text="Unittest_Ventilation_max_display_text",
+ presence_state="Unittest_Presence_state",
+ state="Unittest_Ventilation_max_Custom_State",
+ ),
+ parameter=parameter_max,
+ )
+
+ config_min = habapp_rules.actors.config.ventilation.VentilationConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationItems(ventilation_level="Unittest_Ventilation_min_level", manual="Unittest_Ventilation_min_manual", state="H_Unittest_Ventilation_min_level_state"),
+ )
+
+ self.ventilation_min = habapp_rules.actors.ventilation.Ventilation(config_min)
+ self.ventilation_max = habapp_rules.actors.ventilation.Ventilation(config_max)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Ventilation"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=self.ventilation_min.state, show_conditions=False
+ )
+
+ graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state != "Auto_Init"]:
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=state_name, show_conditions=True)
+ graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ # check timeouts
+ self.assertEqual(3600, self.ventilation_min.state_machine.get_state("Auto_PowerHand").timeout)
+ self.assertEqual(3600, self.ventilation_min.state_machine.get_state("Auto_LongAbsence_On").timeout)
+
+ self.assertEqual(42 * 60, self.ventilation_max.state_machine.get_state("Auto_PowerHand").timeout)
+ self.assertEqual(1800, self.ventilation_max.state_machine.get_state("Auto_LongAbsence_On").timeout)
+
+ def test_get_initial_state(self) -> None:
+ """Test getting initial state."""
+ TestCase = collections.namedtuple("TestCase", "presence_state, manual, hand_request, external_request, expected_state_min, expected_state_max")
+
+ test_cases = [
+ # present
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, False, False, "Auto_Normal", "Auto_Normal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, True, True, True, "Manual", "Manual"),
+ # long absence
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, False, False, "Auto_Normal", "Auto_LongAbsence"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, False, True, "Auto_Normal", "Auto_LongAbsence"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, True, True, True, "Manual", "Manual"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Ventilation_min_manual", "ON" if test_case.manual else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_manual", "ON" if test_case.manual else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_hand_request", "ON" if test_case.hand_request else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON" if test_case.external_request else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_state)
+
+ self.assertEqual(test_case.expected_state_min, self.ventilation_min._get_initial_state())
+ self.assertEqual(test_case.expected_state_max, self.ventilation_max._get_initial_state())
+
+ def test_set_level(self) -> None:
+ """Test _set_level."""
+ TestCase = collections.namedtuple("TestCase", "state, expected_level")
+
+ test_cases = [
+ TestCase("Manual", None),
+ TestCase("Auto_PowerHand", 102),
+ TestCase("Auto_Normal", 101),
+ TestCase("Auto_PowerExternal", 103),
+ TestCase("Auto_LongAbsence_On", 105),
+ TestCase("Auto_LongAbsence_Off", 0),
+ TestCase("Auto_Init", None),
+ ]
+
+ with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ send_mock.reset_mock()
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_max._set_level()
+
+ if test_case.expected_level is not None:
+ send_mock.assert_called_once_with(self.ventilation_max._config.items.ventilation_level, test_case.expected_level)
+ else:
+ send_mock.assert_not_called()
+
+ def test_set_feedback_states(self) -> None:
+ """Test _set_feedback_states."""
+ TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
+
+ test_cases = [
+ TestCase(None, "Auto_PowerHand", False, False, "Hand Custom 42min"),
+ TestCase(None, "Auto_Normal", False, False, "Normal Custom"),
+ TestCase(None, "Auto_PowerExternal", False, False, "External Custom"),
+ TestCase(None, "Auto_LongAbsence_On", False, False, "Absence Custom ON"),
+ TestCase(None, "Auto_LongAbsence_Off", False, False, "Absence Custom OFF"),
+ TestCase(None, "Auto_Init", False, False, "Absence Custom OFF"),
+ TestCase(0, "Auto_PowerHand", False, False, "Hand Custom 42min"),
+ TestCase(0, "Auto_Normal", False, False, "Normal Custom"),
+ TestCase(0, "Auto_PowerExternal", False, False, "External Custom"),
+ TestCase(0, "Auto_LongAbsence_On", False, False, "Absence Custom ON"),
+ TestCase(0, "Auto_LongAbsence_Off", False, False, "Absence Custom OFF"),
+ TestCase(0, "Auto_Init", False, False, "Absence Custom OFF"),
+ TestCase(1, "Auto_PowerHand", True, False, "Hand Custom 42min"),
+ TestCase(1, "Auto_Normal", True, False, "Normal Custom"),
+ TestCase(1, "Auto_PowerExternal", True, False, "External Custom"),
+ TestCase(1, "Auto_LongAbsence_On", True, False, "Absence Custom ON"),
+ TestCase(1, "Auto_LongAbsence_Off", True, False, "Absence Custom OFF"),
+ TestCase(1, "Auto_Init", True, False, "Absence Custom OFF"),
+ TestCase(2, "Auto_PowerHand", True, True, "Hand Custom 42min"),
+ TestCase(2, "Auto_Normal", True, True, "Normal Custom"),
+ TestCase(2, "Auto_PowerExternal", True, True, "External Custom"),
+ TestCase(2, "Auto_LongAbsence_On", True, True, "Absence Custom ON"),
+ TestCase(2, "Auto_LongAbsence_Off", True, True, "Absence Custom OFF"),
+ TestCase(2, "Auto_Init", True, True, "Absence Custom OFF"),
+ TestCase(42, "Auto_PowerHand", True, True, "Hand Custom 42min"),
+ TestCase(42, "Auto_Normal", True, True, "Normal Custom"),
+ TestCase(42, "Auto_PowerExternal", True, True, "External Custom"),
+ TestCase(42, "Auto_LongAbsence_On", True, True, "Absence Custom ON"),
+ TestCase(42, "Auto_LongAbsence_Off", True, True, "Absence Custom OFF"),
+ TestCase(42, "Auto_Init", True, True, "Absence Custom OFF"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.ventilation_min._ventilation_level = test_case.ventilation_level
+ self.ventilation_max._ventilation_level = test_case.ventilation_level
+ self.ventilation_min.state = test_case.state
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_min._set_feedback_states()
+ self.ventilation_max._set_feedback_states()
+
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
+
+ def test_on_enter_long_absence_off(self) -> None:
+ """Test on_enter_Auto_LongAbsence_Off."""
+ with unittest.mock.patch.object(self.ventilation_max, "_trigger_long_absence_power_on") as trigger_on_mock:
+ self.ventilation_max.to_Auto_LongAbsence_Off()
+ self.run_at_mock.assert_called_once_with(datetime.time(18), trigger_on_mock)
+
+ def test_trigger_long_absence_power_on(self) -> None:
+ """Test _trigger_long_absence_power_on."""
+ with unittest.mock.patch.object(self.ventilation_max, "_long_absence_power_on") as power_on_mock:
+ self.ventilation_max._trigger_long_absence_power_on()
+ power_on_mock.assert_called_once()
+
+ def test__set_hand_display_text(self) -> None:
+ """Test __set_hand_display_text."""
+ # wrong state
+ for state in ["Manual", "Auto_Normal", "Auto_PowerHumidity", "Auto_PowerDryer", "Auto_LongAbsence_On", "Auto_LongAbsence_Off"]:
+ self.ventilation_max.state = state
+
+ self.ventilation_max._VentilationBase__set_hand_display_text()
+ self.run_at_mock.assert_not_called()
+
+ # PowerHand state:
+ TestCase = collections.namedtuple("TestCase", "changed_time, now_time, expected_display")
+
+ test_cases = [
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0), "Hand Custom 42min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0, 30), "Hand Custom 42min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 0, 31), "Hand Custom 41min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 2, 0), "Hand Custom 40min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 2, 31), "Hand Custom 39min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 42, 0), "Hand Custom 0min"),
+ TestCase(datetime.datetime(2024, 1, 1, 12), datetime.datetime(2024, 1, 1, 12, 45, 0), "Hand Custom 0min"),
+ ]
+
+ self.ventilation_max.state = "Auto_PowerHand"
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.ventilation_max._state_change_time = test_case.changed_time
+ now_value = test_case.now_time
+ self.run_at_mock.reset_mock()
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ datetime_mock.now.return_value = now_value
+ self.ventilation_max._VentilationBase__set_hand_display_text()
+ self.run_at_mock.assert_called_once()
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display)
+
+ def test_external_active_and_configured(self) -> None:
+ """Test _external_active_and_configured."""
+ self.assertFalse(self.ventilation_min._external_active_and_configured())
+
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "OFF")
+ self.assertFalse(self.ventilation_max._external_active_and_configured())
+
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON")
+ self.assertTrue(self.ventilation_max._external_active_and_configured())
+
+ def test_auto_normal_transitions(self) -> None:
+ """Test transitions of state Auto_Normal."""
+ # to Auto_PowerHand
+ self.ventilation_min.to_Auto_Normal()
+ self.ventilation_max.to_Auto_Normal()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+
+ # back to Auto_Normal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+
+ # to Auto_PowerExternal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+
+ # back to Auto_Normal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+
+ # to Auto_LongAbsence
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.LONG_ABSENCE.value)
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_LongAbsence_Off", self.ventilation_max.state)
- # back to Auto_Normal
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
-
- def test_auto_power_external_transitions(self):
- """Test transitions of state Auto_PowerExternal"""
- # to Auto_PowerExternal
- self.ventilation_max.to_Auto_PowerExternal()
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ # back to Auto_Normal
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.PRESENCE.value)
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+
+ def test_auto_power_external_transitions(self) -> None:
+ """Test transitions of state Auto_PowerExternal."""
+ # to Auto_PowerExternal
+ self.ventilation_max.to_Auto_PowerExternal()
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
- # back to AutoPowerExternal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+ # back to AutoPowerExternal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
- # to Auto_PowerHand
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ # to Auto_PowerHand
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- # back to AutoPowerExternal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+ # back to AutoPowerExternal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
- # to Auto_LongAbsence
- tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.LONG_ABSENCE.value)
- self.assertEqual("Auto_LongAbsence_Off", self.ventilation_max.state)
+ # to Auto_LongAbsence
+ tests.helper.oh_item.item_state_change_event("Unittest_Presence_state", habapp_rules.system.PresenceState.LONG_ABSENCE.value)
+ self.assertEqual("Auto_LongAbsence_Off", self.ventilation_max.state)
- def test_auto_power_hand_transitions(self):
- """Test transitions of state Auto_PowerHand"""
- # set Auto_LongAbsence as initial state
- self.ventilation_max.to_Auto_LongAbsence_On()
+ def test_auto_power_hand_transitions(self) -> None:
+ """Test transitions of state Auto_PowerHand."""
+ # set Auto_LongAbsence as initial state
+ self.ventilation_max.to_Auto_LongAbsence_On()
- # to Auto_PowerHand
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ # to Auto_PowerHand
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- # to Auto_Normal (external request is not ON)
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ # to Auto_Normal (external request is not ON)
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
- # back to Auto_PowerHand
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ # back to Auto_PowerHand
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- # to Auto_PowerExternal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+ # to Auto_PowerExternal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
- # back to Auto_PowerHand
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ # back to Auto_PowerHand
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- # timeout
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_hand_request", "OFF")
+ # timeout
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_hand_request", "OFF")
- def test_manual_transitions(self):
- """Test transitions of state Manual."""
- # set Auto as initial state
- self.ventilation_min.to_Auto_Normal()
- self.ventilation_max.to_Auto_Normal()
+ def test_manual_transitions(self) -> None:
+ """Test transitions of state Manual."""
+ # set Auto as initial state
+ self.ventilation_min.to_Auto_Normal()
+ self.ventilation_max.to_Auto_Normal()
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_min_manual", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_manual", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_min_manual", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_manual", "ON")
- self.assertEqual("Manual", self.ventilation_min.state)
- self.assertEqual("Manual", self.ventilation_max.state)
+ self.assertEqual("Manual", self.ventilation_min.state)
+ self.assertEqual("Manual", self.ventilation_max.state)
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_min_manual", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_manual", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_min_manual", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_manual", "OFF")
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
class TestVentilationHeliosTwoStage(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing VentilationHeliosTwoStage."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_output_on_state", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
-
- parameter_max = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter(
- state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
- state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
- state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
- state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
- state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18)),
- state_after_run=habapp_rules.actors.config.ventilation.StateConfig(level=99, display_text="AfterRun Custom"),
- after_run_timeout=350
- )
-
- config_max = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Unittest_Ventilation_max_output_on",
- ventilation_output_power="Unittest_Ventilation_max_output_power",
- manual="Unittest_Ventilation_max_manual",
- hand_request="Unittest_Ventilation_max_hand_request",
- external_request="Unittest_Ventilation_max_external_request",
- feedback_on="Unittest_Ventilation_max_feedback_on",
- feedback_power="Unittest_Ventilation_max_feedback_power",
- display_text="Unittest_Ventilation_max_display_text",
- presence_state="Unittest_Presence_state",
- state="Unittest_Ventilation_max_Custom_State"
- ),
- parameter=parameter_max
- )
-
- config_min = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Unittest_Ventilation_min_output_on",
- ventilation_output_power="Unittest_Ventilation_min_output_power",
- manual="Unittest_Ventilation_min_manual",
- state="H_Unittest_Ventilation_min_output_on_state"
- )
- )
-
- self.ventilation_min = habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config_min)
- self.ventilation_max = habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config_max)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "VentilationHeliosTwoStage_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=self.ventilation_min.state,
- show_conditions=False)
-
- graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state not in ["Auto_Init"]]:
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=state_name,
- show_conditions=True)
- graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
-
- def test_set_level(self):
- """test _set_level."""
- TestCase = collections.namedtuple("TestCase", "state, expected_on, expected_power")
-
- test_cases = [
- TestCase("Manual", None, None),
- TestCase("Auto_PowerHand", "ON", "ON"),
- TestCase("Auto_Normal", "ON", "OFF"),
- TestCase("Auto_PowerExternal", "ON", "ON"),
- TestCase("Auto_LongAbsence_On", "ON", "ON"),
- TestCase("Auto_LongAbsence_Off", "OFF", "OFF"),
- TestCase("Auto_Init", None, None),
- TestCase("Auto_PowerAfterRun", "ON", "OFF"),
- ]
-
- self.ventilation_max._config.parameter.state_normal.level = 1
-
- with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- send_mock.reset_mock()
- self.ventilation_max.state = test_case.state
-
- self.ventilation_max._set_level()
-
- if test_case.expected_on is not None:
- send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_on, test_case.expected_on)
-
- if test_case.expected_power is not None:
- send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_power, test_case.expected_power)
-
- def test_set_feedback_states(self):
- """test _set_feedback_states."""
- TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
-
- test_cases = [
- TestCase(None, "Auto_PowerAfterRun", False, False, "AfterRun Custom"),
- TestCase(0, "Auto_PowerAfterRun", False, False, "AfterRun Custom"),
- TestCase(1, "Auto_PowerAfterRun", True, False, "AfterRun Custom")
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.ventilation_min._ventilation_level = test_case.ventilation_level
- self.ventilation_max._ventilation_level = test_case.ventilation_level
- self.ventilation_min.state = test_case.state
- self.ventilation_max.state = test_case.state
-
- self.ventilation_min._set_feedback_states()
- self.ventilation_max._set_feedback_states()
-
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
-
- def test_power_after_run_transitions(self):
- """Test transitions of PowerAfterRun."""
- # PowerAfterRun to PowerHand
- self.ventilation_max.to_Auto_PowerAfterRun()
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
-
- # back to PowerAfterRun
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
- self.assertEqual("Auto_PowerAfterRun", self.ventilation_max.state)
-
- # PowerAfterRun to PowerExternal
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
-
- # back to PowerAfterRun
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
- self.assertEqual("Auto_PowerAfterRun", self.ventilation_max.state)
-
- # timeout of PowerAfterRun
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ """Tests cases for testing VentilationHeliosTwoStage."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_output_on_state", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
+
+ parameter_max = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter(
+ state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
+ state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
+ state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
+ state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
+ state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18)),
+ state_after_run=habapp_rules.actors.config.ventilation.StateConfig(level=99, display_text="AfterRun Custom"),
+ after_run_timeout=350,
+ )
+
+ config_max = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Unittest_Ventilation_max_output_on",
+ ventilation_output_power="Unittest_Ventilation_max_output_power",
+ manual="Unittest_Ventilation_max_manual",
+ hand_request="Unittest_Ventilation_max_hand_request",
+ external_request="Unittest_Ventilation_max_external_request",
+ feedback_on="Unittest_Ventilation_max_feedback_on",
+ feedback_power="Unittest_Ventilation_max_feedback_power",
+ display_text="Unittest_Ventilation_max_display_text",
+ presence_state="Unittest_Presence_state",
+ state="Unittest_Ventilation_max_Custom_State",
+ ),
+ parameter=parameter_max,
+ )
+
+ config_min = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Unittest_Ventilation_min_output_on", ventilation_output_power="Unittest_Ventilation_min_output_power", manual="Unittest_Ventilation_min_manual", state="H_Unittest_Ventilation_min_output_on_state"
+ )
+ )
+
+ self.ventilation_min = habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config_min)
+ self.ventilation_max = habapp_rules.actors.ventilation.VentilationHeliosTwoStage(config_max)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "VentilationHeliosTwoStage"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=self.ventilation_min.state, show_conditions=False
+ )
+
+ graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state != "Auto_Init"]:
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=state_name, show_conditions=True)
+ graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
+
+ def test_set_level(self) -> None:
+ """Test _set_level."""
+ TestCase = collections.namedtuple("TestCase", "state, expected_on, expected_power")
+
+ test_cases = [
+ TestCase("Manual", None, None),
+ TestCase("Auto_PowerHand", "ON", "ON"),
+ TestCase("Auto_Normal", "ON", "OFF"),
+ TestCase("Auto_PowerExternal", "ON", "ON"),
+ TestCase("Auto_LongAbsence_On", "ON", "ON"),
+ TestCase("Auto_LongAbsence_Off", "OFF", "OFF"),
+ TestCase("Auto_Init", None, None),
+ TestCase("Auto_PowerAfterRun", "ON", "OFF"),
+ ]
+
+ self.ventilation_max._config.parameter.state_normal.level = 1
+
+ with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ send_mock.reset_mock()
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_max._set_level()
+
+ if test_case.expected_on is not None:
+ send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_on, test_case.expected_on)
+
+ if test_case.expected_power is not None:
+ send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_power, test_case.expected_power)
+
+ def test_set_feedback_states(self) -> None:
+ """Test _set_feedback_states."""
+ TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
+
+ test_cases = [TestCase(None, "Auto_PowerAfterRun", False, False, "AfterRun Custom"), TestCase(0, "Auto_PowerAfterRun", False, False, "AfterRun Custom"), TestCase(1, "Auto_PowerAfterRun", True, False, "AfterRun Custom")]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.ventilation_min._ventilation_level = test_case.ventilation_level
+ self.ventilation_max._ventilation_level = test_case.ventilation_level
+ self.ventilation_min.state = test_case.state
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_min._set_feedback_states()
+ self.ventilation_max._set_feedback_states()
+
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
+
+ def test_power_after_run_transitions(self) -> None:
+ """Test transitions of PowerAfterRun."""
+ # PowerAfterRun to PowerHand
+ self.ventilation_max.to_Auto_PowerAfterRun()
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+
+ # back to PowerAfterRun
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+ self.assertEqual("Auto_PowerAfterRun", self.ventilation_max.state)
+
+ # PowerAfterRun to PowerExternal
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+
+ # back to PowerAfterRun
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "OFF")
+ self.assertEqual("Auto_PowerAfterRun", self.ventilation_max.state)
+
+ # timeout of PowerAfterRun
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
class TestVentilationHeliosTwoStageHumidity(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing VentilationHeliosTwoStageHumidity."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_min_current", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_output_on_state", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_max_current", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
-
- parameter_max = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter(
- state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
- state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
- state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
- state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
- state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18)),
- after_run_timeout=350,
- current_threshold_power=0.5
- )
-
- config_max = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Unittest_Ventilation_max_output_on",
- ventilation_output_power="Unittest_Ventilation_max_output_power",
- current="Unittest_Ventilation_max_current",
- manual="Unittest_Ventilation_max_manual",
- hand_request="Unittest_Ventilation_max_hand_request",
- external_request="Unittest_Ventilation_max_external_request",
- feedback_on="Unittest_Ventilation_max_feedback_on",
- feedback_power="Unittest_Ventilation_max_feedback_power",
- display_text="Unittest_Ventilation_max_display_text",
- presence_state="Unittest_Presence_state",
- state="Unittest_Ventilation_max_Custom_State"
- ),
- parameter=parameter_max
- )
-
- config_min = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Unittest_Ventilation_min_output_on",
- ventilation_output_power="Unittest_Ventilation_min_output_power",
- current="Unittest_Ventilation_min_current",
- manual="Unittest_Ventilation_min_manual",
- state="H_Unittest_Ventilation_min_output_on_state"
- )
- )
-
- self.ventilation_min = habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config_min)
- self.ventilation_max = habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config_max)
-
- def test_init_without_current_item(self):
- """Test __init__ without current item."""
- config = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
- items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
- ventilation_output_on="Unittest_Ventilation_min_output_on",
- ventilation_output_power="Unittest_Ventilation_min_output_power",
- manual="Unittest_Ventilation_min_manual",
- state="H_Unittest_Ventilation_min_output_on_state"
- )
- )
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config)
-
- def test_set_level(self):
- """test _set_level."""
- TestCase = collections.namedtuple("TestCase", "state, expected_on, expected_power")
-
- test_cases = [
- TestCase("Manual", None, None),
- TestCase("Auto_PowerHand", "ON", "ON"),
- TestCase("Auto_Normal", "ON", "OFF"),
- TestCase("Auto_PowerExternal", "ON", "ON"),
- TestCase("Auto_LongAbsence_On", "ON", "ON"),
- TestCase("Auto_LongAbsence_Off", "OFF", "OFF"),
- TestCase("Auto_Init", None, None),
- TestCase("Auto_PowerAfterRun", "ON", "OFF"),
- TestCase("Auto_PowerHumidity", "ON", "OFF"),
- ]
-
- self.ventilation_max._config.parameter.state_normal.level = 1
-
- with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- send_mock.reset_mock()
- self.ventilation_max.state = test_case.state
-
- self.ventilation_max._set_level()
-
- if test_case.expected_on is not None:
- send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_on, test_case.expected_on)
-
- if test_case.expected_power is not None:
- send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_power, test_case.expected_power)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "VentilationHeliosTwoStageHumidity_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=self.ventilation_min.state,
- show_conditions=False)
-
- graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
-
- for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state not in ["Auto_Init"]]:
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.ventilation_min.states,
- transitions=self.ventilation_min.trans,
- initial=state_name,
- show_conditions=True)
- graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
-
- def test_get_initial_state(self):
- """Test _get_initial_state."""
- TestCase = collections.namedtuple("TestCase", "presence_state, current, manual, hand_request, external_request, expected_state_min, expected_state_max")
-
- test_cases = [
- # present | current = None
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, False, False, "Auto_Normal", "Auto_Normal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, True, True, "Manual", "Manual"),
-
- # present | current smaller than the threshold
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, False, False, "Auto_Normal", "Auto_Normal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, True, True, "Manual", "Manual"),
-
- # present | current greater than the threshold
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, False, False, "Auto_Normal", "Auto_PowerHumidity"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, True, True, "Manual", "Manual"),
-
- # long absence
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, False, False, "Auto_Normal", "Auto_LongAbsence"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, False, True, "Auto_Normal", "Auto_LongAbsence"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, True, False, "Auto_Normal", "Auto_PowerHand"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, True, True, "Auto_Normal", "Auto_PowerHand"),
-
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, False, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, False, True, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, True, False, "Manual", "Manual"),
- TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, True, True, "Manual", "Manual"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Ventilation_min_manual", "ON" if test_case.manual else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_manual", "ON" if test_case.manual else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_current", test_case.current)
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_hand_request", "ON" if test_case.hand_request else "OFF")
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON" if test_case.external_request else "OFF")
- tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_state)
-
- self.assertEqual(test_case.expected_state_min, self.ventilation_min._get_initial_state())
- self.assertEqual(test_case.expected_state_max, self.ventilation_max._get_initial_state())
-
- def test_set_feedback_states(self):
- """test _set_feedback_states."""
- TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
-
- test_cases = [
- TestCase(None, "Auto_PowerHumidity", False, False, "Humidity Custom"),
- TestCase(0, "Auto_PowerHumidity", False, False, "Humidity Custom"),
- TestCase(1, "Auto_PowerHumidity", True, False, "Humidity Custom"),
- TestCase(2, "Auto_PowerHumidity", True, True, "Humidity Custom")
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.ventilation_min._ventilation_level = test_case.ventilation_level
- self.ventilation_max._ventilation_level = test_case.ventilation_level
- self.ventilation_min.state = test_case.state
- self.ventilation_max.state = test_case.state
-
- self.ventilation_min._set_feedback_states()
- self.ventilation_max._set_feedback_states()
-
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
-
- def test_current_greater_threshold(self):
- """Test __current_greater_threshold."""
-
- TestCase = collections.namedtuple("TestCase", "threshold, item_value, given_value, expected_result")
-
- test_cases = [
- TestCase(42, 0, 0, False),
- TestCase(42, 0, 100, True),
- TestCase(42, 100, 0, False),
- TestCase(42, 100, 100, True),
-
- TestCase(42, 0, None, False),
- TestCase(42, 100, None, True),
- TestCase(42, None, None, False)
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self.ventilation_max._current_threshold_power = test_case.threshold
- tests.helper.oh_item.set_state("Unittest_Ventilation_max_current", test_case.item_value)
-
- result = self.ventilation_max._current_greater_threshold() if test_case.given_value is None else self.ventilation_max._current_greater_threshold(test_case.given_value)
-
- self.assertEqual(test_case.expected_result, result)
-
- def test_power_after_run_transitions(self):
- """Test transitions of PowerAfterRun."""
- # _end_after_run triggered
- self.ventilation_max.to_Auto_PowerAfterRun()
- self.ventilation_max._end_after_run()
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
-
- def test_power_humidity_transitions(self):
- """Test transitions of state Auto_PowerHumidity."""
- # set default config parameters
- self.ventilation_min._config.parameter = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter()
- self.ventilation_max._config.parameter = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter()
-
- # set AutoNormal as initial state
- self.ventilation_min.to_Auto_Normal()
- self.ventilation_max.to_Auto_Normal()
-
- # set correct output states
- self.ventilation_min._config.items.ventilation_output_power.set_value("OFF")
- self.ventilation_max._config.items.ventilation_output_power.set_value("OFF")
-
- # state != Auto_PowerHumidity | current below the threshold
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
-
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
-
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
-
- # state != Auto_PowerHumidity | current grater then the threshold
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
-
- self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
- self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
-
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
+ """Tests cases for testing VentilationHeliosTwoStageHumidity."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_output_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_min_current", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_min_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Ventilation_min_output_on_state", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_output_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Ventilation_max_current", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_Custom_State", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_hand_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_external_request", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_on", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Ventilation_max_feedback_power", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Ventilation_max_display_text", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Presence_state", None)
+
+ parameter_max = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter(
+ state_normal=habapp_rules.actors.config.ventilation.StateConfig(level=101, display_text="Normal Custom"),
+ state_hand=habapp_rules.actors.config.ventilation.StateConfigWithTimeout(level=102, display_text="Hand Custom", timeout=42 * 60),
+ state_external=habapp_rules.actors.config.ventilation.StateConfig(level=103, display_text="External Custom"),
+ state_humidity=habapp_rules.actors.config.ventilation.StateConfig(level=104, display_text="Humidity Custom"),
+ state_long_absence=habapp_rules.actors.config.ventilation.StateConfigLongAbsence(level=105, display_text="Absence Custom", duration=1800, start_time=datetime.time(18)),
+ after_run_timeout=350,
+ current_threshold_power=0.5,
+ )
+
+ config_max = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Unittest_Ventilation_max_output_on",
+ ventilation_output_power="Unittest_Ventilation_max_output_power",
+ current="Unittest_Ventilation_max_current",
+ manual="Unittest_Ventilation_max_manual",
+ hand_request="Unittest_Ventilation_max_hand_request",
+ external_request="Unittest_Ventilation_max_external_request",
+ feedback_on="Unittest_Ventilation_max_feedback_on",
+ feedback_power="Unittest_Ventilation_max_feedback_power",
+ display_text="Unittest_Ventilation_max_display_text",
+ presence_state="Unittest_Presence_state",
+ state="Unittest_Ventilation_max_Custom_State",
+ ),
+ parameter=parameter_max,
+ )
+
+ config_min = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Unittest_Ventilation_min_output_on",
+ ventilation_output_power="Unittest_Ventilation_min_output_power",
+ current="Unittest_Ventilation_min_current",
+ manual="Unittest_Ventilation_min_manual",
+ state="H_Unittest_Ventilation_min_output_on_state",
+ )
+ )
+
+ self.ventilation_min = habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config_min)
+ self.ventilation_max = habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config_max)
+
+ def test_init_without_current_item(self) -> None:
+ """Test __init__ without current item."""
+ config = habapp_rules.actors.config.ventilation.VentilationTwoStageConfig(
+ items=habapp_rules.actors.config.ventilation.VentilationTwoStageItems(
+ ventilation_output_on="Unittest_Ventilation_min_output_on", ventilation_output_power="Unittest_Ventilation_min_output_power", manual="Unittest_Ventilation_min_manual", state="H_Unittest_Ventilation_min_output_on_state"
+ )
+ )
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.actors.ventilation.VentilationHeliosTwoStageHumidity(config)
+
+ def test_set_level(self) -> None:
+ """Test _set_level."""
+ TestCase = collections.namedtuple("TestCase", "state, expected_on, expected_power")
+
+ test_cases = [
+ TestCase("Manual", None, None),
+ TestCase("Auto_PowerHand", "ON", "ON"),
+ TestCase("Auto_Normal", "ON", "OFF"),
+ TestCase("Auto_PowerExternal", "ON", "ON"),
+ TestCase("Auto_LongAbsence_On", "ON", "ON"),
+ TestCase("Auto_LongAbsence_Off", "OFF", "OFF"),
+ TestCase("Auto_Init", None, None),
+ TestCase("Auto_PowerAfterRun", "ON", "OFF"),
+ TestCase("Auto_PowerHumidity", "ON", "OFF"),
+ ]
+
+ self.ventilation_max._config.parameter.state_normal.level = 1
+
+ with unittest.mock.patch("habapp_rules.core.helper.send_if_different") as send_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ send_mock.reset_mock()
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_max._set_level()
+
+ if test_case.expected_on is not None:
+ send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_on, test_case.expected_on)
+
+ if test_case.expected_power is not None:
+ send_mock.assert_any_call(self.ventilation_max._config.items.ventilation_output_power, test_case.expected_power)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "VentilationHeliosTwoStageHumidity"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=self.ventilation_min.state, show_conditions=False
+ )
+
+ graph.get_graph().draw(picture_dir / "Ventilation.png", format="png", prog="dot")
+
+ for state_name in [state for state in self._get_state_names(self.ventilation_min.states) if state != "Auto_Init"]:
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.ventilation_min.states, transitions=self.ventilation_min.trans, initial=state_name, show_conditions=True)
+ graph.get_graph(force_new=True, show_roi=True).draw(picture_dir / f"Ventilation_{state_name}.png", format="png", prog="dot")
+
+ def test_get_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ TestCase = collections.namedtuple("TestCase", "presence_state, current, manual, hand_request, external_request, expected_state_min, expected_state_max")
+
+ test_cases = [
+ # present | current = None
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, False, False, "Auto_Normal", "Auto_Normal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, None, True, True, True, "Manual", "Manual"),
+ # present | current smaller than the threshold
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, False, False, "Auto_Normal", "Auto_Normal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 0.01, True, True, True, "Manual", "Manual"),
+ # present | current greater than the threshold
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, False, False, "Auto_Normal", "Auto_PowerHumidity"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, False, True, "Auto_Normal", "Auto_PowerExternal"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.PRESENCE.value, 1, True, True, True, "Manual", "Manual"),
+ # long absence
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, False, False, "Auto_Normal", "Auto_LongAbsence"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, False, True, "Auto_Normal", "Auto_LongAbsence"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, True, False, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, False, True, True, "Auto_Normal", "Auto_PowerHand"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, False, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, False, True, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, True, False, "Manual", "Manual"),
+ TestCase(habapp_rules.system.PresenceState.LONG_ABSENCE.value, 20, True, True, True, "Manual", "Manual"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Ventilation_min_manual", "ON" if test_case.manual else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_manual", "ON" if test_case.manual else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_current", test_case.current)
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_hand_request", "ON" if test_case.hand_request else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_external_request", "ON" if test_case.external_request else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Presence_state", test_case.presence_state)
+
+ self.assertEqual(test_case.expected_state_min, self.ventilation_min._get_initial_state())
+ self.assertEqual(test_case.expected_state_max, self.ventilation_max._get_initial_state())
+
+ def test_set_feedback_states(self) -> None:
+ """Test _set_feedback_states."""
+ TestCase = collections.namedtuple("TestCase", "ventilation_level, state, expected_on, expected_power, expected_display_text")
+
+ test_cases = [
+ TestCase(None, "Auto_PowerHumidity", False, False, "Humidity Custom"),
+ TestCase(0, "Auto_PowerHumidity", False, False, "Humidity Custom"),
+ TestCase(1, "Auto_PowerHumidity", True, False, "Humidity Custom"),
+ TestCase(2, "Auto_PowerHumidity", True, True, "Humidity Custom"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.ventilation_min._ventilation_level = test_case.ventilation_level
+ self.ventilation_max._ventilation_level = test_case.ventilation_level
+ self.ventilation_min.state = test_case.state
+ self.ventilation_max.state = test_case.state
+
+ self.ventilation_min._set_feedback_states()
+ self.ventilation_max._set_feedback_states()
+
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_on", "ON" if test_case.expected_on else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_feedback_power", "ON" if test_case.expected_power else "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_display_text", test_case.expected_display_text)
+
+ def test_current_greater_threshold(self) -> None:
+ """Test __current_greater_threshold."""
+ TestCase = collections.namedtuple("TestCase", "threshold, item_value, given_value, expected_result")
+
+ test_cases = [TestCase(42, 0, 0, False), TestCase(42, 0, 100, True), TestCase(42, 100, 0, False), TestCase(42, 100, 100, True), TestCase(42, 0, None, False), TestCase(42, 100, None, True), TestCase(42, None, None, False)]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self.ventilation_max._current_threshold_power = test_case.threshold
+ tests.helper.oh_item.set_state("Unittest_Ventilation_max_current", test_case.item_value)
+
+ result = self.ventilation_max._current_greater_threshold() if test_case.given_value is None else self.ventilation_max._current_greater_threshold(test_case.given_value)
+
+ self.assertEqual(test_case.expected_result, result)
+
+ def test_power_after_run_transitions(self) -> None:
+ """Test transitions of PowerAfterRun."""
+ # _end_after_run triggered
+ self.ventilation_max.to_Auto_PowerAfterRun()
+ self.ventilation_max._end_after_run()
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+
+ def test_power_humidity_transitions(self) -> None:
+ """Test transitions of state Auto_PowerHumidity."""
+ # set default config parameters
+ self.ventilation_min._config.parameter = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter()
+ self.ventilation_max._config.parameter = habapp_rules.actors.config.ventilation.VentilationTwoStageParameter()
+
+ # set AutoNormal as initial state
+ self.ventilation_min.to_Auto_Normal()
+ self.ventilation_max.to_Auto_Normal()
+
+ # set correct output states
+ self.ventilation_min._config.items.ventilation_output_power.set_value("OFF")
+ self.ventilation_max._config.items.ventilation_output_power.set_value("OFF")
+
+ # state != Auto_PowerHumidity | current below the threshold
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
+
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
+
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
+
+ # state != Auto_PowerHumidity | current grater then the threshold
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
+
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
+
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
- # state == Auto_PowerHumidity | current grater then the threshold
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
+ # state == Auto_PowerHumidity | current grater then the threshold
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
- self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
- self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
- # state == Auto_PowerHumidity | current below then the threshold
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
+ # state == Auto_PowerHumidity | current below then the threshold
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
- # state == Auto_PowerAfterRun | current below the threshold
- self.ventilation_min.to_Auto_PowerAfterRun()
- self.ventilation_max.to_Auto_PowerAfterRun()
+ # state == Auto_PowerAfterRun | current below the threshold
+ self.ventilation_min.to_Auto_PowerAfterRun()
+ self.ventilation_max.to_Auto_PowerAfterRun()
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.1)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.1)
- self.ventilation_min._after_run_timeout()
- self.ventilation_max._after_run_timeout()
+ self.ventilation_min._after_run_timeout()
+ self.ventilation_max._after_run_timeout()
- self.assertEqual("Auto_Normal", self.ventilation_min.state)
- self.assertEqual("Auto_Normal", self.ventilation_max.state)
+ self.assertEqual("Auto_Normal", self.ventilation_min.state)
+ self.assertEqual("Auto_Normal", self.ventilation_max.state)
- # state == Auto_PowerAfterRun | current grater then the threshold
- self.ventilation_min.to_Auto_PowerAfterRun()
- self.ventilation_max.to_Auto_PowerAfterRun()
+ # state == Auto_PowerAfterRun | current grater then the threshold
+ self.ventilation_min.to_Auto_PowerAfterRun()
+ self.ventilation_max.to_Auto_PowerAfterRun()
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
- tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_min_current", 0.2)
+ tests.helper.oh_item.item_state_event("Unittest_Ventilation_max_current", 0.6)
- self.ventilation_min._after_run_timeout()
- self.ventilation_max._after_run_timeout()
+ self.ventilation_min._after_run_timeout()
+ self.ventilation_max._after_run_timeout()
- self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
- self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_min.state)
+ self.assertEqual("Auto_PowerHumidity", self.ventilation_max.state)
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_min_output_power", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
- # state == Auto_PowerHumidity | _hand_on triggered
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
- self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
+ # state == Auto_PowerHumidity | _hand_on triggered
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "ON")
+ self.assertEqual("Auto_PowerHand", self.ventilation_max.state)
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_hand_request", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
- tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_on", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Ventilation_max_output_power", "OFF")
- # state == Auto_PowerHumidity | _external_on triggered
- self.ventilation_max.to_Auto_PowerHumidity()
- tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
- self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
+ # state == Auto_PowerHumidity | _external_on triggered
+ self.ventilation_max.to_Auto_PowerHumidity()
+ tests.helper.oh_item.item_state_change_event("Unittest_Ventilation_max_external_request", "ON")
+ self.assertEqual("Auto_PowerExternal", self.ventilation_max.state)
diff --git a/tests/bridge/config/knx_mqtt.py b/tests/bridge/config/knx_mqtt.py
index 9b68854..a2aa946 100644
--- a/tests/bridge/config/knx_mqtt.py
+++ b/tests/bridge/config/knx_mqtt.py
@@ -1,4 +1,5 @@
"""Test config models for KNX / MQTT bridge rules."""
+
import HABApp
import habapp_rules.bridge.config.knx_mqtt
@@ -7,47 +8,36 @@
class TestKnxMqttConfig(tests.helper.test_case_base.TestCaseBase):
- """Test KnxMqttConfig"""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_MQTT_dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_KNX_dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_KNX_switch", None)
-
- def test_validate_knx_items(self):
- """Test validate_knx_items."""
- # Both KNX items are given
- habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_MQTT_dimmer",
- knx_switch_ctr="Unittest_KNX_switch",
- knx_dimmer_ctr="Unittest_KNX_dimmer"
- )
- )
-
- # only KNX switch is given
- habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_MQTT_dimmer",
- knx_switch_ctr="Unittest_KNX_switch",
- )
- )
-
- # only KNX dimmer is given
- habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_MQTT_dimmer",
- knx_dimmer_ctr="Unittest_KNX_dimmer"
- )
- )
-
- # no KNX item is given
- with self.assertRaises(ValueError):
- habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_MQTT_dimmer",
- )
- )
+ """Test KnxMqttConfig."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_MQTT_dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_KNX_dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_KNX_switch", None)
+
+ def test_validate_knx_items(self) -> None:
+ """Test validate_knx_items."""
+ # Both KNX items are given
+ habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_MQTT_dimmer", knx_switch_ctr="Unittest_KNX_switch", knx_dimmer_ctr="Unittest_KNX_dimmer"))
+
+ # only KNX switch is given
+ habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
+ items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
+ mqtt_dimmer="Unittest_MQTT_dimmer",
+ knx_switch_ctr="Unittest_KNX_switch",
+ )
+ )
+
+ # only KNX dimmer is given
+ habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_MQTT_dimmer", knx_dimmer_ctr="Unittest_KNX_dimmer"))
+
+ # no KNX item is given
+ with self.assertRaises(ValueError):
+ habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
+ items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
+ mqtt_dimmer="Unittest_MQTT_dimmer",
+ )
+ )
diff --git a/tests/bridge/knx_mqtt.py b/tests/bridge/knx_mqtt.py
index c0ee276..5865f41 100644
--- a/tests/bridge/knx_mqtt.py
+++ b/tests/bridge/knx_mqtt.py
@@ -1,4 +1,5 @@
"""Test KNX MQTT bridges."""
+
import collections
import unittest
import unittest.mock
@@ -12,172 +13,139 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestLight(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing Light rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_full_KNX_Dimmer_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_full_KNX_Switch_ctr", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_full_MQTT_dimmer", 0)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_switch_KNX_Switch_ctr", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_switch_MQTT_dimmer", 0)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_dimmer_KNX_Dimmer_ctr", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_dimmer_MQTT_dimmer", 0)
-
- config_full = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_full_MQTT_dimmer",
- knx_switch_ctr="Unittest_full_KNX_Switch_ctr",
- knx_dimmer_ctr="Unittest_full_KNX_Dimmer_ctr"
- )
- )
-
- config_switch = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_switch_MQTT_dimmer",
- knx_switch_ctr="Unittest_switch_KNX_Switch_ctr"
- )
- )
-
- config_dimmer = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_dimmer_MQTT_dimmer",
- knx_dimmer_ctr="Unittest_dimmer_KNX_Dimmer_ctr"
- )
- )
-
- self._knx_bridge_full = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_full)
- self._knx_bridge_switch = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_switch)
- self._knx_bridge_dimmer = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_dimmer)
-
- def test__init__(self):
- """Test __init__"""
- self.assertIsNotNone(self._knx_bridge_full._config.items.knx_switch_ctr)
- self.assertIsNotNone(self._knx_bridge_full._config.items.knx_dimmer_ctr)
-
- self.assertIsNotNone(self._knx_bridge_switch._config.items.knx_switch_ctr)
- self.assertIsNone(self._knx_bridge_switch._config.items.knx_dimmer_ctr)
-
- self.assertIsNone(self._knx_bridge_dimmer._config.items.knx_switch_ctr)
- self.assertIsNotNone(self._knx_bridge_dimmer._config.items.knx_dimmer_ctr)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_full_MQTT_dimmer", None)
- tests.helper.oh_item.set_state("Unittest_full_KNX_Switch_ctr", None)
- tests.helper.oh_item.set_state("Unittest_full_KNX_Dimmer_ctr", None)
- tests.helper.oh_item.set_state("Unittest_switch_MQTT_dimmer", None)
- tests.helper.oh_item.set_state("Unittest_switch_KNX_Switch_ctr", None)
- tests.helper.oh_item.set_state("Unittest_dimmer_MQTT_dimmer", None)
- tests.helper.oh_item.set_state("Unittest_dimmer_KNX_Dimmer_ctr", None)
-
- config_full = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_full_MQTT_dimmer",
- knx_switch_ctr="Unittest_full_KNX_Switch_ctr",
- knx_dimmer_ctr="Unittest_full_KNX_Dimmer_ctr"
- )
- )
-
- config_switch = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_switch_MQTT_dimmer",
- knx_switch_ctr="Unittest_switch_KNX_Switch_ctr"
- )
- )
-
- config_dimmer = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
- items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(
- mqtt_dimmer="Unittest_dimmer_MQTT_dimmer",
- knx_dimmer_ctr="Unittest_dimmer_KNX_Dimmer_ctr"
- )
- )
-
- habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_full)
- habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_switch)
- habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_dimmer)
-
-
- def test_knx_on_off(self):
- """Test ON/OFF from KNX"""
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- # ON via KNX
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Switch_ctr", "ON")
- self.assertEqual(100, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- # OFF via KNX
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Switch_ctr", "OFF")
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- # 50 via KNX
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", 50)
- self.assertEqual(50, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- # 0 via KNX
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", 0)
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- def test_knx_increase(self):
- """Test increase from KNX."""
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "INCREASE")
- self.assertEqual(60, self._knx_bridge_full._config.items.mqtt_dimmer.value)
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "INCREASE")
- self.assertEqual(100, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- def test_knx_decrease(self):
- """Test decrease from KNX."""
- self._knx_bridge_full._config.items.mqtt_dimmer.oh_send_command(100)
- self.assertEqual(100,self._knx_bridge_full._config.items.mqtt_dimmer.value)
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "DECREASE")
- self.assertEqual(30, self._knx_bridge_full._config.items.mqtt_dimmer.value)
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "DECREASE")
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
-
- def test_knx_not_supported(self):
- """Test not supported command coming from KNX."""
- with unittest.mock.patch.object(self._knx_bridge_full, "_instance_logger") as logger_mock:
- tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "NotSupported")
- logger_mock.error.assert_called_once_with("command 'NotSupported' ist not supported!")
-
- def test_mqtt_events(self):
- """Test if KNX item is updated correctly if MQTT item changed."""
- self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
- TestCase = collections.namedtuple("TestCase", "send_value, expected_call_dimmer, expected_call_switch")
-
- test_cases = [
- TestCase(70, 70, "ON"),
- TestCase(100, 100, "ON"),
- TestCase(1, 1, "ON"),
- TestCase(0, 0, "OFF")
- ]
-
- with unittest.mock.patch.object(self._knx_bridge_full._config.items, "knx_dimmer_ctr") as full_knx_dimmer_item_mock, \
- unittest.mock.patch.object(self._knx_bridge_full._config.items, "knx_switch_ctr") as full_knx_switch_item_mock, \
- unittest.mock.patch.object(self._knx_bridge_switch._config.items, "knx_switch_ctr") as switch_knx_switch_item_mock, \
- unittest.mock.patch.object(self._knx_bridge_dimmer._config.items, "knx_dimmer_ctr") as dimmer_knx_dimmer_item_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- full_knx_dimmer_item_mock.oh_post_update.reset_mock()
- full_knx_switch_item_mock.oh_post_update.reset_mock()
- switch_knx_switch_item_mock.oh_post_update.reset_mock()
- dimmer_knx_dimmer_item_mock.oh_post_update.reset_mock()
-
- tests.helper.oh_item.item_state_change_event("Unittest_full_MQTT_dimmer", test_case.send_value)
- tests.helper.oh_item.item_state_change_event("Unittest_switch_MQTT_dimmer", test_case.send_value)
- tests.helper.oh_item.item_state_change_event("Unittest_dimmer_MQTT_dimmer", test_case.send_value)
-
- # full bridge (switch and dimmer item for KNX)
- full_knx_dimmer_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_dimmer)
- full_knx_switch_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_switch)
-
- # partial bridges
- switch_knx_switch_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_switch)
- dimmer_knx_dimmer_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_dimmer)
+ """Tests cases for testing Light rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_full_KNX_Dimmer_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_full_KNX_Switch_ctr", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_full_MQTT_dimmer", 0)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_switch_KNX_Switch_ctr", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_switch_MQTT_dimmer", 0)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_dimmer_KNX_Dimmer_ctr", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_dimmer_MQTT_dimmer", 0)
+
+ config_full = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
+ items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_full_MQTT_dimmer", knx_switch_ctr="Unittest_full_KNX_Switch_ctr", knx_dimmer_ctr="Unittest_full_KNX_Dimmer_ctr")
+ )
+
+ config_switch = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_switch_MQTT_dimmer", knx_switch_ctr="Unittest_switch_KNX_Switch_ctr"))
+
+ config_dimmer = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_dimmer_MQTT_dimmer", knx_dimmer_ctr="Unittest_dimmer_KNX_Dimmer_ctr"))
+
+ self._knx_bridge_full = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_full)
+ self._knx_bridge_switch = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_switch)
+ self._knx_bridge_dimmer = habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_dimmer)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ self.assertIsNotNone(self._knx_bridge_full._config.items.knx_switch_ctr)
+ self.assertIsNotNone(self._knx_bridge_full._config.items.knx_dimmer_ctr)
+
+ self.assertIsNotNone(self._knx_bridge_switch._config.items.knx_switch_ctr)
+ self.assertIsNone(self._knx_bridge_switch._config.items.knx_dimmer_ctr)
+
+ self.assertIsNone(self._knx_bridge_dimmer._config.items.knx_switch_ctr)
+ self.assertIsNotNone(self._knx_bridge_dimmer._config.items.knx_dimmer_ctr)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_full_MQTT_dimmer", None)
+ tests.helper.oh_item.set_state("Unittest_full_KNX_Switch_ctr", None)
+ tests.helper.oh_item.set_state("Unittest_full_KNX_Dimmer_ctr", None)
+ tests.helper.oh_item.set_state("Unittest_switch_MQTT_dimmer", None)
+ tests.helper.oh_item.set_state("Unittest_switch_KNX_Switch_ctr", None)
+ tests.helper.oh_item.set_state("Unittest_dimmer_MQTT_dimmer", None)
+ tests.helper.oh_item.set_state("Unittest_dimmer_KNX_Dimmer_ctr", None)
+
+ config_full = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(
+ items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_full_MQTT_dimmer", knx_switch_ctr="Unittest_full_KNX_Switch_ctr", knx_dimmer_ctr="Unittest_full_KNX_Dimmer_ctr")
+ )
+
+ config_switch = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_switch_MQTT_dimmer", knx_switch_ctr="Unittest_switch_KNX_Switch_ctr"))
+
+ config_dimmer = habapp_rules.bridge.config.knx_mqtt.KnxMqttConfig(items=habapp_rules.bridge.config.knx_mqtt.KnxMqttItems(mqtt_dimmer="Unittest_dimmer_MQTT_dimmer", knx_dimmer_ctr="Unittest_dimmer_KNX_Dimmer_ctr"))
+
+ habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_full)
+ habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_switch)
+ habapp_rules.bridge.knx_mqtt.KnxMqttDimmerBridge(config_dimmer)
+
+ def test_knx_on_off(self) -> None:
+ """Test ON/OFF from KNX."""
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ # ON via KNX
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Switch_ctr", "ON")
+ self.assertEqual(100, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ # OFF via KNX
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Switch_ctr", "OFF")
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ # 50 via KNX
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", 50)
+ self.assertEqual(50, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ # 0 via KNX
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", 0)
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ def test_knx_increase(self) -> None:
+ """Test increase from KNX."""
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "INCREASE")
+ self.assertEqual(60, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "INCREASE")
+ self.assertEqual(100, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ def test_knx_decrease(self) -> None:
+ """Test decrease from KNX."""
+ self._knx_bridge_full._config.items.mqtt_dimmer.oh_send_command(100)
+ self.assertEqual(100, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "DECREASE")
+ self.assertEqual(30, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "DECREASE")
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+
+ def test_knx_not_supported(self) -> None:
+ """Test not supported command coming from KNX."""
+ with unittest.mock.patch.object(self._knx_bridge_full, "_instance_logger") as logger_mock:
+ tests.helper.oh_item.item_command_event("Unittest_full_KNX_Dimmer_ctr", "NotSupported")
+ logger_mock.error.assert_called_once_with("command 'NotSupported' ist not supported!")
+
+ def test_mqtt_events(self) -> None:
+ """Test if KNX item is updated correctly if MQTT item changed."""
+ self.assertEqual(0, self._knx_bridge_full._config.items.mqtt_dimmer.value)
+ TestCase = collections.namedtuple("TestCase", "send_value, expected_call_dimmer, expected_call_switch")
+
+ test_cases = [TestCase(70, 70, "ON"), TestCase(100, 100, "ON"), TestCase(1, 1, "ON"), TestCase(0, 0, "OFF")]
+
+ with (
+ unittest.mock.patch.object(self._knx_bridge_full._config.items, "knx_dimmer_ctr") as full_knx_dimmer_item_mock,
+ unittest.mock.patch.object(self._knx_bridge_full._config.items, "knx_switch_ctr") as full_knx_switch_item_mock,
+ unittest.mock.patch.object(self._knx_bridge_switch._config.items, "knx_switch_ctr") as switch_knx_switch_item_mock,
+ unittest.mock.patch.object(self._knx_bridge_dimmer._config.items, "knx_dimmer_ctr") as dimmer_knx_dimmer_item_mock,
+ ):
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ full_knx_dimmer_item_mock.oh_post_update.reset_mock()
+ full_knx_switch_item_mock.oh_post_update.reset_mock()
+ switch_knx_switch_item_mock.oh_post_update.reset_mock()
+ dimmer_knx_dimmer_item_mock.oh_post_update.reset_mock()
+
+ tests.helper.oh_item.item_state_change_event("Unittest_full_MQTT_dimmer", test_case.send_value)
+ tests.helper.oh_item.item_state_change_event("Unittest_switch_MQTT_dimmer", test_case.send_value)
+ tests.helper.oh_item.item_state_change_event("Unittest_dimmer_MQTT_dimmer", test_case.send_value)
+
+ # full bridge (switch and dimmer item for KNX)
+ full_knx_dimmer_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_dimmer)
+ full_knx_switch_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_switch)
+
+ # partial bridges
+ switch_knx_switch_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_switch)
+ dimmer_knx_dimmer_item_mock.oh_post_update.assert_called_once_with(test_case.expected_call_dimmer)
diff --git a/tests/common/config/filter.py b/tests/common/config/filter.py
index 9166097..221a816 100644
--- a/tests/common/config/filter.py
+++ b/tests/common/config/filter.py
@@ -1,4 +1,5 @@
"""Test config models for filter rules."""
+
import HABApp
import habapp_rules.common.config.filter
@@ -7,62 +8,38 @@
class TestExponentialFilterConfig(tests.helper.test_case_base.TestCaseBase):
- """Test ExponentialFilterConfig."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Raw", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered", 0)
-
- def test_init(self):
- """Test __init__."""
- # instant_increase and instant_decrease is not set
- habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=42,
- )
- )
-
- # instant_increase is set and instant_decrease is not set
- habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=42,
- instant_increase=True
- )
- )
-
- # instant_increase is not set and instant_decrease is set
- habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=42,
- instant_decrease=True
- )
- )
-
- # instant_increase and instant_decrease is set
- with self.assertRaises(ValueError):
- habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=42,
- instant_increase=True,
- instant_decrease=True
- )
- )
+ """Test ExponentialFilterConfig."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Raw", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered", 0)
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ # instant_increase and instant_decrease is not set
+ habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered"),
+ parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
+ tau=42,
+ ),
+ )
+
+ # instant_increase is set and instant_decrease is not set
+ habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered"), parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=42, instant_increase=True)
+ )
+
+ # instant_increase is not set and instant_decrease is set
+ habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered"), parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=42, instant_decrease=True)
+ )
+
+ # instant_increase and instant_decrease is set
+ with self.assertRaises(ValueError):
+ habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered"),
+ parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=42, instant_increase=True, instant_decrease=True),
+ )
diff --git a/tests/common/config/logic.py b/tests/common/config/logic.py
index c039cc0..26fb0a2 100644
--- a/tests/common/config/logic.py
+++ b/tests/common/config/logic.py
@@ -1,4 +1,5 @@
"""Test config models of logic rules."""
+
import HABApp
import habapp_rules.common.config.logic
@@ -7,35 +8,23 @@
class TestBinaryLogicItems(tests.helper.test_case_base.TestCaseBase):
- """Test BinaryLogicItems."""
-
- def tests_model_validator(self) -> None:
- """tests model_validator."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_Switch", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Input_Contact", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Switch", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Output_Contact", None)
-
- # input and output items are the same
- habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Input_Switch"],
- output="Unittest_Output_Switch"
- )
-
- habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Input_Contact"],
- output="Unittest_Output_Contact"
- )
-
- # input and output items are different
- with self.assertRaises(ValueError):
- habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Input_Switch"],
- output="Unittest_Output_Contact"
- )
-
- with self.assertRaises(ValueError):
- habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Input_Contact"],
- output="Unittest_Output_Switch"
- )
+ """Test BinaryLogicItems."""
+
+ def tests_model_validator(self) -> None:
+ """Tests model_validator."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Input_Contact", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Output_Contact", None)
+
+ # input and output items are the same
+ habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Input_Switch"], output="Unittest_Output_Switch")
+
+ habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Input_Contact"], output="Unittest_Output_Contact")
+
+ # input and output items are different
+ with self.assertRaises(TypeError):
+ habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Input_Switch"], output="Unittest_Output_Contact")
+
+ with self.assertRaises(TypeError):
+ habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Input_Contact"], output="Unittest_Output_Switch")
diff --git a/tests/common/filter.py b/tests/common/filter.py
index b3d25ab..53debc4 100644
--- a/tests/common/filter.py
+++ b/tests/common/filter.py
@@ -1,6 +1,6 @@
"""Unit-test for filter functions / rules."""
+
import collections
-import unittest.mock
import HABApp
@@ -10,121 +10,99 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestExponentialFilter(tests.helper.test_case_base.TestCaseBase):
- """Tests ExponentialFilter."""
-
- 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_Raw", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered_2", None)
-
- config = habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=10
- )
- )
-
- config_increase = habapp_rules.common.config.filter.ExponentialFilterConfig(
- items=habapp_rules.common.config.filter.ExponentialFilterItems(
- raw="Unittest_Raw",
- filtered="Unittest_Filtered_2"
- ),
- parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
- tau=100,
- instant_increase=True
- )
- )
-
- self._rule_run_mock = unittest.mock.MagicMock()
- with unittest.mock.patch("HABApp.rule.rule._HABAppSchedulerView", return_value=self._rule_run_mock):
- self.filter = habapp_rules.common.filter.ExponentialFilter(config)
- self.filter_increase = habapp_rules.common.filter.ExponentialFilter(config_increase)
-
- def test__init__(self):
- """Test __init__."""
- self._rule_run_mock.every.assert_has_calls([ # check if self.run.every was called
- unittest.mock.call(None, 2.0, self.filter._cb_cyclic_calculate_and_update_output),
- unittest.mock.call(None, 20.0, self.filter_increase._cb_cyclic_calculate_and_update_output)
- ])
-
- self.assertEqual("Unittest_Raw", self.filter._config.items.raw.name)
- self.assertEqual("Unittest_Raw", self.filter_increase._config.items.raw.name)
-
- self.assertEqual("Unittest_Filtered", self.filter._config.items.filtered.name)
- self.assertEqual("Unittest_Filtered_2", self.filter_increase._config.items.filtered.name)
-
- self.assertEqual(0.2, self.filter._alpha)
- self.assertEqual(0.2, self.filter_increase._alpha)
-
- self.assertFalse(self.filter._config.parameter.instant_increase)
- self.assertFalse(self.filter._config.parameter.instant_decrease)
-
- self.assertTrue(self.filter_increase._config.parameter.instant_increase)
- self.assertFalse(self.filter_increase._config.parameter.instant_decrease)
-
- def test_cb_cyclic_calculate_and_update_output(self):
- """Test _cb_cyclic_calculate_and_update_output."""
- TestCase = collections.namedtuple("TestCase", "new_value, previous_value, expected_result")
-
- test_cases = [
- TestCase(1, 1, 1),
- TestCase(2, 1, 1.2),
- TestCase(2, 0, 0.4),
- TestCase(0, 2, 1.6),
- TestCase(None, None, 999),
- TestCase(None, 42, 999),
- TestCase(42, None, 999),
- ]
-
- for test_case in test_cases:
- tests.helper.oh_item.set_state("Unittest_Raw", test_case.new_value)
- tests.helper.oh_item.set_state("Unittest_Filtered", 999) # set some random value
- self.filter._previous_value = test_case.previous_value
-
- self.filter._cb_cyclic_calculate_and_update_output()
-
- self.assertEqual(test_case.expected_result, round(self.filter._config.items.filtered.value, 2))
-
- if test_case.new_value is not None and test_case.previous_value is not None:
- self.assertEqual(test_case.expected_result, round(self.filter._previous_value, 2))
- else:
- self.assertEqual(test_case.previous_value, self.filter._previous_value)
-
- def test_cb_item_raw(self):
- """Test _cb_item_raw."""
- TestCase = collections.namedtuple("TestCase", "new_value, previous_value, instant_increase, instant_decrease, expected_value")
-
- test_cases = [
- TestCase(200, 100, False, False, 100),
- TestCase(200, 100, False, True, 100),
- TestCase(200, 100, True, False, 200),
- TestCase(200, None, False, False, 200),
- TestCase(200, None, False, True, 200),
- TestCase(200, None, True, False, 200),
-
- TestCase(50, 100, False, False, 100),
- TestCase(50, 100, False, True, 50),
- TestCase(50, 100, True, False, 100),
- TestCase(50, None, False, False, 50),
- TestCase(50, None, False, True, 50),
- TestCase(50, None, True, False, 50),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Filtered_2", 100) # set some "random" value
- self.filter_increase._previous_value = test_case.previous_value
- self.filter_increase._config.parameter.instant_increase = test_case.instant_increase
- self.filter_increase._config.parameter.instant_decrease = test_case.instant_decrease
-
- tests.helper.oh_item.item_state_change_event("Unittest_Raw", test_case.new_value)
-
- tests.helper.oh_item.assert_value("Unittest_Filtered_2", test_case.expected_value)
+ """Tests ExponentialFilter."""
+
+ 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_Raw", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Filtered_2", None)
+
+ config = habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered"), parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=10)
+ )
+
+ config_increase = habapp_rules.common.config.filter.ExponentialFilterConfig(
+ items=habapp_rules.common.config.filter.ExponentialFilterItems(raw="Unittest_Raw", filtered="Unittest_Filtered_2"), parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(tau=100, instant_increase=True)
+ )
+
+ self.filter = habapp_rules.common.filter.ExponentialFilter(config)
+ self.filter_increase = habapp_rules.common.filter.ExponentialFilter(config_increase)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ self.assertEqual("Unittest_Raw", self.filter._config.items.raw.name)
+ self.assertEqual("Unittest_Raw", self.filter_increase._config.items.raw.name)
+
+ self.assertEqual("Unittest_Filtered", self.filter._config.items.filtered.name)
+ self.assertEqual("Unittest_Filtered_2", self.filter_increase._config.items.filtered.name)
+
+ self.assertEqual(0.2, self.filter._alpha)
+ self.assertEqual(0.2, self.filter_increase._alpha)
+
+ self.assertFalse(self.filter._config.parameter.instant_increase)
+ self.assertFalse(self.filter._config.parameter.instant_decrease)
+
+ self.assertTrue(self.filter_increase._config.parameter.instant_increase)
+ self.assertFalse(self.filter_increase._config.parameter.instant_decrease)
+
+ def test_cb_cyclic_calculate_and_update_output(self) -> None:
+ """Test _cb_cyclic_calculate_and_update_output."""
+ TestCase = collections.namedtuple("TestCase", "new_value, previous_value, expected_result")
+
+ test_cases = [
+ TestCase(1, 1, 1),
+ TestCase(2, 1, 1.2),
+ TestCase(2, 0, 0.4),
+ TestCase(0, 2, 1.6),
+ TestCase(None, None, 999),
+ TestCase(None, 42, 999),
+ TestCase(42, None, 999),
+ ]
+
+ for test_case in test_cases:
+ tests.helper.oh_item.set_state("Unittest_Raw", test_case.new_value)
+ tests.helper.oh_item.set_state("Unittest_Filtered", 999) # set some random value
+ self.filter._previous_value = test_case.previous_value
+
+ self.filter._cb_cyclic_calculate_and_update_output()
+
+ self.assertEqual(test_case.expected_result, round(self.filter._config.items.filtered.value, 2))
+
+ if test_case.new_value is not None and test_case.previous_value is not None:
+ self.assertEqual(test_case.expected_result, round(self.filter._previous_value, 2))
+ else:
+ self.assertEqual(test_case.previous_value, self.filter._previous_value)
+
+ def test_cb_item_raw(self) -> None:
+ """Test _cb_item_raw."""
+ TestCase = collections.namedtuple("TestCase", "new_value, previous_value, instant_increase, instant_decrease, expected_value")
+
+ test_cases = [
+ TestCase(200, 100, False, False, 100),
+ TestCase(200, 100, False, True, 100),
+ TestCase(200, 100, True, False, 200),
+ TestCase(200, None, False, False, 200),
+ TestCase(200, None, False, True, 200),
+ TestCase(200, None, True, False, 200),
+ TestCase(50, 100, False, False, 100),
+ TestCase(50, 100, False, True, 50),
+ TestCase(50, 100, True, False, 100),
+ TestCase(50, None, False, False, 50),
+ TestCase(50, None, False, True, 50),
+ TestCase(50, None, True, False, 50),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Filtered_2", 100) # set some "random" value
+ self.filter_increase._previous_value = test_case.previous_value
+ self.filter_increase._config.parameter.instant_increase = test_case.instant_increase
+ self.filter_increase._config.parameter.instant_decrease = test_case.instant_decrease
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Raw", test_case.new_value)
+
+ tests.helper.oh_item.assert_value("Unittest_Filtered_2", test_case.expected_value)
diff --git a/tests/common/hysteresis.py b/tests/common/hysteresis.py
index 2f5838d..ece065f 100644
--- a/tests/common/hysteresis.py
+++ b/tests/common/hysteresis.py
@@ -1,103 +1,98 @@
"""Test for hysteresis switch."""
+
import collections
import unittest
import habapp_rules.common.hysteresis
-# pylint: disable=protected-access
class TestHysteresis(unittest.TestCase):
- """Tests for HysteresisSwitch."""
-
- def setUp(self):
- """Setup for all test cases."""
- self.hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(42, 2)
- self.hysteresis_switch_on_off = habapp_rules.common.hysteresis.HysteresisSwitch(42, 2, False)
-
- def test_get_output(self):
- """test get_output"""
- TestCase = collections.namedtuple("TestCase", "threshold, hysteresis, state, value, expected_result")
-
- test_cases = [
- # hysteresis = 1, current_state = False
- TestCase(10, 1, False, 9, False),
- TestCase(10, 1, False, 9.4, False),
- TestCase(10, 1, False, 9.5, False),
- TestCase(10, 1, False, 9.6, False),
- TestCase(10, 1, False, 10.4, False),
- TestCase(10, 1, False, 10.5, True),
- TestCase(10, 1, False, 10.5, True),
-
- # hysteresis = 1, current_state = True
- TestCase(10, 1, True, 9, False),
- TestCase(10, 1, True, 9.4, False),
- TestCase(10, 1, True, 9.5, True),
- TestCase(10, 1, True, 9.6, True),
- TestCase(10, 1, True, 10.4, True),
- TestCase(10, 1, True, 10.5, True),
- TestCase(10, 1, True, 10.5, True),
-
- # hysteresis = 4, current_state = False
- TestCase(42, 4, False, 39.9, False),
- TestCase(42, 4, False, 40, False),
- TestCase(42, 4, False, 40.1, False),
- TestCase(42, 4, False, 42, False),
- TestCase(42, 4, False, 43.9, False),
- TestCase(42, 4, False, 44, True),
- TestCase(42, 4, False, 44.1, True),
-
- # hysteresis = 4, current_state = True
- TestCase(42, 4, True, 39.9, False),
- TestCase(42, 4, True, 40, True),
- TestCase(42, 4, True, 40.1, True),
- TestCase(42, 4, True, 42, True),
- TestCase(42, 4, True, 43.9, True),
- TestCase(42, 4, True, 44, True),
- TestCase(42, 4, True, 44.1, True),
-
- # threshold not set -> always False
- TestCase(None, 4, True, 39.9, False),
- TestCase(None, 4, True, 40, False),
- TestCase(None, 4, True, 40.1, False),
- TestCase(None, 4, True, 42, False),
- TestCase(None, 4, True, 43.9, False),
- TestCase(None, 4, True, 44, False),
- TestCase(None, 4, True, 44.1, False),
- ]
-
- for test_case in test_cases:
- self.hysteresis_switch._threshold = test_case.threshold
- self.hysteresis_switch._hysteresis = test_case.hysteresis
- self.hysteresis_switch._on_off_state = test_case.state
- self.hysteresis_switch_on_off._threshold = test_case.threshold
- self.hysteresis_switch_on_off._hysteresis = test_case.hysteresis
- self.hysteresis_switch_on_off._on_off_state = test_case.state
-
- self.assertEqual(test_case.expected_result, self.hysteresis_switch.get_output(test_case.value))
- self.assertEqual("ON" if test_case.expected_result else "OFF", self.hysteresis_switch_on_off.get_output(test_case.value))
-
- self.assertEqual(test_case.expected_result, self.hysteresis_switch._on_off_state)
- self.assertEqual(test_case.expected_result, self.hysteresis_switch_on_off._on_off_state)
-
- def test_get_output_without_argument(self):
- """Test get_output without value argument."""
- self.hysteresis_switch._value_last = 10
- self.assertFalse(self.hysteresis_switch.get_output())
-
- self.hysteresis_switch._value_last = 100
- self.assertTrue(self.hysteresis_switch.get_output())
-
- self.hysteresis_switch.get_output(50)
- self.assertEqual(50, self.hysteresis_switch._value_last)
-
- def test_set_threshold(self):
- """test set_threshold."""
-
- self.assertEqual(42, self.hysteresis_switch._threshold)
- self.assertEqual(42, self.hysteresis_switch_on_off._threshold)
-
- self.hysteresis_switch.set_threshold_on(83)
- self.hysteresis_switch_on_off.set_threshold_on(83)
-
- self.assertEqual(83, self.hysteresis_switch._threshold)
- self.assertEqual(83, self.hysteresis_switch_on_off._threshold)
+ """Tests for HysteresisSwitch."""
+
+ def setUp(self) -> None:
+ """Setup for all test cases."""
+ self.hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(42, 2)
+ self.hysteresis_switch_on_off = habapp_rules.common.hysteresis.HysteresisSwitch(42, 2, False)
+
+ def test_get_output(self) -> None:
+ """Test get_output."""
+ TestCase = collections.namedtuple("TestCase", "threshold, hysteresis, state, value, expected_result")
+
+ test_cases = [
+ # hysteresis = 1, current_state = False
+ TestCase(10, 1, False, 9, False),
+ TestCase(10, 1, False, 9.4, False),
+ TestCase(10, 1, False, 9.5, False),
+ TestCase(10, 1, False, 9.6, False),
+ TestCase(10, 1, False, 10.4, False),
+ TestCase(10, 1, False, 10.5, True),
+ TestCase(10, 1, False, 10.5, True),
+ # hysteresis = 1, current_state = True
+ TestCase(10, 1, True, 9, False),
+ TestCase(10, 1, True, 9.4, False),
+ TestCase(10, 1, True, 9.5, True),
+ TestCase(10, 1, True, 9.6, True),
+ TestCase(10, 1, True, 10.4, True),
+ TestCase(10, 1, True, 10.5, True),
+ TestCase(10, 1, True, 10.5, True),
+ # hysteresis = 4, current_state = False
+ TestCase(42, 4, False, 39.9, False),
+ TestCase(42, 4, False, 40, False),
+ TestCase(42, 4, False, 40.1, False),
+ TestCase(42, 4, False, 42, False),
+ TestCase(42, 4, False, 43.9, False),
+ TestCase(42, 4, False, 44, True),
+ TestCase(42, 4, False, 44.1, True),
+ # hysteresis = 4, current_state = True
+ TestCase(42, 4, True, 39.9, False),
+ TestCase(42, 4, True, 40, True),
+ TestCase(42, 4, True, 40.1, True),
+ TestCase(42, 4, True, 42, True),
+ TestCase(42, 4, True, 43.9, True),
+ TestCase(42, 4, True, 44, True),
+ TestCase(42, 4, True, 44.1, True),
+ # threshold not set -> always False
+ TestCase(None, 4, True, 39.9, False),
+ TestCase(None, 4, True, 40, False),
+ TestCase(None, 4, True, 40.1, False),
+ TestCase(None, 4, True, 42, False),
+ TestCase(None, 4, True, 43.9, False),
+ TestCase(None, 4, True, 44, False),
+ TestCase(None, 4, True, 44.1, False),
+ ]
+
+ for test_case in test_cases:
+ self.hysteresis_switch._threshold = test_case.threshold
+ self.hysteresis_switch._hysteresis = test_case.hysteresis
+ self.hysteresis_switch._on_off_state = test_case.state
+ self.hysteresis_switch_on_off._threshold = test_case.threshold
+ self.hysteresis_switch_on_off._hysteresis = test_case.hysteresis
+ self.hysteresis_switch_on_off._on_off_state = test_case.state
+
+ self.assertEqual(test_case.expected_result, self.hysteresis_switch.get_output(test_case.value))
+ self.assertEqual("ON" if test_case.expected_result else "OFF", self.hysteresis_switch_on_off.get_output(test_case.value))
+
+ self.assertEqual(test_case.expected_result, self.hysteresis_switch._on_off_state)
+ self.assertEqual(test_case.expected_result, self.hysteresis_switch_on_off._on_off_state)
+
+ def test_get_output_without_argument(self) -> None:
+ """Test get_output without value argument."""
+ self.hysteresis_switch._value_last = 10
+ self.assertFalse(self.hysteresis_switch.get_output())
+
+ self.hysteresis_switch._value_last = 100
+ self.assertTrue(self.hysteresis_switch.get_output())
+
+ self.hysteresis_switch.get_output(50)
+ self.assertEqual(50, self.hysteresis_switch._value_last)
+
+ def test_set_threshold(self) -> None:
+ """Test set_threshold."""
+ self.assertEqual(42, self.hysteresis_switch._threshold)
+ self.assertEqual(42, self.hysteresis_switch_on_off._threshold)
+
+ self.hysteresis_switch.set_threshold_on(83)
+ self.hysteresis_switch_on_off.set_threshold_on(83)
+
+ self.assertEqual(83, self.hysteresis_switch._threshold)
+ self.assertEqual(83, self.hysteresis_switch_on_off._threshold)
diff --git a/tests/common/logic.py b/tests/common/logic.py
index a03153b..96d5418 100644
--- a/tests/common/logic.py
+++ b/tests/common/logic.py
@@ -1,4 +1,5 @@
"""Unit-test for logic functions."""
+
import collections
import unittest.mock
@@ -11,451 +12,337 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestAndOR(tests.helper.test_case_base.TestCaseBase):
- """Tests for AND / OR."""
-
- def setUp(self) -> None:
- """Setup unit-tests."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- self.post_update_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.post_update", new=tests.helper.oh_item.send_command)
- self.addCleanup(self.post_update_mock_patcher.stop)
- self.post_update_mock_patcher.start()
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_out", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in3", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_out", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in3", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "Unittest_DateTime", None)
-
- def test_and_callback_switch(self):
- """Test for switch items."""
- TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
-
- test_steps = [
- # test toggle of one switch
- TestStep("Unittest_Switch_in1", "ON", "OFF"),
- TestStep("Unittest_Switch_in1", "OFF", "OFF"),
-
- # switch on all
- TestStep("Unittest_Switch_in1", "ON", "OFF"),
- TestStep("Unittest_Switch_in2", "ON", "OFF"),
- TestStep("Unittest_Switch_in3", "ON", "ON"),
-
- # toggle one switch
- TestStep("Unittest_Switch_in1", "OFF", "OFF"),
- TestStep("Unittest_Switch_in1", "ON", "ON"),
-
- # switch off all
- TestStep("Unittest_Switch_in2", "OFF", "OFF"),
- TestStep("Unittest_Switch_in1", "OFF", "OFF"),
- TestStep("Unittest_Switch_in3", "OFF", "OFF"),
- ]
-
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Switch_in1", "Unittest_Switch_in2", "Unittest_Switch_in3"],
- output="Unittest_Switch_out"
- )
- )
-
- habapp_rules.common.logic.And(config)
- output_item = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch_out")
-
- for step in test_steps:
- tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
- self.assertEqual(step.expected_output, output_item.value)
-
- def test_or_callback_switch(self):
- """Test for switch items."""
- TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
-
- test_steps = [
- # test toggle of one switch
- TestStep("Unittest_Switch_in1", "ON", "ON"),
- TestStep("Unittest_Switch_in1", "OFF", "OFF"),
-
- # switch on all
- TestStep("Unittest_Switch_in1", "ON", "ON"),
- TestStep("Unittest_Switch_in2", "ON", "ON"),
- TestStep("Unittest_Switch_in3", "ON", "ON"),
-
- # toggle one switch
- TestStep("Unittest_Switch_in1", "OFF", "ON"),
- TestStep("Unittest_Switch_in1", "ON", "ON"),
-
- # switch off all
- TestStep("Unittest_Switch_in2", "OFF", "ON"),
- TestStep("Unittest_Switch_in1", "OFF", "ON"),
- TestStep("Unittest_Switch_in3", "OFF", "OFF"),
- ]
-
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Switch_in1", "Unittest_Switch_in2", "Unittest_Switch_in3"],
- output="Unittest_Switch_out"
- )
- )
-
- habapp_rules.common.logic.Or(config)
- output_item = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch_out")
-
- for step in test_steps:
- tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
- self.assertEqual(step.expected_output, output_item.value)
-
- def test_and_callback_contact(self):
- """Test for contact items."""
- TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
-
- test_steps = [
- # test toggle of one Contact
- TestStep("Unittest_Contact_in1", "CLOSED", "OPEN"),
- TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
-
- # Contact on all
- TestStep("Unittest_Contact_in1", "CLOSED", "OPEN"),
- TestStep("Unittest_Contact_in2", "CLOSED", "OPEN"),
- TestStep("Unittest_Contact_in3", "CLOSED", "CLOSED"),
-
- # toggle one Contact
- TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
- TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
-
- # Contact off all
- TestStep("Unittest_Contact_in2", "OPEN", "OPEN"),
- TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
- TestStep("Unittest_Contact_in3", "OPEN", "OPEN"),
- ]
-
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Contact_in1", "Unittest_Contact_in2", "Unittest_Contact_in3"],
- output="Unittest_Contact_out"
- )
- )
-
- habapp_rules.common.logic.And(config)
- output_item = HABApp.openhab.items.ContactItem.get_item("Unittest_Contact_out")
-
- for step in test_steps:
- tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
- self.assertEqual(step.expected_output, output_item.value)
-
- def test_or_callback_contact(self):
- """Test for contact items."""
- TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
-
- test_steps = [
- # test toggle of one Contact
- TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
- TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
-
- # Contact on all
- TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
- TestStep("Unittest_Contact_in2", "CLOSED", "CLOSED"),
- TestStep("Unittest_Contact_in3", "CLOSED", "CLOSED"),
-
- # toggle one Contact
- TestStep("Unittest_Contact_in1", "OPEN", "CLOSED"),
- TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
-
- # Contact off all
- TestStep("Unittest_Contact_in2", "OPEN", "CLOSED"),
- TestStep("Unittest_Contact_in1", "OPEN", "CLOSED"),
- TestStep("Unittest_Contact_in3", "OPEN", "OPEN"),
- ]
-
- config = habapp_rules.common.config.logic.BinaryLogicConfig(
- items=habapp_rules.common.config.logic.BinaryLogicItems(
- inputs=["Unittest_Contact_in1", "Unittest_Contact_in2", "Unittest_Contact_in3"],
- output="Unittest_Contact_out"
- )
- )
-
- habapp_rules.common.logic.Or(config)
- output_item = HABApp.openhab.items.ContactItem.get_item("Unittest_Contact_out")
-
- for step in test_steps:
- tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
- self.assertEqual(step.expected_output, output_item.value)
+ """Tests for AND / OR."""
+
+ def setUp(self) -> None:
+ """Setup unit-tests."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ self.post_update_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.post_update", new=tests.helper.oh_item.send_command)
+ self.addCleanup(self.post_update_mock_patcher.stop)
+ self.post_update_mock_patcher.start()
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_out", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_in3", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_out", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact_in3", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "Unittest_DateTime", None)
+
+ def test_and_callback_switch(self) -> None:
+ """Test for switch items."""
+ TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
+
+ test_steps = [
+ # test toggle of one switch
+ TestStep("Unittest_Switch_in1", "ON", "OFF"),
+ TestStep("Unittest_Switch_in1", "OFF", "OFF"),
+ # switch on all
+ TestStep("Unittest_Switch_in1", "ON", "OFF"),
+ TestStep("Unittest_Switch_in2", "ON", "OFF"),
+ TestStep("Unittest_Switch_in3", "ON", "ON"),
+ # toggle one switch
+ TestStep("Unittest_Switch_in1", "OFF", "OFF"),
+ TestStep("Unittest_Switch_in1", "ON", "ON"),
+ # switch off all
+ TestStep("Unittest_Switch_in2", "OFF", "OFF"),
+ TestStep("Unittest_Switch_in1", "OFF", "OFF"),
+ TestStep("Unittest_Switch_in3", "OFF", "OFF"),
+ ]
+
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(items=habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Switch_in1", "Unittest_Switch_in2", "Unittest_Switch_in3"], output="Unittest_Switch_out"))
+
+ habapp_rules.common.logic.And(config)
+ output_item = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch_out")
+
+ for step in test_steps:
+ tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
+ self.assertEqual(step.expected_output, output_item.value)
+
+ def test_or_callback_switch(self) -> None:
+ """Test for switch items."""
+ TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
+
+ test_steps = [
+ # test toggle of one switch
+ TestStep("Unittest_Switch_in1", "ON", "ON"),
+ TestStep("Unittest_Switch_in1", "OFF", "OFF"),
+ # switch on all
+ TestStep("Unittest_Switch_in1", "ON", "ON"),
+ TestStep("Unittest_Switch_in2", "ON", "ON"),
+ TestStep("Unittest_Switch_in3", "ON", "ON"),
+ # toggle one switch
+ TestStep("Unittest_Switch_in1", "OFF", "ON"),
+ TestStep("Unittest_Switch_in1", "ON", "ON"),
+ # switch off all
+ TestStep("Unittest_Switch_in2", "OFF", "ON"),
+ TestStep("Unittest_Switch_in1", "OFF", "ON"),
+ TestStep("Unittest_Switch_in3", "OFF", "OFF"),
+ ]
+
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(items=habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Switch_in1", "Unittest_Switch_in2", "Unittest_Switch_in3"], output="Unittest_Switch_out"))
+
+ habapp_rules.common.logic.Or(config)
+ output_item = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch_out")
+
+ for step in test_steps:
+ tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
+ self.assertEqual(step.expected_output, output_item.value)
+
+ def test_and_callback_contact(self) -> None:
+ """Test for contact items."""
+ TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
+
+ test_steps = [
+ # test toggle of one Contact
+ TestStep("Unittest_Contact_in1", "CLOSED", "OPEN"),
+ TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
+ # Contact on all
+ TestStep("Unittest_Contact_in1", "CLOSED", "OPEN"),
+ TestStep("Unittest_Contact_in2", "CLOSED", "OPEN"),
+ TestStep("Unittest_Contact_in3", "CLOSED", "CLOSED"),
+ # toggle one Contact
+ TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
+ TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
+ # Contact off all
+ TestStep("Unittest_Contact_in2", "OPEN", "OPEN"),
+ TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
+ TestStep("Unittest_Contact_in3", "OPEN", "OPEN"),
+ ]
+
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(items=habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Contact_in1", "Unittest_Contact_in2", "Unittest_Contact_in3"], output="Unittest_Contact_out"))
+
+ habapp_rules.common.logic.And(config)
+ output_item = HABApp.openhab.items.ContactItem.get_item("Unittest_Contact_out")
+
+ for step in test_steps:
+ tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
+ self.assertEqual(step.expected_output, output_item.value)
+
+ def test_or_callback_contact(self) -> None:
+ """Test for contact items."""
+ TestStep = collections.namedtuple("TestStep", "event_item_name, event_item_value, expected_output")
+
+ test_steps = [
+ # test toggle of one Contact
+ TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
+ TestStep("Unittest_Contact_in1", "OPEN", "OPEN"),
+ # Contact on all
+ TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
+ TestStep("Unittest_Contact_in2", "CLOSED", "CLOSED"),
+ TestStep("Unittest_Contact_in3", "CLOSED", "CLOSED"),
+ # toggle one Contact
+ TestStep("Unittest_Contact_in1", "OPEN", "CLOSED"),
+ TestStep("Unittest_Contact_in1", "CLOSED", "CLOSED"),
+ # Contact off all
+ TestStep("Unittest_Contact_in2", "OPEN", "CLOSED"),
+ TestStep("Unittest_Contact_in1", "OPEN", "CLOSED"),
+ TestStep("Unittest_Contact_in3", "OPEN", "OPEN"),
+ ]
+
+ config = habapp_rules.common.config.logic.BinaryLogicConfig(items=habapp_rules.common.config.logic.BinaryLogicItems(inputs=["Unittest_Contact_in1", "Unittest_Contact_in2", "Unittest_Contact_in3"], output="Unittest_Contact_out"))
+
+ habapp_rules.common.logic.Or(config)
+ output_item = HABApp.openhab.items.ContactItem.get_item("Unittest_Contact_out")
+
+ for step in test_steps:
+ tests.helper.oh_item.send_command(step.event_item_name, step.event_item_value)
+ self.assertEqual(step.expected_output, output_item.value)
class TestNumericLogic(tests.helper.test_case_base.TestCaseBase):
- """Tests Numeric logic rules."""
-
- 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", 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", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in3", None)
-
- 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", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in3", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "Unittest_DateTime", None)
-
- 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, 100, 100, 100),
- TestStep(1, 0, 0, 0, 0),
- TestStep(1, -100, -100, -100, -100),
-
- # change all values to 5000
- TestStep(1, 5000, 5000, 5000, 5000),
- TestStep(2, 5000, 5000, 5000, 10_000),
- TestStep(3, 5000, 5000, 5000, 15_000),
-
- # some random values
- TestStep(3, -1000, -1000, 5000, 9000),
- TestStep(3, -500, -500, 5000, 9500),
- TestStep(1, 200, -500, 5000, 4700)
- ]
-
- config_min = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"],
- output="Unittest_Number_out_min"
- )
- )
-
- config_max = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"],
- output="Unittest_Number_out_max"
- )
- )
-
- config_sum = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"],
- output="Unittest_Number_out_sum"
- )
- )
-
- habapp_rules.common.logic.Min(config_min)
- habapp_rules.common.logic.Max(config_max)
- habapp_rules.common.logic.Sum(config_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."""
- TestStep = collections.namedtuple("TestStep", "event_item_index, event_item_value, expected_min, expected_max")
-
- test_steps = [
- # test change single value
- TestStep(1, 100, 100, 100),
- TestStep(1, 0, 0, 0),
- TestStep(1, 50, 50, 50),
-
- # change all values to 80
- TestStep(1, 80, 80, 80),
- TestStep(2, 80, 80, 80),
- TestStep(3, 80, 80, 80),
-
- # some random values
- TestStep(3, 1, 1, 80),
- TestStep(3, 20, 20, 80),
- TestStep(1, 50, 20, 80)
- ]
-
- config_min = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"],
- output="Unittest_Dimmer_out_min"
- )
- )
-
- config_max = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"],
- output="Unittest_Dimmer_out_max"
- )
- )
-
- habapp_rules.common.logic.Min(config_min)
- habapp_rules.common.logic.Max(config_max)
-
- output_item_dimmer_min = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_out_min")
- output_item_dimmer_max = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_out_max")
-
- for step in test_steps:
- tests.helper.oh_item.item_state_change_event(f"Unittest_Dimmer_in{step.event_item_index}", step.event_item_value)
-
- self.assertEqual(step.expected_min, output_item_dimmer_min.value)
- self.assertEqual(step.expected_max, output_item_dimmer_max.value)
-
- def test_cb_input_event(self):
- """Test _cb_input_event."""
- config_min = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"],
- output="Unittest_Dimmer_out_min"
- )
- )
-
- config_max = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"],
- output="Unittest_Dimmer_out_max"
- )
- )
-
- rule_min = habapp_rules.common.logic.Min(config_min)
- rule_max = habapp_rules.common.logic.Max(config_max)
-
- with unittest.mock.patch("habapp_rules.core.helper.filter_updated_items", return_value=[None]), unittest.mock.patch.object(rule_min, "_set_output_state") as set_output_mock:
- rule_min._cb_input_event(None)
- set_output_mock.assert_not_called()
-
- 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."""
- config_max = habapp_rules.common.config.logic.NumericLogicConfig(
- items=habapp_rules.common.config.logic.NumericLogicItems(
- inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"],
- output="Unittest_Dimmer_out_max"
- )
- )
-
- with self.assertRaises(TypeError):
- habapp_rules.common.logic.Sum(config_max)
+ """Tests Numeric logic rules."""
+
+ 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", 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", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number_in3", None)
+
+ 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", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_in3", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Contact", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.RollershutterItem, "Unittest_RollerShutter", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "Unittest_DateTime", None)
+
+ def test_number_min_max_sum_without_filter(self) -> None:
+ """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, 100, 100, 100),
+ TestStep(1, 0, 0, 0, 0),
+ TestStep(1, -100, -100, -100, -100),
+ # change all values to 5000
+ TestStep(1, 5000, 5000, 5000, 5000),
+ TestStep(2, 5000, 5000, 5000, 10_000),
+ TestStep(3, 5000, 5000, 5000, 15_000),
+ # some random values
+ TestStep(3, -1000, -1000, 5000, 9000),
+ TestStep(3, -500, -500, 5000, 9500),
+ TestStep(1, 200, -500, 5000, 4700),
+ ]
+
+ config_min = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], output="Unittest_Number_out_min"))
+
+ config_max = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], output="Unittest_Number_out_max"))
+
+ config_sum = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Number_in1", "Unittest_Number_in2", "Unittest_Number_in3"], output="Unittest_Number_out_sum"))
+
+ habapp_rules.common.logic.Min(config_min)
+ habapp_rules.common.logic.Max(config_max)
+ habapp_rules.common.logic.Sum(config_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) -> None:
+ """Test min / max for dimmer items."""
+ TestStep = collections.namedtuple("TestStep", "event_item_index, event_item_value, expected_min, expected_max")
+
+ test_steps = [
+ # test change single value
+ TestStep(1, 100, 100, 100),
+ TestStep(1, 0, 0, 0),
+ TestStep(1, 50, 50, 50),
+ # change all values to 80
+ TestStep(1, 80, 80, 80),
+ TestStep(2, 80, 80, 80),
+ TestStep(3, 80, 80, 80),
+ # some random values
+ TestStep(3, 1, 1, 80),
+ TestStep(3, 20, 20, 80),
+ TestStep(1, 50, 20, 80),
+ ]
+
+ config_min = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], output="Unittest_Dimmer_out_min"))
+
+ config_max = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], output="Unittest_Dimmer_out_max"))
+
+ habapp_rules.common.logic.Min(config_min)
+ habapp_rules.common.logic.Max(config_max)
+
+ output_item_dimmer_min = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_out_min")
+ output_item_dimmer_max = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_out_max")
+
+ for step in test_steps:
+ tests.helper.oh_item.item_state_change_event(f"Unittest_Dimmer_in{step.event_item_index}", step.event_item_value)
+
+ self.assertEqual(step.expected_min, output_item_dimmer_min.value)
+ self.assertEqual(step.expected_max, output_item_dimmer_max.value)
+
+ def test_cb_input_event(self) -> None:
+ """Test _cb_input_event."""
+ config_min = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], output="Unittest_Dimmer_out_min"))
+
+ config_max = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], output="Unittest_Dimmer_out_max"))
+
+ rule_min = habapp_rules.common.logic.Min(config_min)
+ rule_max = habapp_rules.common.logic.Max(config_max)
+
+ with unittest.mock.patch("habapp_rules.core.helper.filter_updated_items", return_value=[None]), unittest.mock.patch.object(rule_min, "_set_output_state") as set_output_mock:
+ rule_min._cb_input_event(None)
+ set_output_mock.assert_not_called()
+
+ 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) -> None:
+ """Test exception if Sum is instantiated with dimmer items."""
+ config_max = habapp_rules.common.config.logic.NumericLogicConfig(items=habapp_rules.common.config.logic.NumericLogicItems(inputs=["Unittest_Dimmer_in1", "Unittest_Dimmer_in2", "Unittest_Dimmer_in3"], output="Unittest_Dimmer_out_max"))
+
+ with self.assertRaises(TypeError):
+ habapp_rules.common.logic.Sum(config_max)
class TestInvertValue(tests.helper.test_case_base.TestCaseBase):
- """Tests InvertValue rule."""
-
- 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_Input", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Output", None)
-
- def test_invert_value_without_pos_neg(self):
- """Test invert value rule without pos / neg set."""
- TestCase = collections.namedtuple("TestCase", "input, expected_output")
-
- test_cases = [
- TestCase(10, -10),
- TestCase(1, -1),
- TestCase(0.1, -0.1),
- TestCase(0, 0),
- TestCase(-0.1, 0.1),
- TestCase(-1, 1),
- TestCase(-10, 10)
- ]
-
- config = habapp_rules.common.config.logic.InvertValueConfig(
- items=habapp_rules.common.config.logic.InvertValueItems(
- input="Unittest_Input",
- output="Unittest_Output"
- )
- )
-
- habapp_rules.common.logic.InvertValue(config)
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
- tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
-
- def test_invert_value_with_only_pos(self):
- """Test invert value rule with only pos is set."""
- TestCase = collections.namedtuple("TestCase", "input, expected_output")
-
- test_cases = [
- TestCase(10, 0),
- TestCase(1, 0),
- TestCase(0.1, 0),
- TestCase(0, 0),
- TestCase(-0.1, 0.1),
- TestCase(-1, 1),
- TestCase(-10, 10)
- ]
-
- config = habapp_rules.common.config.logic.InvertValueConfig(
- items=habapp_rules.common.config.logic.InvertValueItems(
- input="Unittest_Input",
- output="Unittest_Output"
- ),
- parameter=habapp_rules.common.config.logic.InvertValueParameter(only_positive=True)
- )
-
- habapp_rules.common.logic.InvertValue(config)
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
- tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
-
- def test_invert_value_with_only_neg(self):
- """Test invert value rule with only neg is set."""
- TestCase = collections.namedtuple("TestCase", "input, expected_output")
-
- test_cases = [
- TestCase(10, -10),
- TestCase(1, -1),
- TestCase(0.1, -0.1),
- TestCase(0, 0),
- TestCase(-0.1, 0),
- TestCase(-1, 0),
- TestCase(-10, 0)
- ]
-
- config = habapp_rules.common.config.logic.InvertValueConfig(
- items=habapp_rules.common.config.logic.InvertValueItems(
- input="Unittest_Input",
- output="Unittest_Output"
- ),
- parameter=habapp_rules.common.config.logic.InvertValueParameter(only_negative=True)
- )
-
- habapp_rules.common.logic.InvertValue(config)
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
- tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
+ """Tests InvertValue rule."""
+
+ 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_Input", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Output", None)
+
+ def test_invert_value_without_pos_neg(self) -> None:
+ """Test invert value rule without pos / neg set."""
+ TestCase = collections.namedtuple("TestCase", "input, expected_output")
+
+ test_cases = [TestCase(10, -10), TestCase(1, -1), TestCase(0.1, -0.1), TestCase(0, 0), TestCase(-0.1, 0.1), TestCase(-1, 1), TestCase(-10, 10)]
+
+ config = habapp_rules.common.config.logic.InvertValueConfig(items=habapp_rules.common.config.logic.InvertValueItems(input="Unittest_Input", output="Unittest_Output"))
+
+ habapp_rules.common.logic.InvertValue(config)
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
+ tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
+
+ def test_invert_value_with_only_pos(self) -> None:
+ """Test invert value rule with only pos is set."""
+ TestCase = collections.namedtuple("TestCase", "input, expected_output")
+
+ test_cases = [TestCase(10, 0), TestCase(1, 0), TestCase(0.1, 0), TestCase(0, 0), TestCase(-0.1, 0.1), TestCase(-1, 1), TestCase(-10, 10)]
+
+ config = habapp_rules.common.config.logic.InvertValueConfig(
+ items=habapp_rules.common.config.logic.InvertValueItems(input="Unittest_Input", output="Unittest_Output"), parameter=habapp_rules.common.config.logic.InvertValueParameter(only_positive=True)
+ )
+
+ habapp_rules.common.logic.InvertValue(config)
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
+ tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
+
+ def test_invert_value_with_only_neg(self) -> None:
+ """Test invert value rule with only neg is set."""
+ TestCase = collections.namedtuple("TestCase", "input, expected_output")
+
+ test_cases = [TestCase(10, -10), TestCase(1, -1), TestCase(0.1, -0.1), TestCase(0, 0), TestCase(-0.1, 0), TestCase(-1, 0), TestCase(-10, 0)]
+
+ config = habapp_rules.common.config.logic.InvertValueConfig(
+ items=habapp_rules.common.config.logic.InvertValueItems(input="Unittest_Input", output="Unittest_Output"), parameter=habapp_rules.common.config.logic.InvertValueParameter(only_negative=True)
+ )
+
+ habapp_rules.common.logic.InvertValue(config)
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.item_state_change_event("Unittest_Input", test_case.input)
+ tests.helper.oh_item.assert_value("Unittest_Output", test_case.expected_output)
diff --git a/tests/core/helper.py b/tests/core/helper.py
index 7172c77..fc2ec18 100644
--- a/tests/core/helper.py
+++ b/tests/core/helper.py
@@ -1,10 +1,12 @@
"""Unit tests for habapp_rules helper."""
+
import collections
+import time
import unittest
import unittest.mock
import HABApp
-import pendulum
+import whenever
import habapp_rules.core.exceptions
import habapp_rules.core.helper
@@ -14,87 +16,82 @@
class TestHelperFunctions(tests.helper.test_case_base.TestCaseBase):
- """Tests for all helper functions"""
-
- def test_create_additional_item(self):
- """Test create additional item."""
- # check if item is created if NOT existing
- self.item_exists_mock.return_value = False
- TestCase = collections.namedtuple("TestCase", "item_type, name, label_input, label_call, groups")
-
- test_cases = [
- TestCase("Switch", "Item_name", "Some label", "Some label", None),
- TestCase("Switch", "Item_name", None, "Item name", None),
- TestCase("String", "Item_name", "Some label", "Some label", None),
- TestCase("String", "Item_name", "Some label", "Some label", None),
- TestCase("String", "Item_name", None, "Item name", None),
- TestCase("String", "Item_name", None, "Item name", ["test_group"]),
- ]
-
- with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item) as create_mock, \
- unittest.mock.patch("HABApp.openhab.items.OpenhabItem.get_item"):
- for test_case in test_cases:
- create_mock.reset_mock()
- habapp_rules.core.helper.create_additional_item(test_case.name, test_case.item_type, test_case.label_input, test_case.groups)
- create_mock.assert_called_once_with(item_type=test_case.item_type, name=f"H_{test_case.name}", label=test_case.label_call, groups=test_case.groups)
-
- # check if item is NOT created if existing
- self.item_exists_mock.return_value = True
- with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item) as create_mock, \
- unittest.mock.patch("HABApp.openhab.items.OpenhabItem.get_item"):
- habapp_rules.core.helper.create_additional_item("Name_of_Item", "Switch")
- create_mock.assert_not_called()
-
- def test_test_create_additional_item_exception(self):
- """Test exceptions of _create_additional_item."""
- self.item_exists_mock.return_value = False
- with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item, return_value=False), \
- self.assertRaises(habapp_rules.core.exceptions.HabAppRulesException):
- habapp_rules.core.helper.create_additional_item("Name_of_Item", "Switch")
-
- def test_send_if_different(self):
- """Test send_if_different."""
- # item given
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", 0)
- number_item = HABApp.openhab.items.NumberItem.get_item("Unittest_Number")
-
- habapp_rules.core.helper.send_if_different(number_item, 0)
- tests.helper.oh_item.assert_value("Unittest_Number", 0)
-
- habapp_rules.core.helper.send_if_different(number_item, 42)
- tests.helper.oh_item.assert_value("Unittest_Number", 42)
-
- # name given
- habapp_rules.core.helper.send_if_different("Unittest_Number", 42)
- tests.helper.oh_item.assert_value("Unittest_Number", 42)
-
- habapp_rules.core.helper.send_if_different("Unittest_Number", 84)
- tests.helper.oh_item.assert_value("Unittest_Number", 84)
-
-
-# pylint: disable=protected-access
-class TestHelperWithItems(tests.helper.test_case_base.TestCaseBase):
- """Test helper functions with OpenHAB items"""
+ """Tests for all helper functions."""
+
+ def test_create_additional_item(self) -> None:
+ """Test create additional item."""
+ # check if item is created if NOT existing
+ self.item_exists_mock.return_value = False
+ TestCase = collections.namedtuple("TestCase", "item_type, name, label_input, label_call, groups")
+
+ test_cases = [
+ TestCase("Switch", "Item_name", "Some label", "Some label", None),
+ TestCase("Switch", "Item_name", None, "Item name", None),
+ TestCase("String", "Item_name", "Some label", "Some label", None),
+ TestCase("String", "Item_name", "Some label", "Some label", None),
+ TestCase("String", "Item_name", None, "Item name", None),
+ TestCase("String", "Item_name", None, "Item name", ["test_group"]),
+ ]
+
+ with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item) as create_mock, unittest.mock.patch("HABApp.openhab.items.OpenhabItem.get_item"):
+ for test_case in test_cases:
+ create_mock.reset_mock()
+ habapp_rules.core.helper.create_additional_item(test_case.name, test_case.item_type, test_case.label_input, test_case.groups)
+ create_mock.assert_called_once_with(item_type=test_case.item_type, name=f"H_{test_case.name}", label=test_case.label_call, groups=test_case.groups)
+
+ # check if item is NOT created if existing
+ self.item_exists_mock.return_value = True
+ with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item) as create_mock, unittest.mock.patch("HABApp.openhab.items.OpenhabItem.get_item"):
+ habapp_rules.core.helper.create_additional_item("Name_of_Item", "Switch")
+ create_mock.assert_not_called()
+
+ def test_test_create_additional_item_exception(self) -> None:
+ """Test exceptions of _create_additional_item."""
+ self.item_exists_mock.return_value = False
+ with unittest.mock.patch("HABApp.openhab.interface_sync.create_item", spec=HABApp.openhab.interface_sync.create_item, return_value=False), self.assertRaises(habapp_rules.core.exceptions.HabAppRulesError):
+ habapp_rules.core.helper.create_additional_item("Name_of_Item", "Switch")
+
+ def test_send_if_different(self) -> None:
+ """Test send_if_different."""
+ # item given
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", 0)
+ number_item = HABApp.openhab.items.NumberItem.get_item("Unittest_Number")
+
+ habapp_rules.core.helper.send_if_different(number_item, 0)
+ tests.helper.oh_item.assert_value("Unittest_Number", 0)
+
+ habapp_rules.core.helper.send_if_different(number_item, 42)
+ tests.helper.oh_item.assert_value("Unittest_Number", 42)
+
+ # name given
+ habapp_rules.core.helper.send_if_different("Unittest_Number", 42)
+ tests.helper.oh_item.assert_value("Unittest_Number", 42)
+
+ habapp_rules.core.helper.send_if_different("Unittest_Number", 84)
+ tests.helper.oh_item.assert_value("Unittest_Number", 84)
- def test_filter_updated_items(self):
- """Test filter_updated_items."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", "OFF")
+class TestHelperWithItems(tests.helper.test_case_base.TestCaseBase):
+ """Test helper functions with OpenHAB items."""
+
+ def test_filter_updated_items(self) -> None:
+ """Test filter_updated_items."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", "OFF")
- item_number = HABApp.openhab.items.NumberItem.get_item("Unittest_Number")
- item_dimmer = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer")
- item_switch = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch")
+ item_number = HABApp.openhab.items.NumberItem.get_item("Unittest_Number")
+ item_dimmer = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer")
+ item_switch = HABApp.openhab.items.SwitchItem.get_item("Unittest_Switch")
- # without filter
- result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch])
- self.assertListEqual([item_number, item_dimmer, item_switch], result)
+ # without filter
+ result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch])
+ self.assertListEqual([item_number, item_dimmer, item_switch], result)
- # with filter
- result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch], 60)
- self.assertListEqual([item_number, item_dimmer, item_switch], result)
+ # with filter
+ result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch], 60)
+ self.assertListEqual([item_number, item_dimmer, item_switch], result)
- item_dimmer._last_update = HABApp.core.items.base_item.UpdatedTime("Unittest_Dimmer", pendulum.DateTime.now().subtract(seconds=61))
- result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch], 60)
- self.assertListEqual([item_number, item_switch], result)
+ item_dimmer._last_update = HABApp.core.items.base_item.UpdatedTime("Unittest_Dimmer", whenever.Instant.from_timestamp(time.time() - 61))
+ result = habapp_rules.core.helper.filter_updated_items([item_number, item_dimmer, item_switch], 60)
+ self.assertListEqual([item_number, item_switch], result)
diff --git a/tests/core/logger.py b/tests/core/logger.py
index c5c0d9b..0ed0414 100644
--- a/tests/core/logger.py
+++ b/tests/core/logger.py
@@ -1,4 +1,5 @@
"""Unit tests for habapp_rules logger."""
+
import logging
import unittest
import unittest.mock
@@ -7,41 +8,44 @@
class TestLoggerFunctions(unittest.TestCase):
- """Tests for all logger functions"""
+ """Tests for all logger functions."""
+
+ def test_setup_logger(self) -> None:
+ """Test setup_logger."""
+ stream_handler_mock = unittest.mock.MagicMock()
+ file_handler_mock = unittest.mock.MagicMock()
- def test_setup_logger(self):
- """Test setup_logger."""
- stream_handler_mock = unittest.mock.MagicMock()
- file_handler_mock = unittest.mock.MagicMock()
+ with unittest.mock.patch("logging.StreamHandler", return_value=stream_handler_mock), unittest.mock.patch("HABApp.config.logging.MidnightRotatingFileHandler", return_value=file_handler_mock):
+ habapp_rules.core.logger.setup_logger()
- with unittest.mock.patch("logging.StreamHandler", return_value=stream_handler_mock), \
- unittest.mock.patch("HABApp.config.logging.MidnightRotatingFileHandler", return_value=file_handler_mock):
- habapp_rules.core.logger.setup_logger()
+ stream_handler_mock.setFormatter.assert_called_once()
+ stream_handler_mock.setLevel.assert_called_once_with(logging.DEBUG)
- stream_handler_mock.setFormatter.assert_called_once()
- stream_handler_mock.setLevel.assert_called_once_with(logging.DEBUG)
+ file_handler_mock.setFormatter.assert_called_once()
+ file_handler_mock.setLevel.assert_called_once_with(logging.DEBUG)
- file_handler_mock.setFormatter.assert_called_once()
- file_handler_mock.setLevel.assert_called_once_with(logging.DEBUG)
+ # path is existing
+ with unittest.mock.patch("pathlib.Path.is_dir", return_value=False), unittest.mock.patch("pathlib.Path.mkdir") as makedirs_mock:
+ habapp_rules.core.logger.setup_logger()
+ makedirs_mock.assert_called_once_with(parents=True)
- # path is existing
- with unittest.mock.patch("pathlib.Path.is_dir", return_value=False), unittest.mock.patch("os.makedirs") as makedirs_mock:
- habapp_rules.core.logger.setup_logger()
- makedirs_mock.assert_called_once()
+ # path is not existing
+ with unittest.mock.patch("pathlib.Path.is_dir", return_value=True), unittest.mock.patch("pathlib.Path.mkdir") as makedirs_mock:
+ habapp_rules.core.logger.setup_logger()
+ makedirs_mock.assert_not_called()
- # path is not existing
- with unittest.mock.patch("pathlib.Path.is_dir", return_value=True), unittest.mock.patch("os.makedirs") as makedirs_mock:
- habapp_rules.core.logger.setup_logger()
- makedirs_mock.assert_not_called()
+ # remove handler
+ habapp_rules_logger = logging.getLogger("habapp_rules")
+ habapp_rules_logger.removeHandler(stream_handler_mock)
+ habapp_rules_logger.removeHandler(file_handler_mock)
-# pylint: disable=protected-access
class TestInstanceLogger(unittest.TestCase):
- """Test for instanceLogger"""
+ """Test for instanceLogger."""
- def test_instance_logger(self):
- """Test instance_logger"""
- instance_logger = habapp_rules.core.logger.InstanceLogger(logging.getLogger(__name__), "test_instance")
+ def test_instance_logger(self) -> None:
+ """Test instance_logger."""
+ instance_logger = habapp_rules.core.logger.InstanceLogger(logging.getLogger(__name__), "test_instance")
- self.assertEqual("test_instance", instance_logger._instance_name)
- self.assertEqual(("test_instance | test message", {}), instance_logger.process("test message", {}))
+ self.assertEqual("test_instance", instance_logger._instance_name)
+ self.assertEqual(("test_instance | test message", {}), instance_logger.process("test message", {}))
diff --git a/tests/core/pydantic_base.py b/tests/core/pydantic_base.py
index 122375b..a750e93 100644
--- a/tests/core/pydantic_base.py
+++ b/tests/core/pydantic_base.py
@@ -1,4 +1,5 @@
"""Test pydantic base models."""
+
import unittest.mock
import HABApp
@@ -11,59 +12,65 @@
class ItemsForTesting(habapp_rules.core.pydantic_base.ItemBase):
- """Items for testing."""
- switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item for testing")
- switch_create: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item for testing", json_schema_extra={"create_if_not_exists": True})
- dimmer_list: list[HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="list of dimmer items for testing")
- optional_contact: HABApp.openhab.items.ContactItem | None = pydantic.Field(None, description="optional contact item for testing")
- not_supported: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="not supported item for testing")
+ """Items for testing."""
+
+ switch: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item for testing")
+ switch_create: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="switch item for testing", json_schema_extra={"create_if_not_exists": True})
+ dimmer_list: list[HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="list of dimmer items for testing")
+ optional_contact: HABApp.openhab.items.ContactItem | None = pydantic.Field(None, description="optional contact item for testing")
+ not_supported: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="not supported item for testing")
class ItemsListCreateException(habapp_rules.core.pydantic_base.ItemBase):
- """Model with list object where create_if_not_exists is set."""
- some_items: list[HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="list of items for testing", json_schema_extra={"create_if_not_exists": True})
+ """Model with list object where create_if_not_exists is set."""
+
+ some_items: list[HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem] = pydantic.Field(..., description="list of items for testing", json_schema_extra={"create_if_not_exists": True})
class WrongTypeException(habapp_rules.core.pydantic_base.ItemBase):
- """Model with wrong type."""
- item: str = pydantic.Field(..., description="wrong type for testing")
+ """Model with wrong type."""
+
+ item: str = pydantic.Field(..., description="wrong type for testing")
class MultipleTypeForCreateException(habapp_rules.core.pydantic_base.ItemBase):
- """Model with multiple types where create_if_not_exists is set."""
- item: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="list of items for testing", json_schema_extra={"create_if_not_exists": True})
+ """Model with multiple types where create_if_not_exists is set."""
+
+ item: HABApp.openhab.items.SwitchItem | HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="list of items for testing", json_schema_extra={"create_if_not_exists": True})
class TestItemBase(tests.helper.test_case_base.TestCaseBase):
- """Test ItemBase."""
-
- def test_check_all_fields_oh_items_exceptions(self):
- """Test all exceptions of check_all_fields_oh_items."""
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- ItemsListCreateException(some_items=["Name1", "Name2"])
-
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- WrongTypeException(item="Name1")
-
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- MultipleTypeForCreateException(item="Name1")
-
- def test_convert_to_oh_item(self):
- """Test convert_to_oh_item."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_2", None)
-
- dimmer = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_2")
-
- with (unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.SwitchItem("Unittest_Switch_Created", "")) as create_item_mock,
- self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException)):
- ItemsForTesting(
- switch="Unittest_Switch", # normal case
- switch_create="Unittest_Switch_Created", # item which will be created
- dimmer_list=["Unittest_Dimmer_1", dimmer], # mixed list of strings and HABApp.openhab.items.DimmerItem
- optional_contact=None, # test if None is OK
- not_supported=5 # this causes an exception
- )
-
- create_item_mock.assert_called_once_with("Unittest_Switch_Created", "Switch")
+ """Test ItemBase."""
+
+ def test_check_all_fields_oh_items_exceptions(self) -> None:
+ """Test all exceptions of check_all_fields_oh_items."""
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ ItemsListCreateException(some_items=["Name1", "Name2"])
+
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ WrongTypeException(item="Name1")
+
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ MultipleTypeForCreateException(item="Name1")
+
+ def test_convert_to_oh_item(self) -> None:
+ """Test convert_to_oh_item."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DimmerItem, "Unittest_Dimmer_2", None)
+
+ dimmer = HABApp.openhab.items.DimmerItem.get_item("Unittest_Dimmer_2")
+
+ with (
+ unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.SwitchItem("Unittest_Switch_Created", "")) as create_item_mock,
+ self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError),
+ ):
+ ItemsForTesting(
+ switch="Unittest_Switch", # normal case
+ switch_create="Unittest_Switch_Created", # item which will be created
+ dimmer_list=["Unittest_Dimmer_1", dimmer], # mixed list of strings and HABApp.openhab.items.DimmerItem
+ optional_contact=None, # test if None is OK
+ not_supported=5, # this causes an exception
+ )
+
+ create_item_mock.assert_called_once_with("Unittest_Switch_Created", "Switch")
diff --git a/tests/core/state_machine_rule.py b/tests/core/state_machine_rule.py
index ce07713..6927ba6 100644
--- a/tests/core/state_machine_rule.py
+++ b/tests/core/state_machine_rule.py
@@ -1,4 +1,5 @@
"""Unit-test for state_machine."""
+
import collections
import time
import unittest
@@ -12,71 +13,64 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestStateMachineRule(tests.helper.test_case_base.TestCaseBase):
- """Tests for StateMachineRule."""
-
- 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.StringItem, "Unittest_State", None)
- self.state_item = HABApp.openhab.items.StringItem.get_item("Unittest_State")
-
- self.item_exists_mock.return_value = False
-
- with unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.string_item.StringItem("rules_common_state_machine_rule_StateMachineRule_state", "")):
- self._state_machine = habapp_rules.core.state_machine_rule.StateMachineRule(self.state_item)
-
- def test_get_initial_state(self):
- """Test getting of initial state."""
- TestCase = collections.namedtuple("TestCase", "item_value, state_names, default, expected_result")
- test_cases = [
- TestCase("state1", ["state1", "state2"], "default", "state1"),
- TestCase("wrong_state", ["state1", "state2"], "default", "default"),
- TestCase("state1", ["new_state1", "new_state_2"], "default", "default"),
- TestCase("state1", [], "default", "default")
- ]
-
- with unittest.mock.patch.object(self._state_machine, "_item_state") as state_item_mock:
- for test_case in test_cases:
- state_item_mock.value = test_case.item_value
- self._state_machine.states = [{"name": name} for name in test_case.state_names]
- self.assertEqual(self._state_machine._get_initial_state(test_case.default), test_case.expected_result)
-
- def test_update_openhab_state(self):
- """Test if OpenHAB state will be updated."""
- self._state_machine.state = "some_state"
- with unittest.mock.patch.object(self._state_machine, "_item_state") as state_item:
- self._state_machine._update_openhab_state()
- state_item.oh_send_command.assert_called_once_with("some_state")
-
- def test_on_rule_removed(self):
- """Test on_rule_removed."""
- # check if 'on_rule_removed' is still available in HABApp
- getattr(HABApp.rule.Rule, "on_rule_removed")
-
- # check if timer is stopped correctly
- states = [
- {"name": "stopped"},
- {"name": "running", "timeout": 99, "on_timeout": "trigger_stop"}
- ]
-
- with unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.string_item.StringItem("rules_common_state_machine_rule_StateMachineRule_state", "")):
- for initial_state in ["stopped", "running"]:
- state_machine_rule = habapp_rules.core.state_machine_rule.StateMachineRule(self.state_item)
-
- state_machine_rule.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(
- model=state_machine_rule,
- states=states,
- ignore_invalid_triggers=True)
-
- state_machine_rule._set_state(initial_state)
-
- if initial_state == "running":
- self.assertTrue(list(state_machine_rule.state_machine.states["running"].runner.values())[0].is_alive())
-
- state_machine_rule.on_rule_removed()
-
- if initial_state == "running":
- time.sleep(0.001)
- self.assertFalse(list(state_machine_rule.state_machine.states["running"].runner.values())[0].is_alive())
+ """Tests for StateMachineRule."""
+
+ 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.StringItem, "Unittest_State", None)
+ self.state_item = HABApp.openhab.items.StringItem.get_item("Unittest_State")
+
+ self.item_exists_mock.return_value = False
+
+ with unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.string_item.StringItem("rules_common_state_machine_rule_StateMachineRule_state", "")):
+ self._state_machine = habapp_rules.core.state_machine_rule.StateMachineRule(self.state_item)
+
+ def test_get_initial_state(self) -> None:
+ """Test getting of initial state."""
+ TestCase = collections.namedtuple("TestCase", "item_value, state_names, default, expected_result")
+ test_cases = [
+ TestCase("state1", ["state1", "state2"], "default", "state1"),
+ TestCase("wrong_state", ["state1", "state2"], "default", "default"),
+ TestCase("state1", ["new_state1", "new_state_2"], "default", "default"),
+ TestCase("state1", [], "default", "default"),
+ ]
+
+ with unittest.mock.patch.object(self._state_machine, "_item_state") as state_item_mock:
+ for test_case in test_cases:
+ state_item_mock.value = test_case.item_value
+ self._state_machine.states = [{"name": name} for name in test_case.state_names]
+ self.assertEqual(self._state_machine._get_initial_state(test_case.default), test_case.expected_result)
+
+ def test_update_openhab_state(self) -> None:
+ """Test if OpenHAB state will be updated."""
+ self._state_machine.state = "some_state"
+ with unittest.mock.patch.object(self._state_machine, "_item_state") as state_item:
+ self._state_machine._update_openhab_state()
+ state_item.oh_send_command.assert_called_once_with("some_state")
+
+ def test_on_rule_removed(self) -> None:
+ """Test on_rule_removed."""
+ # check if 'on_rule_removed' is still available in HABApp
+ self.assertIsNotNone(HABApp.rule.Rule.on_rule_removed)
+
+ # check if timer is stopped correctly
+ states = [{"name": "stopped"}, {"name": "running", "timeout": 99, "on_timeout": "trigger_stop"}]
+
+ with unittest.mock.patch("habapp_rules.core.helper.create_additional_item", return_value=HABApp.openhab.items.string_item.StringItem("rules_common_state_machine_rule_StateMachineRule_state", "")):
+ for initial_state in ["stopped", "running"]:
+ state_machine_rule = habapp_rules.core.state_machine_rule.StateMachineRule(self.state_item)
+
+ state_machine_rule.state_machine = habapp_rules.core.state_machine_rule.StateMachineWithTimeout(model=state_machine_rule, states=states, ignore_invalid_triggers=True)
+
+ state_machine_rule._set_state(initial_state)
+
+ if initial_state == "running":
+ self.assertTrue(next(iter(state_machine_rule.state_machine.states["running"].runner.values())).is_alive())
+
+ state_machine_rule.on_rule_removed()
+
+ if initial_state == "running":
+ time.sleep(0.001)
+ self.assertFalse(next(iter(state_machine_rule.state_machine.states["running"].runner.values())).is_alive())
diff --git a/tests/core/timeout_list.py b/tests/core/timeout_list.py
index cbf8fda..8e3f888 100644
--- a/tests/core/timeout_list.py
+++ b/tests/core/timeout_list.py
@@ -1,4 +1,5 @@
"""Unittest for TimeoutList."""
+
import unittest
import unittest.mock
@@ -6,127 +7,138 @@
class TestTimeoutList(unittest.TestCase):
- """Tests for TimeoutList."""
-
- def setUp(self) -> None:
- """Setup Tests."""
- self.normal_list = []
- self.timeout_list = habapp_rules.core.timeout_list.TimeoutList()
-
- def test_repr(self):
- """Test repr of TimeoutList."""
- self.assertEqual(str(self.normal_list), str(self.timeout_list))
- for add_value in [None, "some_string", 14, 42.2]:
- self.normal_list.append(add_value)
- self.timeout_list.append(add_value, 100)
- self.assertEqual(str(self.normal_list), str(self.timeout_list))
-
- def test_bool(self):
- """Test bool of TimeoutList."""
- with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_mock:
- self.assertFalse(bool(self.timeout_list))
- remove_mock.assert_called_once()
-
- remove_mock.reset_mock()
- self.timeout_list.append(42, 10)
- self.assertTrue(bool(self.timeout_list))
- remove_mock.assert_called_once()
-
- def test_contains(self):
- """Test contains of TimeoutList."""
- with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_mock:
- # empty list
- self.assertFalse(42 in self.timeout_list)
- self.assertFalse("test" in self.timeout_list)
- self.assertEqual(2, remove_mock.call_count)
-
- # add 42
- remove_mock.reset_mock()
- self.timeout_list.append(42, 10)
- self.assertTrue(42 in self.timeout_list)
- self.assertFalse("test" in self.timeout_list)
- self.assertEqual(2, remove_mock.call_count)
-
- # add test string
- remove_mock.reset_mock()
- self.timeout_list.append("test", 10)
- self.assertTrue(42 in self.timeout_list)
- self.assertTrue("test" in self.timeout_list)
- self.assertEqual(2, remove_mock.call_count)
-
- def test_get_item(self):
- """Test getting item from TimeoutList."""
- # empty list
- with self.assertRaises(IndexError):
- self.assertIsNone(self.timeout_list[0])
-
- with self.assertRaises(IndexError):
- self.assertIsNone(self.timeout_list[1])
-
- # list with two elements
- self.timeout_list.append("test", 10)
- self.timeout_list.append(42, 10)
-
- self.assertEqual("test", self.timeout_list[0])
- self.assertEqual(42, self.timeout_list[1])
-
- with self.assertRaises(IndexError):
- self.assertIsNone(self.timeout_list[2])
-
- def test_equal(self):
- """Test equal of TimeoutList."""
- with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_old_mock:
- self.assertEqual(self.timeout_list, habapp_rules.core.timeout_list.TimeoutList())
- self.assertEqual(1, remove_old_mock.call_count)
-
- self.assertEqual(self.timeout_list, [])
- self.assertEqual(2, remove_old_mock.call_count)
-
- self.timeout_list.append(42, 10)
- self.assertEqual(self.timeout_list, [42])
- self.assertEqual(3, remove_old_mock.call_count)
-
- self.assertNotEqual(self.timeout_list, "")
- self.assertEqual(3, remove_old_mock.call_count)
-
- def test_not_equal(self):
- """Test not equal of TimeoutList."""
- self.assertFalse(self.timeout_list != [])
-
- self.timeout_list.append(42, 10)
- self.assertTrue(self.timeout_list != [])
- self.assertTrue(self.timeout_list != [80])
- self.assertFalse(self.timeout_list != [42])
-
- def test_remove(self):
- """Test remove of TimeoutList."""
- with self.assertRaises(ValueError) as context:
- self.timeout_list.remove(42)
- self.assertEqual("TimeoutList.remove(x): x not in list", str(context.exception))
-
- self.timeout_list.append(42, 10)
- self.timeout_list.append("test", 10)
- self.assertIsNone(self.timeout_list.remove(42))
-
- self.assertEqual(self.timeout_list, ["test"])
-
- def test_pop(self):
- """Test pop of TimeoutList."""
- with self.assertRaises(IndexError):
- self.timeout_list.pop(0)
-
- self.timeout_list.append(42, 10)
-
- with self.assertRaises(IndexError):
- self.timeout_list.pop(1)
-
- with self.assertRaises(TypeError):
- self.timeout_list.pop("first")
-
- self.assertEqual(42, self.timeout_list.pop(0))
- self.assertEqual([], self.timeout_list)
-
- self.timeout_list.append(42, 10)
- self.timeout_list.append("test", 10)
- self.assertEqual("test", self.timeout_list.pop(1))
- self.assertEqual([42], self.timeout_list)
+ """Tests for TimeoutList."""
+
+ def setUp(self) -> None:
+ """Setup Tests."""
+ self.normal_list = []
+ self.timeout_list = habapp_rules.core.timeout_list.TimeoutList()
+
+ def test_repr(self) -> None:
+ """Test repr of TimeoutList."""
+ self.assertEqual(str(self.normal_list), str(self.timeout_list))
+ for add_value in [None, "some_string", 14, 42.2]:
+ self.normal_list.append(add_value)
+ self.timeout_list.append(add_value, 100)
+ self.assertEqual(str(self.normal_list), str(self.timeout_list))
+
+ def test_bool(self) -> None:
+ """Test bool of TimeoutList."""
+ with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_mock:
+ self.assertFalse(bool(self.timeout_list))
+ remove_mock.assert_called_once()
+
+ remove_mock.reset_mock()
+ self.timeout_list.append(42, 10)
+ self.assertTrue(bool(self.timeout_list))
+ remove_mock.assert_called_once()
+
+ def test_contains(self) -> None:
+ """Test contains of TimeoutList."""
+ with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_mock:
+ # empty list
+ self.assertFalse(42 in self.timeout_list)
+ self.assertFalse("test" in self.timeout_list)
+ self.assertEqual(2, remove_mock.call_count)
+
+ # add 42
+ remove_mock.reset_mock()
+ self.timeout_list.append(42, 10)
+ self.assertTrue(42 in self.timeout_list)
+ self.assertFalse("test" in self.timeout_list)
+ self.assertEqual(2, remove_mock.call_count)
+
+ # add test string
+ remove_mock.reset_mock()
+ self.timeout_list.append("test", 10)
+ self.assertTrue(42 in self.timeout_list)
+ self.assertTrue("test" in self.timeout_list)
+ self.assertEqual(2, remove_mock.call_count)
+
+ def test_get_item(self) -> None:
+ """Test getting item from TimeoutList."""
+ # empty list
+ with self.assertRaises(IndexError):
+ self.assertIsNone(self.timeout_list[0])
+
+ with self.assertRaises(IndexError):
+ self.assertIsNone(self.timeout_list[1])
+
+ # list with two elements
+ self.timeout_list.append("test", 10)
+ self.timeout_list.append(42, 10)
+
+ self.assertEqual("test", self.timeout_list[0])
+ self.assertEqual(42, self.timeout_list[1])
+
+ with self.assertRaises(IndexError):
+ self.assertIsNone(self.timeout_list[2])
+
+ def test_equal(self) -> None:
+ """Test equal of TimeoutList."""
+ with unittest.mock.patch.object(self.timeout_list, "_TimeoutList__remove_old_items") as remove_old_mock:
+ self.assertEqual(self.timeout_list, habapp_rules.core.timeout_list.TimeoutList())
+ self.assertEqual(1, remove_old_mock.call_count)
+
+ self.assertEqual(self.timeout_list, [])
+ self.assertEqual(2, remove_old_mock.call_count)
+
+ self.timeout_list.append(42, 10)
+ self.assertEqual(self.timeout_list, [42])
+ self.assertEqual(3, remove_old_mock.call_count)
+
+ self.assertNotEqual(self.timeout_list, "")
+ self.assertEqual(3, remove_old_mock.call_count)
+
+ def test_not_equal(self) -> None:
+ """Test not equal of TimeoutList."""
+ self.assertFalse(self.timeout_list != [])
+
+ self.timeout_list.append(42, 10)
+ self.assertTrue(self.timeout_list != [])
+ self.assertTrue(self.timeout_list != [80])
+ self.assertFalse(self.timeout_list != [42])
+
+ def test_remove(self) -> None:
+ """Test remove of TimeoutList."""
+ with self.assertRaises(ValueError) as context:
+ self.timeout_list.remove(42)
+ self.assertEqual("TimeoutList.remove(x): x not in list", str(context.exception))
+
+ self.timeout_list.append(42, 10)
+ self.timeout_list.append("test", 10)
+ self.assertIsNone(self.timeout_list.remove(42))
+
+ self.assertEqual(self.timeout_list, ["test"])
+
+ def test_pop(self) -> None:
+ """Test pop of TimeoutList."""
+ with self.assertRaises(IndexError):
+ self.timeout_list.pop(0)
+
+ self.timeout_list.append(42, 10)
+
+ with self.assertRaises(IndexError):
+ self.timeout_list.pop(1)
+
+ with self.assertRaises(TypeError):
+ self.timeout_list.pop("first")
+
+ self.assertEqual(42, self.timeout_list.pop(0))
+ self.assertEqual([], self.timeout_list)
+
+ self.timeout_list.append(42, 10)
+ self.timeout_list.append("test", 10)
+ self.assertEqual("test", self.timeout_list.pop(1))
+ self.assertEqual([42], self.timeout_list)
+
+ def test_hash(self) -> None:
+ """Test hash method."""
+ self.assertEqual(5740354900026072187, hash(self.timeout_list))
+ self.assertEqual(hash(()), hash(self.timeout_list))
+
+ self.timeout_list.append(2, 2)
+ self.assertEqual(hash((2,)), hash(self.timeout_list))
+
+ self.timeout_list.append(42, 100)
+ self.assertEqual(hash((2, 42)), hash(self.timeout_list))
diff --git a/tests/core/type_of_day.py b/tests/core/type_of_day.py
index 6464d29..20bf1fb 100644
--- a/tests/core/type_of_day.py
+++ b/tests/core/type_of_day.py
@@ -1,72 +1,69 @@
"""Test type of day functions."""
+
import collections
import datetime
import unittest
import unittest.mock
import habapp_rules.core.type_of_day
+from habapp_rules import TIMEZONE
class TestTypeOfDay(unittest.TestCase):
- """Test all type of day functions"""
-
- def test_is_weekend(self):
- """Test is_weekend"""
- TestCase = collections.namedtuple("TestCase", "day, offset, result")
-
- test_cases = [
- # Monday
- TestCase(datetime.datetime(2023, 12, 18), -1, True),
- TestCase(datetime.datetime(2023, 12, 18), 0, False),
- TestCase(datetime.datetime(2023, 12, 18), 1, False),
- TestCase(datetime.datetime(2023, 12, 18), 2, False),
-
- # Friday
- TestCase(datetime.datetime(2023, 12, 22), -1, False),
- TestCase(datetime.datetime(2023, 12, 22), 0, False),
- TestCase(datetime.datetime(2023, 12, 22), 1, True),
- TestCase(datetime.datetime(2023, 12, 22), 2, True),
-
- # Saturday
- TestCase(datetime.datetime(2023, 12, 23), -1, False),
- TestCase(datetime.datetime(2023, 12, 23), 0, True),
- TestCase(datetime.datetime(2023, 12, 23), 1, True),
- TestCase(datetime.datetime(2023, 12, 23), 2, False),
-
- # Sunday
- TestCase(datetime.datetime(2023, 12, 24), -1, True),
- TestCase(datetime.datetime(2023, 12, 24), 0, True),
- TestCase(datetime.datetime(2023, 12, 24), 1, False),
- TestCase(datetime.datetime(2023, 12, 24), 2, False),
- ]
+ """Test all type of day functions."""
- with unittest.mock.patch("datetime.datetime") as datetime_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- datetime_mock.today.return_value = test_case.day
- self.assertEqual(test_case.result, habapp_rules.core.type_of_day.is_weekend(test_case.offset))
+ def test_is_weekend(self) -> None:
+ """Test is_weekend."""
+ TestCase = collections.namedtuple("TestCase", "day, offset, result")
- def test_is_holiday(self):
- """Test is_holiday"""
- TestCase = collections.namedtuple("TestCase", "day, offset, result")
+ test_cases = [
+ # Monday
+ TestCase(datetime.datetime(2023, 12, 18, tzinfo=TIMEZONE), -1, True),
+ TestCase(datetime.datetime(2023, 12, 18, tzinfo=TIMEZONE), 0, False),
+ TestCase(datetime.datetime(2023, 12, 18, tzinfo=TIMEZONE), 1, False),
+ TestCase(datetime.datetime(2023, 12, 18, tzinfo=TIMEZONE), 2, False),
+ # Friday
+ TestCase(datetime.datetime(2023, 12, 22, tzinfo=TIMEZONE), -1, False),
+ TestCase(datetime.datetime(2023, 12, 22, tzinfo=TIMEZONE), 0, False),
+ TestCase(datetime.datetime(2023, 12, 22, tzinfo=TIMEZONE), 1, True),
+ TestCase(datetime.datetime(2023, 12, 22, tzinfo=TIMEZONE), 2, True),
+ # Saturday
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), -1, False),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 0, True),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 1, True),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 2, False),
+ # Sunday
+ TestCase(datetime.datetime(2023, 12, 24, tzinfo=TIMEZONE), -1, True),
+ TestCase(datetime.datetime(2023, 12, 24, tzinfo=TIMEZONE), 0, True),
+ TestCase(datetime.datetime(2023, 12, 24, tzinfo=TIMEZONE), 1, False),
+ TestCase(datetime.datetime(2023, 12, 24, tzinfo=TIMEZONE), 2, False),
+ ]
- test_cases = [
- # Holy evening
- TestCase(datetime.datetime(2023, 12, 23), -1, False),
- TestCase(datetime.datetime(2023, 12, 23), 0, False),
- TestCase(datetime.datetime(2023, 12, 23), 1, False),
- TestCase(datetime.datetime(2023, 12, 23), 2, True),
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ datetime_mock.now.return_value = test_case.day
+ self.assertEqual(test_case.result, habapp_rules.core.type_of_day.is_weekend(test_case.offset))
- # Christmas
- TestCase(datetime.datetime(2023, 12, 25), -1, False),
- TestCase(datetime.datetime(2023, 12, 25), 0, True),
- TestCase(datetime.datetime(2023, 12, 25), 1, True),
- TestCase(datetime.datetime(2023, 12, 25), 2, False),
+ def test_is_holiday(self) -> None:
+ """Test is_holiday."""
+ TestCase = collections.namedtuple("TestCase", "day, offset, result")
- ]
+ test_cases = [
+ # Holy evening
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), -1, False),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 0, False),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 1, False),
+ TestCase(datetime.datetime(2023, 12, 23, tzinfo=TIMEZONE), 2, True),
+ # Christmas
+ TestCase(datetime.datetime(2023, 12, 25, tzinfo=TIMEZONE), -1, False),
+ TestCase(datetime.datetime(2023, 12, 25, tzinfo=TIMEZONE), 0, True),
+ TestCase(datetime.datetime(2023, 12, 25, tzinfo=TIMEZONE), 1, True),
+ TestCase(datetime.datetime(2023, 12, 25, tzinfo=TIMEZONE), 2, False),
+ ]
- with unittest.mock.patch("datetime.datetime") as datetime_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- datetime_mock.today.return_value = test_case.day
- self.assertEqual(test_case.result, habapp_rules.core.type_of_day.is_holiday(test_case.offset))
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ datetime_mock.now.return_value = test_case.day
+ self.assertEqual(test_case.result, habapp_rules.core.type_of_day.is_holiday(test_case.offset))
diff --git a/tests/core/version.py b/tests/core/version.py
index a105e5a..6eb2553 100644
--- a/tests/core/version.py
+++ b/tests/core/version.py
@@ -1,25 +1,26 @@
"""Test version."""
+
import HABApp.openhab.items
-import habapp_rules.__version__
+import habapp_rules
import habapp_rules.core.version
import tests.helper.oh_item
import tests.helper.test_case_base
class TestSetVersions(tests.helper.test_case_base.TestCaseBase):
- """Test for SetVersions."""
+ """Test for SetVersions."""
- def setUp(self) -> None:
- """Set up test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
+ def setUp(self) -> None:
+ """Set up test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_habapp_version", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_habapp_rules_version", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_habapp_version", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_habapp_rules_version", None)
- habapp_rules.core.version.SetVersions()
+ habapp_rules.core.version.SetVersions()
- def test_version_values(self):
- """test if versions were set correctly."""
- tests.helper.oh_item.assert_value("H_habapp_version", HABApp.__version__)
- tests.helper.oh_item.assert_value("H_habapp_rules_version", habapp_rules.__version__.__version__)
+ def test_version_values(self) -> None:
+ """Test if versions were set correctly."""
+ tests.helper.oh_item.assert_value("H_habapp_version", HABApp.__version__)
+ tests.helper.oh_item.assert_value("H_habapp_rules_version", habapp_rules.__version__)
diff --git a/tests/energy/config/monthly_report.py b/tests/energy/config/monthly_report.py
index c3cf699..48ec552 100644
--- a/tests/energy/config/monthly_report.py
+++ b/tests/energy/config/monthly_report.py
@@ -1,4 +1,5 @@
"""Test config models for monthly energy report rules."""
+
import HABApp
import pydantic
@@ -8,36 +9,36 @@
class TestEnergyShare(tests.helper.test_case_base.TestCaseBase):
- """Test EnergyShare dataclass."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Number_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Switch_1", None)
-
- def test_init(self):
- """Test init."""
- # valid init
- energy_share = habapp_rules.energy.config.monthly_report.EnergyShare("Number_1", "First Number")
- self.assertEqual("Number_1", energy_share.energy_item.name)
- self.assertEqual("First Number", energy_share.chart_name)
- self.assertEqual(0, energy_share.monthly_power)
-
- expected_item = HABApp.openhab.items.NumberItem("Number_1")
- self.assertEqual(expected_item, energy_share.energy_item)
-
- # valid init with item
- energy_share = habapp_rules.energy.config.monthly_report.EnergyShare(expected_item, "First Number")
- self.assertEqual("Number_1", energy_share.energy_item.name)
- self.assertEqual("First Number", energy_share.chart_name)
- self.assertEqual(0, energy_share.monthly_power)
-
- # invalid init (Item not found)
- with self.assertRaises(pydantic.ValidationError):
- habapp_rules.energy.config.monthly_report.EnergyShare("Number_2", "Second Number")
-
- # invalid init (Item is not a number)
- with self.assertRaises(pydantic.ValidationError):
- habapp_rules.energy.config.monthly_report.EnergyShare("Switch_1", "Second Number")
+ """Test EnergyShare dataclass."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Number_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Switch_1", None)
+
+ def test_init(self) -> None:
+ """Test init."""
+ # valid init
+ energy_share = habapp_rules.energy.config.monthly_report.EnergyShare("Number_1", "First Number")
+ self.assertEqual("Number_1", energy_share.energy_item.name)
+ self.assertEqual("First Number", energy_share.chart_name)
+ self.assertEqual(0, energy_share.monthly_power)
+
+ expected_item = HABApp.openhab.items.NumberItem("Number_1")
+ self.assertEqual(expected_item, energy_share.energy_item)
+
+ # valid init with item
+ energy_share = habapp_rules.energy.config.monthly_report.EnergyShare(expected_item, "First Number")
+ self.assertEqual("Number_1", energy_share.energy_item.name)
+ self.assertEqual("First Number", energy_share.chart_name)
+ self.assertEqual(0, energy_share.monthly_power)
+
+ # invalid init (Item not found)
+ with self.assertRaises(pydantic.ValidationError):
+ habapp_rules.energy.config.monthly_report.EnergyShare("Number_2", "Second Number")
+
+ # invalid init (Item is not a number)
+ with self.assertRaises(pydantic.ValidationError):
+ habapp_rules.energy.config.monthly_report.EnergyShare("Switch_1", "Second Number")
diff --git a/tests/energy/donut_chart.py b/tests/energy/donut_chart.py
index c5a9303..c076bcb 100644
--- a/tests/energy/donut_chart.py
+++ b/tests/energy/donut_chart.py
@@ -1,43 +1,43 @@
"""Tests for donut chart."""
+
import unittest
import unittest.mock
import habapp_rules.energy.donut_chart
-# pylint: disable=protected-access
class TestDonutFunctions(unittest.TestCase):
- """Test all donut plot functions."""
+ """Test all donut plot functions."""
- def test_auto_percent_format(self):
- """Test _auto_percent_format."""
- values = [100, 20, 80]
- percent_values = [val / sum(values) * 100 for val in values]
+ def test_auto_percent_format(self) -> None:
+ """Test _auto_percent_format."""
+ values = [100, 20, 80]
+ percent_values = [val / sum(values) * 100 for val in values]
- label_function = habapp_rules.energy.donut_chart._auto_percent_format(values)
+ label_function = habapp_rules.energy.donut_chart._auto_percent_format(values)
- for idx, percent_value in enumerate(percent_values):
- self.assertEqual(f"{values[idx]:.1f} kWh", label_function(percent_value))
+ for idx, percent_value in enumerate(percent_values):
+ self.assertEqual(f"{values[idx]:.1f} kWh", label_function(percent_value))
- def test_create_chart(self):
- """Test create_chart."""
- labels = ["one", "two", "three"]
- values = [1, 2, 3.0]
- path = unittest.mock.MagicMock()
+ def test_create_chart(self) -> None:
+ """Test create_chart."""
+ labels = ["one", "two", "three"]
+ values = [1, 2, 3.0]
+ path = unittest.mock.MagicMock()
- with unittest.mock.patch("matplotlib.pyplot") as pyplot_mock:
- ax_mock = unittest.mock.MagicMock()
- pyplot_mock.subplots.return_value = None, ax_mock
- text_mock_1 = unittest.mock.MagicMock()
- text_mock_2 = unittest.mock.MagicMock()
- ax_mock.pie.return_value = None, [text_mock_1, text_mock_2], None
+ with unittest.mock.patch("habapp_rules.energy.donut_chart.plt") as pyplot_mock:
+ ax_mock = unittest.mock.MagicMock()
+ pyplot_mock.subplots.return_value = None, ax_mock
+ text_mock_1 = unittest.mock.MagicMock()
+ text_mock_2 = unittest.mock.MagicMock()
+ ax_mock.pie.return_value = None, [text_mock_1, text_mock_2], None
- habapp_rules.energy.donut_chart.create_chart(labels, values, path)
+ habapp_rules.energy.donut_chart.create_chart(labels, values, path)
- pyplot_mock.subplots.assert_called_once()
- ax_mock.pie.assert_called_once_with(values, labels=labels, autopct=unittest.mock.ANY, pctdistance=0.7, textprops={"fontsize": 10})
+ pyplot_mock.subplots.assert_called_once()
+ ax_mock.pie.assert_called_once_with(values, labels=labels, autopct=unittest.mock.ANY, pctdistance=0.7, textprops={"fontsize": 10})
- text_mock_1.set_backgroundcolor.assert_called_once_with("white")
- text_mock_2.set_backgroundcolor.assert_called_once_with("white")
+ text_mock_1.set_backgroundcolor.assert_called_once_with("white")
+ text_mock_2.set_backgroundcolor.assert_called_once_with("white")
- pyplot_mock.savefig.assert_called_once_with(str(path), bbox_inches="tight", transparent=True)
+ pyplot_mock.savefig.assert_called_once_with(str(path), bbox_inches="tight", transparent=True)
diff --git a/tests/energy/monthly_report.py b/tests/energy/monthly_report.py
index 0e0b758..4f2d894 100644
--- a/tests/energy/monthly_report.py
+++ b/tests/energy/monthly_report.py
@@ -1,4 +1,5 @@
"""Tests for monthly energy report."""
+
import collections
import datetime
import unittest
@@ -8,7 +9,7 @@
import HABApp.openhab.items
import multi_notifier.connectors.connector_mail
-import habapp_rules.__version__
+import habapp_rules
import habapp_rules.core.exceptions
import habapp_rules.energy.config.monthly_report
import habapp_rules.energy.monthly_report
@@ -16,183 +17,143 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestFunctions(unittest.TestCase):
- """Test all global functions."""
-
- def test_get_last_month_name(self):
- """Test _get_last_month_name."""
- TestCase = collections.namedtuple("TestCase", ["month_number", "expected_name"])
-
- test_cases = [
- TestCase(1, "Dezember"),
- TestCase(2, "Januar"),
- TestCase(3, "Februar"),
- TestCase(4, "März"),
- TestCase(5, "April"),
- TestCase(6, "Mai"),
- TestCase(7, "Juni"),
- TestCase(8, "Juli"),
- TestCase(9, "August"),
- TestCase(10, "September"),
- TestCase(11, "Oktober"),
- TestCase(12, "November"),
- ]
-
- today = datetime.datetime.today()
- with unittest.mock.patch("datetime.date") as mock_date:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- mock_date.today.return_value = today.replace(month=test_case.month_number, day=1)
- self.assertEqual(test_case.expected_name, habapp_rules.energy.monthly_report._get_previous_month_name())
-
- def test_get_next_trigger(self):
- """Test _get_next_trigger."""
- TestCase = collections.namedtuple("TestCase", ["current_datetime", "expected_trigger"])
-
- test_cases = [
- TestCase(datetime.datetime(2022, 1, 1, 0, 0, 0), datetime.datetime(2022, 2, 1, 0, 0, 0)),
- TestCase(datetime.datetime(2023, 12, 17, 2, 42, 55), datetime.datetime(2024, 1, 1, 0, 0, 0)),
- TestCase(datetime.datetime(2024, 2, 29, 23, 59, 59), datetime.datetime(2024, 3, 1, 0, 0, 0))
- ]
-
- with unittest.mock.patch("datetime.datetime") as mock_datetime:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- mock_datetime.now.return_value = test_case.current_datetime
- self.assertEqual(test_case.expected_trigger, habapp_rules.energy.monthly_report._get_next_trigger())
+ """Test all global functions."""
+
+ def test_get_last_month_name(self) -> None:
+ """Test _get_last_month_name."""
+ TestCase = collections.namedtuple("TestCase", ["month_number", "expected_name"])
+
+ test_cases = [
+ TestCase(1, "Dezember"),
+ TestCase(2, "Januar"),
+ TestCase(3, "Februar"),
+ TestCase(4, "März"),
+ TestCase(5, "April"),
+ TestCase(6, "Mai"),
+ TestCase(7, "Juni"),
+ TestCase(8, "Juli"),
+ TestCase(9, "August"),
+ TestCase(10, "September"),
+ TestCase(11, "Oktober"),
+ TestCase(12, "November"),
+ ]
+
+ today = datetime.datetime.today()
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ datetime_mock.now.return_value = today.replace(month=test_case.month_number, day=1)
+ self.assertEqual(test_case.expected_name, habapp_rules.energy.monthly_report._get_previous_month_name())
class TestMonthlyReport(tests.helper.test_case_base.TestCaseBase):
- """Test MonthlyReport rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_Sum", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_2", None)
-
- self._energy_1 = habapp_rules.energy.config.monthly_report.EnergyShare("Energy_1", "Energy 1")
- self._energy_2 = habapp_rules.energy.config.monthly_report.EnergyShare("Energy_2", "Energy 2")
- self._mail_config = multi_notifier.connectors.connector_mail.MailConfig(user="User", password="Password", smtp_host="smtp.test.de", smtp_port=587)
-
- config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
- items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(
- energy_sum="Energy_Sum"
- ),
- parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
- known_energy_shares=[self._energy_1, self._energy_2],
- config_mail=self._mail_config,
- recipients=["test@test.de"]
- )
- )
-
- self._rule = habapp_rules.energy.monthly_report.MonthlyReport(config)
-
- def test_init(self):
- """Test init."""
- TestCase = collections.namedtuple("TestCase", ["sum_in_group", "item_1_in_group", "item_2_in_group", "raises_exception"])
-
- test_cases = [
- TestCase(True, True, True, False),
- TestCase(True, True, False, True),
- TestCase(True, False, True, True),
- TestCase(True, False, False, True),
- TestCase(False, True, True, True),
- TestCase(False, True, False, True),
- TestCase(False, False, True, True),
- TestCase(False, False, False, True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self._energy_1.energy_item.groups = {"PersistenceGroup"} if test_case.item_1_in_group else set()
- self._energy_2.energy_item.groups = {"PersistenceGroup"} if test_case.item_2_in_group else set()
- self._rule._config.items.energy_sum.groups = {"PersistenceGroup"} if test_case.sum_in_group else set()
-
- config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
- items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(
- energy_sum="Energy_Sum"
- ),
- parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
- known_energy_shares=[self._energy_1, self._energy_2],
- config_mail=self._mail_config,
- recipients=["test@test.de"],
- persistence_group_name="PersistenceGroup"
- )
- )
-
- if test_case.raises_exception:
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.energy.monthly_report.MonthlyReport(config)
- else:
- habapp_rules.energy.monthly_report.MonthlyReport(config)
-
- def test_init_with_debug_mode(self):
- """Test init with debug mode."""
- config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
- items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(
- energy_sum="Energy_Sum"
- ),
- parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
- known_energy_shares=[self._energy_1, self._energy_2],
- config_mail=self._mail_config,
- recipients=["test@test.de"],
- debug=True
- )
- )
-
- self._rule = habapp_rules.energy.monthly_report.MonthlyReport(config)
-
- def test_get_historic_value(self):
- """Test _get_historic_value."""
- mock_item = unittest.mock.MagicMock()
- fake_persistence_data = HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData()
- mock_item.get_persistence_data.return_value = fake_persistence_data
-
- start_time = datetime.datetime.now()
- end_time = start_time + datetime.timedelta(hours=1)
-
- # no data
- self.assertEqual(0, self._rule._get_historic_value(mock_item, start_time))
- mock_item.get_persistence_data.assert_called_once_with(start_time=start_time, end_time=end_time)
-
- # data
- fake_persistence_data.data = {"0.0": 42, "1.0": 1337}
- self.assertEqual(42, self._rule._get_historic_value(mock_item, start_time))
-
- def test_create_html(self):
- """Test create_html."""
- self._rule._config.items.energy_sum.value = 20_123.5489135
-
- template_mock = unittest.mock.MagicMock()
- with (unittest.mock.patch("jinja2.Template", return_value=template_mock),
- unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName")):
- self._rule._create_html(10_042.123456)
-
- template_mock.render.assert_called_once_with(
- month="MonthName",
- energy_now="20123.5",
- energy_last_month="10042.1",
- habapp_version=habapp_rules.__version__.__version__,
- chart="{{ chart }}"
- )
-
- def test_cb_send_energy(self):
- """Test cb_send_energy."""
- self._rule._config.items.energy_sum.value = 1000
- self._energy_1.energy_item.value = 100
- self._energy_2.energy_item.value = 50
-
- with (unittest.mock.patch.object(self._rule, "_get_historic_value", side_effect=[800, 90, 45]),
- unittest.mock.patch("habapp_rules.energy.donut_chart.create_chart", return_value="html text result") as create_chart_mock,
- unittest.mock.patch.object(self._rule, "_create_html") as create_html_mock,
- unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName"),
- unittest.mock.patch.object(self._rule, "_mail") as mail_mock):
- self._rule._cb_send_energy()
-
- create_chart_mock.assert_called_once_with(["Energy 1", "Energy 2", "Rest"], [10, 5, 185], unittest.mock.ANY)
- create_html_mock.assert_called_once_with(200)
- mail_mock.send_message("test@test.de", "html text result", "Stromverbrauch MonthName", images={"chart": unittest.mock.ANY})
+ """Test MonthlyReport rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_Sum", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_2", None)
+
+ self._energy_1 = habapp_rules.energy.config.monthly_report.EnergyShare("Energy_1", "Energy 1")
+ self._energy_2 = habapp_rules.energy.config.monthly_report.EnergyShare("Energy_2", "Energy 2")
+ self._mail_config = multi_notifier.connectors.connector_mail.MailConfig(user="User", password="Password", smtp_host="smtp.test.de", smtp_port=587) # noqa: S106
+
+ config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
+ items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(energy_sum="Energy_Sum"),
+ parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(known_energy_shares=[self._energy_1, self._energy_2], config_mail=self._mail_config, recipients=["test@test.de"]),
+ )
+
+ self._rule = habapp_rules.energy.monthly_report.MonthlyReport(config)
+
+ def test_init(self) -> None:
+ """Test init."""
+ TestCase = collections.namedtuple("TestCase", ["sum_in_group", "item_1_in_group", "item_2_in_group", "raises_exception"])
+
+ test_cases = [
+ TestCase(True, True, True, False),
+ TestCase(True, True, False, True),
+ TestCase(True, False, True, True),
+ TestCase(True, False, False, True),
+ TestCase(False, True, True, True),
+ TestCase(False, True, False, True),
+ TestCase(False, False, True, True),
+ TestCase(False, False, False, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self._energy_1.energy_item.groups = {"PersistenceGroup"} if test_case.item_1_in_group else set()
+ self._energy_2.energy_item.groups = {"PersistenceGroup"} if test_case.item_2_in_group else set()
+ self._rule._config.items.energy_sum.groups = {"PersistenceGroup"} if test_case.sum_in_group else set()
+
+ config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
+ items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(energy_sum="Energy_Sum"),
+ parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(
+ known_energy_shares=[self._energy_1, self._energy_2], config_mail=self._mail_config, recipients=["test@test.de"], persistence_group_name="PersistenceGroup"
+ ),
+ )
+
+ if test_case.raises_exception:
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.energy.monthly_report.MonthlyReport(config)
+ else:
+ habapp_rules.energy.monthly_report.MonthlyReport(config)
+
+ def test_init_with_debug_mode(self) -> None:
+ """Test init with debug mode."""
+ config = habapp_rules.energy.config.monthly_report.MonthlyReportConfig(
+ items=habapp_rules.energy.config.monthly_report.MonthlyReportItems(energy_sum="Energy_Sum"),
+ parameter=habapp_rules.energy.config.monthly_report.MonthlyReportParameter(known_energy_shares=[self._energy_1, self._energy_2], config_mail=self._mail_config, recipients=["test@test.de"], debug=True),
+ )
+
+ self._rule = habapp_rules.energy.monthly_report.MonthlyReport(config)
+
+ def test_get_historic_value(self) -> None:
+ """Test _get_historic_value."""
+ mock_item = unittest.mock.MagicMock()
+ fake_persistence_data = HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData()
+ mock_item.get_persistence_data.return_value = fake_persistence_data
+
+ start_time = datetime.datetime.now()
+ end_time = start_time + datetime.timedelta(hours=1)
+
+ # no data
+ self.assertEqual(0, self._rule._get_historic_value(mock_item, start_time))
+ mock_item.get_persistence_data.assert_called_once_with(start_time=start_time, end_time=end_time)
+
+ # data
+ fake_persistence_data.data = {"0.0": 42, "1.0": 1337}
+ self.assertEqual(42, self._rule._get_historic_value(mock_item, start_time))
+
+ def test_create_html(self) -> None:
+ """Test create_html."""
+ self._rule._config.items.energy_sum.value = 20_123.5489135
+
+ template_mock = unittest.mock.MagicMock()
+ with unittest.mock.patch("jinja2.Template", return_value=template_mock), unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName"):
+ self._rule._create_html(10_042.123456)
+
+ template_mock.render.assert_called_once_with(month="MonthName", energy_now="20123.5", energy_last_month="10042.1", habapp_version=habapp_rules.__version__, chart="{{ chart }}")
+
+ def test_cb_send_energy(self) -> None:
+ """Test cb_send_energy."""
+ self._rule._config.items.energy_sum.value = 1000
+ self._energy_1.energy_item.value = 100
+ self._energy_2.energy_item.value = 50
+
+ with (
+ unittest.mock.patch.object(self._rule, "_get_historic_value", side_effect=[800, 90, 45]),
+ unittest.mock.patch("habapp_rules.energy.donut_chart.create_chart", return_value="html text result") as create_chart_mock,
+ unittest.mock.patch.object(self._rule, "_create_html") as create_html_mock,
+ unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName"),
+ unittest.mock.patch.object(self._rule, "_mail") as mail_mock,
+ ):
+ self._rule._cb_send_energy()
+
+ create_chart_mock.assert_called_once_with(["Energy 1", "Energy 2", "Rest"], [10, 5, 185], unittest.mock.ANY)
+ create_html_mock.assert_called_once_with(200)
+ mail_mock.send_message("test@test.de", "html text result", "Stromverbrauch MonthName", images={"chart": unittest.mock.ANY})
diff --git a/tests/helper/graph_machines.py b/tests/helper/graph_machines.py
index e1c6252..2017506 100644
--- a/tests/helper/graph_machines.py
+++ b/tests/helper/graph_machines.py
@@ -1,4 +1,5 @@
"""Define graph machine classes."""
+
import os
from functools import partial
@@ -6,47 +7,49 @@
from transitions.extensions.diagrams_graphviz import Graph, _filter_states
try:
- import graphviz as pgv
+ import graphviz as pgv
except ImportError:
- pgv = None
+ pgv = None
os.environ["PATH"] += r"C:\Program Files\Graphviz\bin"
class FakeModel:
- """This class is used as fake model for graph creation"""
-
-
-# pylint: disable=protected-access, missing-type-doc
-def get_graph_with_previous_state(self, title=None, roi_state=None) -> object:
- """Monkey patch for transtitions.extentions.diagrams_graphviz.Graph.get_graph, which also adds all previous states.
-
- :param self: graph object
- :param title: title of graph
- :param roi_state: region of interest - state
- :return: graph
- """
- title = title if title else self.machine.title
-
- fsm_graph = pgv.Digraph(
- name=title,
- node_attr=self.machine.style_attributes["node"]["default"],
- edge_attr=self.machine.style_attributes["edge"]["default"],
- graph_attr=self.machine.style_attributes["graph"]["default"],
- )
- fsm_graph.graph_attr.update(**self.machine.machine_attributes)
- fsm_graph.graph_attr["label"] = title
- # For each state, draw a circle
- states, trans = self._get_elements()
- if roi_state:
- trans = [t for t in trans if t["source"] == roi_state or t["dest"] == roi_state or self.custom_styles["edge"][t["source"]][t["dest"]]]
- state_names = [t for trans in trans for t in [trans["source"], trans.get("dest", trans["source"])]]
- state_names += [k for k, style in self.custom_styles["node"].items() if style]
- states = _filter_states(states, state_names, self.machine.state_cls)
- self._add_nodes(states, fsm_graph)
- self._add_edges(trans, fsm_graph)
- setattr(fsm_graph, "draw", partial(self.draw, fsm_graph))
- return fsm_graph
+ """This class is used as fake model for graph creation."""
+
+
+def get_graph_with_previous_state(self: Graph, title: str | None = None, roi_state: str | None = None) -> object:
+ """Monkey patch for transtitions.extentions.diagrams_graphviz.Graph.get_graph, which also adds all previous states.
+
+ Args:
+ self: graph object
+ title: title of graph
+ roi_state: region of interest - state
+
+ Returns:
+ graph
+ """
+ title = title or self.machine.title
+
+ fsm_graph = pgv.Digraph(
+ name=title,
+ node_attr=self.machine.style_attributes["node"]["default"],
+ edge_attr=self.machine.style_attributes["edge"]["default"],
+ graph_attr=self.machine.style_attributes["graph"]["default"],
+ )
+ fsm_graph.graph_attr.update(**self.machine.machine_attributes)
+ fsm_graph.graph_attr["label"] = title
+ # For each state, draw a circle
+ states, trans = self._get_elements()
+ if roi_state:
+ trans = [t for t in trans if t["source"] == roi_state or t["dest"] == roi_state or self.custom_styles["edge"][t["source"]][t["dest"]]]
+ state_names = [t for trans in trans for t in [trans["source"], trans.get("dest", trans["source"])]]
+ state_names += [k for k, style in self.custom_styles["node"].items() if style]
+ states = _filter_states(states, state_names, self.machine.state_cls)
+ self._add_nodes(states, fsm_graph)
+ self._add_edges(trans, fsm_graph)
+ fsm_graph.draw = partial(self.draw, fsm_graph)
+ return fsm_graph
# monkey patching of Graph method
@@ -55,9 +58,9 @@ def get_graph_with_previous_state(self, title=None, roi_state=None) -> object:
@transitions.extensions.states.add_state_features(transitions.extensions.states.Timeout)
class GraphMachineTimer(transitions.extensions.GraphMachine):
- """GraphMachine with Timer."""
+ """GraphMachine with Timer."""
@transitions.extensions.states.add_state_features(transitions.extensions.states.Timeout)
class HierarchicalGraphMachineTimer(transitions.extensions.HierarchicalGraphMachine):
- """HierarchicalGraphMachine with Timer."""
+ """HierarchicalGraphMachine with Timer."""
diff --git a/tests/helper/oh_item.py b/tests/helper/oh_item.py
index 5977704..7bdff5a 100644
--- a/tests/helper/oh_item.py
+++ b/tests/helper/oh_item.py
@@ -1,126 +1,130 @@
"""Helper for OpenHAB items."""
+
from __future__ import annotations
+import contextlib
import datetime
-import typing
import HABApp.core
import HABApp.openhab.events
NO_VALUE = object()
_MOCKED_ITEM_NAMES = []
-StateTypes = typing.Union[str, int, float, datetime.datetime]
+StateTypes = str | float | datetime.datetime
-def add_mock_item(item_type: typing.Type[HABApp.openhab.items.OpenhabItem], name: str, initial_value: typing.Union[str, int, float] = None) -> None:
- """Add a mock item.
+def add_mock_item(item_type: type[HABApp.openhab.items.OpenhabItem], name: str, initial_value: str | float | None = None) -> None:
+ """Add a mock item.
- :param item_type: Type of the mock item
- :param name: Name of the mock item
- :param initial_value: initial value
- """
- if HABApp.core.Items.item_exists(name):
- HABApp.core.Items.pop_item(name)
- item = item_type(name, initial_value)
- HABApp.core.Items.add_item(item)
- _MOCKED_ITEM_NAMES.append(name)
+ Args:
+ item_type: Type of the mock item
+ name: Name of the mock item
+ initial_value: initial value
+ """
+ if HABApp.core.Items.item_exists(name):
+ HABApp.core.Items.pop_item(name)
+ item = item_type(name, initial_value)
+ HABApp.core.Items.add_item(item)
+ _MOCKED_ITEM_NAMES.append(name)
def remove_mocked_item_by_name(name: str) -> None:
- """Remove a mocked item by item name
+ """Remove a mocked item by item name.
- :param name: name of mocked item
- """
- HABApp.core.Items.pop_item(name) # pylint: disable=no-member
- _MOCKED_ITEM_NAMES.remove(name)
+ Args:
+ name: name of mocked item
+ """
+ HABApp.core.Items.pop_item(name)
+ _MOCKED_ITEM_NAMES.remove(name)
def remove_all_mocked_items() -> None:
- """Remove all mocked items."""
- for name in _MOCKED_ITEM_NAMES:
- HABApp.core.Items.pop_item(name) # pylint: disable=no-member
- _MOCKED_ITEM_NAMES.clear()
+ """Remove all mocked items."""
+ for name in _MOCKED_ITEM_NAMES:
+ HABApp.core.Items.pop_item(name)
+ _MOCKED_ITEM_NAMES.clear()
def set_state(item_name: str, value: StateTypes | None) -> None:
- """Helper to set state of item.
+ """Helper to set state of item.
- :param item_name: name of item
- :param value: state which should be set
- """
- item = HABApp.openhab.items.OpenhabItem.get_item(item_name)
- if isinstance(item, HABApp.openhab.items.DimmerItem) and value in {"ON", "OFF"}:
- if value == "ON":
- value = 100
- else:
- value = 0
+ Args:
+ item_name: name of item
+ value: state which should be set
+ """
+ item = HABApp.openhab.items.OpenhabItem.get_item(item_name)
+ if isinstance(item, HABApp.openhab.items.DimmerItem) and value in {"ON", "OFF"}:
+ value = 100 if value == "ON" else 0
- try:
- item.set_value(value)
- except AssertionError:
- print(f"Could not set '{value}' to '{item_name}'")
+ with contextlib.suppress(AssertionError):
+ item.set_value(value)
def send_command(item_name: str, new_value: StateTypes, old_value: StateTypes = NO_VALUE) -> None:
- """Replacement of send_command for unit-tests.
+ """Replacement of send_command for unit-tests.
- :param item_name: Name of item
- :param new_value: new value
- :param old_value: previous value
- """
- old_value = HABApp.openhab.items.OpenhabItem.get_item(item_name).value if old_value is NO_VALUE else old_value
+ Args:
+ item_name: Name of item
+ new_value: new value
+ old_value: previous value
+ """
+ old_value = HABApp.openhab.items.OpenhabItem.get_item(item_name).value if old_value is NO_VALUE else old_value
- set_state(item_name, new_value)
- if old_value is not NO_VALUE and old_value != new_value:
- HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateChangedEvent(item_name, new_value, old_value))
- HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateUpdatedEvent(item_name, new_value))
+ set_state(item_name, new_value)
+ if old_value is not NO_VALUE and old_value != new_value:
+ HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateChangedEvent(item_name, new_value, old_value))
+ HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateUpdatedEvent(item_name, new_value))
def item_command_event(item_name: str, value: StateTypes) -> None:
- """Post a command event to the event bus
+ """Post a command event to the event bus.
- :param item_name: name of item
- :param value: value of the event
- """
- try:
- set_state(item_name, value)
- except HABApp.core.errors.InvalidItemValue:
- pass # print(f"Could not set {value} to {item_name}")
- HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemCommandEvent(item_name, value))
+ Args:
+ item_name: name of item
+ value: value of the event
+ """
+ with contextlib.suppress(HABApp.core.errors.InvalidItemValue):
+ set_state(item_name, value)
+ HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemCommandEvent(item_name, value))
def item_state_event(item_name: str, value: StateTypes) -> None:
- """Post a state event to the event bus
+ """Post a state event to the event bus.
- :param item_name: name of item
- :param value: value of the event
- """
- set_state(item_name, value)
- HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateUpdatedEvent(item_name, value))
+ Args:
+ item_name: name of item
+ value: value of the event
+ """
+ set_state(item_name, value)
+ HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateUpdatedEvent(item_name, value))
def item_state_change_event(item_name: str, value: StateTypes, old_value: StateTypes = None) -> None:
- """Post a state change event to the event bus
-
- :param item_name: name of item
- :param value: value of the event
- :param old_value: previous value
- """
- prev_value = old_value if old_value else HABApp.openhab.items.OpenhabItem.get_item(item_name).value
- set_state(item_name, value)
- HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateChangedEvent(item_name, value, prev_value))
-
-
-def assert_value(item_name: str, value: StateTypes | None, message: str = None) -> None:
- """Helper to assert if item has correct state
-
- :param item_name: name of item
- :param value: expected state
- :param message: message to display if assertion failed
- :raises AssertionError: if value is wrong
- """
- if (current_state := HABApp.openhab.items.OpenhabItem.get_item(item_name).value) != value:
- msg = f"Wrong state of item '{item_name}'. Expected: {value} | Current: {current_state}"
- if message:
- msg += f"message = {message}"
- raise AssertionError(msg)
+ """Post a state change event to the event bus.
+
+ Args:
+ item_name: name of item
+ value: value of the event
+ old_value: previous value
+ """
+ prev_value = old_value or HABApp.openhab.items.OpenhabItem.get_item(item_name).value
+ set_state(item_name, value)
+ HABApp.core.EventBus.post_event(item_name, HABApp.openhab.events.ItemStateChangedEvent(item_name, value, prev_value))
+
+
+def assert_value(item_name: str, value: StateTypes | None, message: str | None = None) -> None:
+ """Helper to assert if item has correct state.
+
+ Args:
+ item_name: name of item
+ value: expected state
+ message: message to display if assertion failed
+
+ Raises:
+ AssertionError: if value is wrong
+ """
+ if (current_state := HABApp.openhab.items.OpenhabItem.get_item(item_name).value) != value:
+ msg = f"Wrong state of item '{item_name}'. Expected: {value} | Current: {current_state}"
+ if message:
+ msg += f"message = {message}"
+ raise AssertionError(msg)
diff --git a/tests/helper/rule_runner.py b/tests/helper/rule_runner.py
index 533ee26..82e3aae 100644
--- a/tests/helper/rule_runner.py
+++ b/tests/helper/rule_runner.py
@@ -1,12 +1,14 @@
-# pylint: skip-file
-from typing import List
+import asyncio
+from types import TracebackType
import HABApp
import HABApp.core.lib.exceptions.format
import HABApp.rule.rule as rule_module
-import HABApp.rule.scheduler.habappschedulerview as ha_sched
+import HABApp.rule.scheduler.job_builder as job_builder_module
+from astral import Observer
+from eascheduler.producers import prod_sun as prod_sun_module
from HABApp.core.asyncio import async_context
-from HABApp.core.internals import setup_internals, ItemRegistry, EventBus
+from HABApp.core.internals import EventBus, ItemRegistry, setup_internals
from HABApp.core.internals.proxy import ConstProxyObj
from HABApp.core.internals.wrapped_function import wrapped_thread, wrapper
from HABApp.core.internals.wrapped_function.wrapped_thread import WrappedThreadFunction
@@ -17,98 +19,114 @@
def suggest_rule_name(obj: object) -> str:
- return f'TestRule.{obj.__class__.__name__}'
+ return f"TestRule.{obj.__class__.__name__}"
class SyncScheduler:
- ALL = []
+ ALL = []
- def __init__(self):
- SyncScheduler.ALL.append(self)
- self.jobs = []
+ def __init__(self, event_loop=None, enabled=True) -> None:
+ SyncScheduler.ALL.append(self)
+ self.jobs = []
- def add_job(self, job):
- self.jobs.append(job)
+ def add_job(self, job) -> None:
+ self.jobs.append(job)
- def remove_job(self, job):
- self.jobs.remove(job)
+ def remove_job(self, job) -> None:
+ if job in self.jobs:
+ self.jobs.remove(job)
- def cancel_all(self):
- self.jobs.clear()
+ def update_job(self, job) -> None:
+ self.remove_job(job)
+ self.add_job(job)
+
+ def remove_all(self) -> None:
+ self.jobs.clear()
+
+ def set_enabled(self, enabled: bool) -> None:
+ pass
class DummyRuntime(Runtime):
- def __init__(self):
- pass
+ def __init__(self) -> None:
+ pass
-def raising_fallback_format(e: Exception, existing_traceback: List[str]) -> List[str]:
- traceback = fallback_format(e, existing_traceback)
- traceback = traceback
- raise
+def raising_fallback_format(e: Exception, existing_traceback: list[str]) -> list[str]:
+ traceback = fallback_format(e, existing_traceback)
+ traceback = traceback
+ raise
class SimpleRuleRunner:
- def __init__(self):
- self.loaded_rules = []
+ def __init__(self) -> None:
+ self.loaded_rules = []
+
+ self.monkeypatch = MonkeyPatch()
+ self.restore = []
+ self.ctx = asyncio.Future()
+
+ def submit(self, callback, *args, **kwargs) -> None:
+ # This executes the callback so we can not ignore exceptions
+ callback(*args, **kwargs)
- self.monkeypatch = MonkeyPatch()
- self.restore = []
+ def set_up(self) -> None:
+ # ensure that we call setup only once!
+ assert isinstance(HABApp.core.Items, ConstProxyObj)
+ assert isinstance(HABApp.core.EventBus, ConstProxyObj)
- def submit(self, callback, *args, **kwargs):
- # This executes the callback so we can not ignore exceptions
- callback(*args, **kwargs)
+ # prevent we're calling from asyncio - this works because we don't use threads
+ self.ctx = async_context.set("Rule Runner")
- def set_up(self):
- # ensure that we call setup only once!
- assert isinstance(HABApp.core.Items, ConstProxyObj)
- assert isinstance(HABApp.core.EventBus, ConstProxyObj)
+ ir = ItemRegistry()
+ eb = EventBus()
+ self.restore = setup_internals(ir, eb, final=False)
- ir = ItemRegistry()
- eb = EventBus()
- self.restore = setup_internals(ir, eb, final=False)
+ # Scheduler
+ self.monkeypatch.setattr(prod_sun_module, "OBSERVER", Observer(52.51870523376821, 13.376072914752532, 10))
- # Overwrite
- self.monkeypatch.setattr(HABApp.core, 'EventBus', eb)
- self.monkeypatch.setattr(HABApp.core, 'Items', ir)
+ # Overwrite
+ self.monkeypatch.setattr(HABApp.core, "EventBus", eb)
+ self.monkeypatch.setattr(HABApp.core, "Items", ir)
- # Patch the hook so we can instantiate the rules
- hook = HABAppRuleHook(self.loaded_rules.append, suggest_rule_name, DummyRuntime(), None)
- self.monkeypatch.setattr(rule_module, '_get_rule_hook', lambda: hook)
+ # Patch the hook so we can instantiate the rules
+ hook = HABAppRuleHook(self.loaded_rules.append, suggest_rule_name, DummyRuntime(), None)
+ self.monkeypatch.setattr(rule_module, "_get_rule_hook", lambda: hook)
- # patch worker with a synchronous worker
- self.monkeypatch.setattr(wrapped_thread, 'POOL', self)
- self.monkeypatch.setattr(wrapper, 'SYNC_CLS', WrappedThreadFunction, raising=False)
+ # patch worker with a synchronous worker
+ self.monkeypatch.setattr(wrapped_thread, "POOL", self)
+ self.monkeypatch.setattr(wrapper, "SYNC_CLS", WrappedThreadFunction, raising=False)
- # raise exceptions during error formatting
- self.monkeypatch.setattr(HABApp.core.lib.exceptions.format, 'fallback_format', raising_fallback_format)
+ # raise exceptions during error formatting
+ self.monkeypatch.setattr(HABApp.core.lib.exceptions.format, "fallback_format", raising_fallback_format)
- # patch scheduler, so we run synchronous
- self.monkeypatch.setattr(ha_sched, '_HABAppScheduler', SyncScheduler)
+ # patch scheduler, so we run synchronous
+ self.monkeypatch.setattr(job_builder_module, "AsyncHABAppScheduler", SyncScheduler)
- def tear_down(self):
- ctx = async_context.set('Tear down test')
+ def tear_down(self) -> None:
+ for rule in self.loaded_rules:
+ rule._habapp_ctx.unload_rule()
+ self.loaded_rules.clear()
- for rule in self.loaded_rules:
- rule._habapp_ctx.unload_rule()
- self.loaded_rules.clear()
+ # restore patched
+ self.monkeypatch.undo()
- # restore patched
- self.monkeypatch.undo()
- async_context.reset(ctx)
+ # restore async context
+ async_context.reset(self.ctx)
+ self.ctx = None
- for r in self.restore:
- r.restore()
+ for r in self.restore:
+ r.restore()
- def process_events(self):
- for s in SyncScheduler.ALL:
- for job in s.jobs:
- job._func.execute()
+ def process_events(self) -> None:
+ for s in SyncScheduler.ALL:
+ for job in s.jobs:
+ job.executor.execute()
- def __enter__(self):
- self.set_up()
+ def __enter__(self) -> None:
+ self.set_up()
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.tear_down()
- # do not supress exception
- return False
+ def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> bool:
+ self.tear_down()
+ # do not supress exception
+ return False
diff --git a/tests/helper/test_case_base.py b/tests/helper/test_case_base.py
index ee3c3db..ef75cb3 100644
--- a/tests/helper/test_case_base.py
+++ b/tests/helper/test_case_base.py
@@ -1,71 +1,77 @@
"""Common part for tests with simulated OpenHAB items."""
+
import threading
import unittest
import unittest.mock
+import habapp_rules.core.state_machine_rule
import tests.helper.oh_item
import tests.helper.rule_runner
-import habapp_rules.core.state_machine_rule
+
class TestCaseBase(unittest.TestCase):
- """Base class for tests with simulated OpenHAB items."""
+ """Base class for tests with simulated OpenHAB items."""
- def setUp(self) -> None:
- """Setup test case."""
- self.send_command_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.send_command", new=tests.helper.oh_item.send_command)
- self.addCleanup(self.send_command_mock_patcher.stop)
- self.send_command_mock = self.send_command_mock_patcher.start()
+ def setUp(self) -> None:
+ """Setup test case."""
+ self.send_command_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.send_command", new=tests.helper.oh_item.send_command)
+ self.addCleanup(self.send_command_mock_patcher.stop)
+ self.send_command_mock = self.send_command_mock_patcher.start()
- self.send_command_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.post_update", new=tests.helper.oh_item.set_state)
- self.addCleanup(self.send_command_mock_patcher.stop)
- self.send_command_mock = self.send_command_mock_patcher.start()
+ self.send_command_mock_patcher = unittest.mock.patch("HABApp.openhab.items.base_item.post_update", new=tests.helper.oh_item.set_state)
+ self.addCleanup(self.send_command_mock_patcher.stop)
+ self.send_command_mock = self.send_command_mock_patcher.start()
- self.item_exists_mock_patcher = unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True)
- self.addCleanup(self.item_exists_mock_patcher.stop)
- self.item_exists_mock = self.item_exists_mock_patcher.start()
+ self.item_exists_mock_patcher = unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True)
+ self.addCleanup(self.item_exists_mock_patcher.stop)
+ self.item_exists_mock = self.item_exists_mock_patcher.start()
- self._runner = tests.helper.rule_runner.SimpleRuleRunner()
- self._runner.set_up()
+ self._runner = tests.helper.rule_runner.SimpleRuleRunner()
+ self._runner.set_up()
- def tearDown(self) -> None:
- """Tear down test case."""
- tests.helper.oh_item.remove_all_mocked_items()
- self._runner.tear_down()
+ def tearDown(self) -> None:
+ """Tear down test case."""
+ tests.helper.oh_item.remove_all_mocked_items()
+ self._runner.tear_down()
class TestCaseBaseStateMachine(TestCaseBase):
- """Base class for tests with simulated OpenHAB items and state machines."""
-
- def setUp(self) -> None:
- TestCaseBase.setUp(self)
-
- self.transitions_timer_mock_patcher = unittest.mock.patch("transitions.extensions.states.Timer", spec=threading.Timer)
- self.addCleanup(self.transitions_timer_mock_patcher.stop)
- self.transitions_timer_mock = self.transitions_timer_mock_patcher.start()
-
- self.threading_timer_mock_patcher = unittest.mock.patch("threading.Timer", spec=threading.Timer)
- self.addCleanup(self.threading_timer_mock_patcher.stop)
- self.threading_timer_mock = self.threading_timer_mock_patcher.start()
-
- self.on_rule_removed_mock_patcher = unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed", spec=habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed)
- self.addCleanup(self.on_rule_removed_mock_patcher.stop)
- self.on_rule_removed_mock_patcher.start()
-
- def _get_state_names(self, states: dict, parent_state: str | None = None) -> list[str]: # pragma: no cover
- """Helper function to get all state names (also nested states)
-
- :param states: dict of all states or children states
- :param parent_state: name of parent state, only if it is a nested state machine
- :return: list of all state names
- """
- state_names = []
- prefix = f"{parent_state}_" if parent_state else ""
- if parent_state:
- states = states["children"]
-
- for state in states:
- if "children" in state:
- state_names += self._get_state_names(state, state["name"])
- else:
- state_names.append(f"{prefix}{state['name']}")
- return state_names
+ """Base class for tests with simulated OpenHAB items and state machines."""
+
+ def setUp(self) -> None:
+ """Setup tests."""
+ TestCaseBase.setUp(self)
+
+ self.transitions_timer_mock_patcher = unittest.mock.patch("transitions.extensions.states.Timer", spec=threading.Timer)
+ self.addCleanup(self.transitions_timer_mock_patcher.stop)
+ self.transitions_timer_mock = self.transitions_timer_mock_patcher.start()
+
+ self.threading_timer_mock_patcher = unittest.mock.patch("threading.Timer", spec=threading.Timer)
+ self.addCleanup(self.threading_timer_mock_patcher.stop)
+ self.threading_timer_mock = self.threading_timer_mock_patcher.start()
+
+ self.on_rule_removed_mock_patcher = unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed", spec=habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed)
+ self.addCleanup(self.on_rule_removed_mock_patcher.stop)
+ self.on_rule_removed_mock_patcher.start()
+
+ def _get_state_names(self, states: dict, parent_state: str | None = None) -> list[str]: # pragma: no cover
+ """Helper function to get all state names (also nested states).
+
+ Args:
+ states: dict of all states or children states
+ parent_state: name of parent state, only if it is a nested state machine
+
+ Returns:
+ list of all state names
+ """
+ state_names = []
+ prefix = f"{parent_state}_" if parent_state else ""
+ if parent_state:
+ states = states["children"]
+
+ for state in states:
+ if "children" in state:
+ state_names += self._get_state_names(state, state["name"])
+ else:
+ state_names.append(f"{prefix}{state['name']}")
+ return state_names
diff --git a/tests/helper/timer.py b/tests/helper/timer.py
index 1521ab1..af87b36 100644
--- a/tests/helper/timer.py
+++ b/tests/helper/timer.py
@@ -1,12 +1,14 @@
"""Helper for testing transitions."""
+
import unittest.mock
def call_timeout(mock_object: unittest.mock.MagicMock) -> None:
- """Helper to simulate timeout of timer.
+ """Helper to simulate timeout of timer.
- :param mock_object: mock object of transitions.extensions.states.Timer
- """
- timer_func = mock_object.call_args.args[1]
- timer_args = mock_object.call_args.kwargs.get("args", {})
- timer_func(*timer_args)
+ Args:
+ mock_object: mock object of transitions.extensions.states.Timer
+ """
+ timer_func = mock_object.call_args.args[1]
+ timer_args = mock_object.call_args.kwargs.get("args", {})
+ timer_func(*timer_args)
diff --git a/tests/run_unittest.py b/tests/run_unittest.py
index 33a488c..1ef3ded 100644
--- a/tests/run_unittest.py
+++ b/tests/run_unittest.py
@@ -1,4 +1,5 @@
"""Run all unit-tests."""
+
import logging
import pathlib
import sys
@@ -7,10 +8,10 @@
sys.path.append(str(pathlib.Path(__file__).resolve().parent.parent))
EXCLUDED_PY_FILES = ["run_unittest.py", "__init__.py", "rule_runner.py"]
-INPUT_MODULES = [f"tests.{'.'.join(f.parts)[:-3]}" for f in pathlib.Path(".").rglob("*.py") if f.name not in EXCLUDED_PY_FILES]
+INPUT_MODULES = [f"{'.'.join(f.parts)[:-3]}" for f in pathlib.Path("tests").rglob("*.py") if f.name not in EXCLUDED_PY_FILES]
logger_mock = unittest.mock.MagicMock()
logger_mock.level = logging.WARNING
with unittest.mock.patch("logging.getLogger", return_value=logger_mock):
- result = unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromNames(INPUT_MODULES))
+ result = unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromNames(INPUT_MODULES))
diff --git a/tests/sensors/DWD_WindAlarm/DWD_Wind_Alarm.png b/tests/sensors/_state_charts/DWD_WindAlarm/DWD_Wind_Alarm.png
similarity index 100%
rename from tests/sensors/DWD_WindAlarm/DWD_Wind_Alarm.png
rename to tests/sensors/_state_charts/DWD_WindAlarm/DWD_Wind_Alarm.png
diff --git a/tests/sensors/Humidity_States/Humidity.png b/tests/sensors/_state_charts/Humidity/Humidity.png
similarity index 100%
rename from tests/sensors/Humidity_States/Humidity.png
rename to tests/sensors/_state_charts/Humidity/Humidity.png
diff --git a/tests/sensors/Motion_States/Motion.png b/tests/sensors/_state_charts/Motion/Motion.png
similarity index 100%
rename from tests/sensors/Motion_States/Motion.png
rename to tests/sensors/_state_charts/Motion/Motion.png
diff --git a/tests/sensors/astro.py b/tests/sensors/astro.py
index 91c2b88..23c26d6 100644
--- a/tests/sensors/astro.py
+++ b/tests/sensors/astro.py
@@ -1,4 +1,5 @@
"""Test astro rules."""
+
import collections
import unittest
import unittest.mock
@@ -11,133 +12,129 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestSetDay(tests.helper.test_case_base.TestCaseBase):
- """Tests for TestSetDay."""
-
- def setUp(self):
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", None)
-
- def test_init(self):
- """Test init without elevation."""
- # default threshold
- config = habapp_rules.sensors.config.astro.SetDayConfig(
- items=habapp_rules.sensors.config.astro.SetDayItems(
- day="Unittest_Day",
- elevation="Unittest_Elevation",
- )
- )
-
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.soon") as run_soon_mock:
- rule = habapp_rules.sensors.astro.SetDay(config)
-
- run_soon_mock.assert_called_once_with(rule._set_night)
- self.assertEqual(0, rule._elevation_threshold)
-
- # custom threshold
- config.parameter = habapp_rules.sensors.config.astro.SetDayParameter(elevation_threshold=-2)
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.soon") as run_soon_mock:
- rule = habapp_rules.sensors.astro.SetDay(config)
-
- run_soon_mock.assert_called_once_with(rule._set_night)
- self.assertEqual(-2, rule._elevation_threshold)
-
- def test_init_with_elevation(self):
- """Test init without elevation."""
- TestCase = collections.namedtuple("TestCase", "elevation_value, night_state")
-
- test_cases = [
- TestCase(None, None),
- TestCase(-1, "OFF"),
- TestCase(0, "OFF"),
- TestCase(0.9, "OFF"),
- TestCase(1, "OFF"),
- TestCase(1.1, "ON"),
- TestCase(2, "ON"),
- TestCase(10, "ON"),
- ]
-
- config = habapp_rules.sensors.config.astro.SetDayConfig(
- items=habapp_rules.sensors.config.astro.SetDayItems(
- day="Unittest_Day",
- elevation="Unittest_Elevation",
- ),
- parameter=habapp_rules.sensors.config.astro.SetDayParameter(
- elevation_threshold=1)
- )
-
- habapp_rules.sensors.astro.SetDay(config)
-
- for test_case in test_cases:
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation_value)
- tests.helper.oh_item.assert_value("Unittest_Day", test_case.night_state)
-
-
-# pylint: disable=protected-access
+ """Tests for TestSetDay."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Day", None)
+
+ def test_init(self) -> None:
+ """Test init without elevation."""
+ # default threshold
+ config = habapp_rules.sensors.config.astro.SetDayConfig(
+ items=habapp_rules.sensors.config.astro.SetDayItems(
+ day="Unittest_Day",
+ elevation="Unittest_Elevation",
+ )
+ )
+
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.soon") as run_soon_mock:
+ rule = habapp_rules.sensors.astro.SetDay(config)
+
+ run_soon_mock.assert_called_once_with(rule._set_night)
+ self.assertEqual(0, rule._elevation_threshold)
+
+ # custom threshold
+ config.parameter = habapp_rules.sensors.config.astro.SetDayParameter(elevation_threshold=-2)
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.soon") as run_soon_mock:
+ rule = habapp_rules.sensors.astro.SetDay(config)
+
+ run_soon_mock.assert_called_once_with(rule._set_night)
+ self.assertEqual(-2, rule._elevation_threshold)
+
+ def test_init_with_elevation(self) -> None:
+ """Test init without elevation."""
+ TestCase = collections.namedtuple("TestCase", "elevation_value, night_state")
+
+ test_cases = [
+ TestCase(None, None),
+ TestCase(-1, "OFF"),
+ TestCase(0, "OFF"),
+ TestCase(0.9, "OFF"),
+ TestCase(1, "OFF"),
+ TestCase(1.1, "ON"),
+ TestCase(2, "ON"),
+ TestCase(10, "ON"),
+ ]
+
+ config = habapp_rules.sensors.config.astro.SetDayConfig(
+ items=habapp_rules.sensors.config.astro.SetDayItems(
+ day="Unittest_Day",
+ elevation="Unittest_Elevation",
+ ),
+ parameter=habapp_rules.sensors.config.astro.SetDayParameter(elevation_threshold=1),
+ )
+
+ habapp_rules.sensors.astro.SetDay(config)
+
+ for test_case in test_cases:
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation_value)
+ tests.helper.oh_item.assert_value("Unittest_Day", test_case.night_state)
+
+
class TestSetNight(tests.helper.test_case_base.TestCaseBase):
- """Tests for TestSetNight."""
-
- def setUp(self):
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", None)
-
- def test_init(self):
- """Test init without elevation."""
- # default threshold
- config = habapp_rules.sensors.config.astro.SetNightConfig(
- items=habapp_rules.sensors.config.astro.SetNightItems(
- night="Unittest_Night",
- elevation="Unittest_Elevation",
- )
- )
-
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.soon") as run_soon_mock:
- rule = habapp_rules.sensors.astro.SetNight(config)
-
- run_soon_mock.assert_called_once_with(rule._set_night)
- self.assertEqual(-8, rule._elevation_threshold)
-
- # custom threshold
- config.parameter = habapp_rules.sensors.config.astro.SetNightParameter(elevation_threshold=-10)
- with unittest.mock.patch("HABApp.rule.scheduler.habappschedulerview.HABAppSchedulerView.soon") as run_soon_mock:
- rule = habapp_rules.sensors.astro.SetNight(config)
-
- run_soon_mock.assert_called_once_with(rule._set_night)
- self.assertEqual(-10, rule._elevation_threshold)
-
- def test_init_with_elevation(self):
- """Test init without elevation."""
- TestCase = collections.namedtuple("TestCase", "elevation_value, night_state")
-
- test_cases = [
- TestCase(None, None),
- TestCase(-9, "ON"),
- TestCase(-8.1, "ON"),
- TestCase(-8, "OFF"),
- TestCase(-7.9, "OFF"),
- TestCase(-5, "OFF"),
- TestCase(0, "OFF"),
- TestCase(10, "OFF"),
- ]
-
- config = habapp_rules.sensors.config.astro.SetNightConfig(
- items=habapp_rules.sensors.config.astro.SetNightItems(
- night="Unittest_Night",
- elevation="Unittest_Elevation",
- ),
- parameter=habapp_rules.sensors.config.astro.SetNightParameter(
- elevation_threshold=-8)
- )
-
- habapp_rules.sensors.astro.SetNight(config)
-
- for test_case in test_cases:
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation_value)
- tests.helper.oh_item.assert_value("Unittest_Night", test_case.night_state)
+ """Tests for TestSetNight."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Night", None)
+
+ def test_init(self) -> None:
+ """Test init without elevation."""
+ # default threshold
+ config = habapp_rules.sensors.config.astro.SetNightConfig(
+ items=habapp_rules.sensors.config.astro.SetNightItems(
+ night="Unittest_Night",
+ elevation="Unittest_Elevation",
+ )
+ )
+
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.soon") as run_soon_mock:
+ rule = habapp_rules.sensors.astro.SetNight(config)
+
+ run_soon_mock.assert_called_once_with(rule._set_night)
+ self.assertEqual(-8, rule._elevation_threshold)
+
+ # custom threshold
+ config.parameter = habapp_rules.sensors.config.astro.SetNightParameter(elevation_threshold=-10)
+ with unittest.mock.patch("HABApp.rule.scheduler.job_builder.HABAppJobBuilder.soon") as run_soon_mock:
+ rule = habapp_rules.sensors.astro.SetNight(config)
+
+ run_soon_mock.assert_called_once_with(rule._set_night)
+ self.assertEqual(-10, rule._elevation_threshold)
+
+ def test_init_with_elevation(self) -> None:
+ """Test init without elevation."""
+ TestCase = collections.namedtuple("TestCase", "elevation_value, night_state")
+
+ test_cases = [
+ TestCase(None, None),
+ TestCase(-9, "ON"),
+ TestCase(-8.1, "ON"),
+ TestCase(-8, "OFF"),
+ TestCase(-7.9, "OFF"),
+ TestCase(-5, "OFF"),
+ TestCase(0, "OFF"),
+ TestCase(10, "OFF"),
+ ]
+
+ config = habapp_rules.sensors.config.astro.SetNightConfig(
+ items=habapp_rules.sensors.config.astro.SetNightItems(
+ night="Unittest_Night",
+ elevation="Unittest_Elevation",
+ ),
+ parameter=habapp_rules.sensors.config.astro.SetNightParameter(elevation_threshold=-8),
+ )
+
+ habapp_rules.sensors.astro.SetNight(config)
+
+ for test_case in test_cases:
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation_value)
+ tests.helper.oh_item.assert_value("Unittest_Night", test_case.night_state)
diff --git a/tests/sensors/config/dwd.py b/tests/sensors/config/dwd.py
index 6b264d2..32f1698 100644
--- a/tests/sensors/config/dwd.py
+++ b/tests/sensors/config/dwd.py
@@ -1,4 +1,5 @@
"""Test config models of dwd rules."""
+
import HABApp
import habapp_rules.core.exceptions
@@ -8,57 +9,30 @@
class TestWindAlarmConfig(tests.helper.test_case_base.TestCaseBase):
- """Test WindAlarmConfig."""
-
- def test_check_hand_timeout(self):
- """Test check_hand_timeout."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Hand_Timeout", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Wind_Alarm_state", None)
-
- # no timeout is given
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm",
- manual="Unittest_Manual",
- state="H_Unittest_Wind_Alarm_state"
- )
- )
-
- # only timeout item is given
- habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm",
- manual="Unittest_Manual",
- hand_timeout="Unittest_Hand_Timeout",
- state="H_Unittest_Wind_Alarm_state"
- )
- )
-
- # only timeout parameter is given
- habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm",
- manual="Unittest_Manual",
- state="H_Unittest_Wind_Alarm_state"
- ),
- parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
- hand_timeout=12 * 3600
- )
- )
-
- # timeout parameter and item are given
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm",
- manual="Unittest_Manual",
- hand_timeout="Unittest_Hand_Timeout",
- state="H_Unittest_Wind_Alarm_state"
- ),
- parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
- hand_timeout=12 * 3600
- )
- )
+ """Test WindAlarmConfig."""
+
+ def test_check_hand_timeout(self) -> None:
+ """Test check_hand_timeout."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Hand_Timeout", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Wind_Alarm_state", None)
+
+ # no timeout is given
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.sensors.config.dwd.WindAlarmConfig(items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm", manual="Unittest_Manual", state="H_Unittest_Wind_Alarm_state"))
+
+ # only timeout item is given
+ habapp_rules.sensors.config.dwd.WindAlarmConfig(items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm", manual="Unittest_Manual", hand_timeout="Unittest_Hand_Timeout", state="H_Unittest_Wind_Alarm_state"))
+
+ # only timeout parameter is given
+ habapp_rules.sensors.config.dwd.WindAlarmConfig(
+ items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm", manual="Unittest_Manual", state="H_Unittest_Wind_Alarm_state"), parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(hand_timeout=12 * 3600)
+ )
+
+ # timeout parameter and item are given
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.sensors.config.dwd.WindAlarmConfig(
+ items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm", manual="Unittest_Manual", hand_timeout="Unittest_Hand_Timeout", state="H_Unittest_Wind_Alarm_state"),
+ parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(hand_timeout=12 * 3600),
+ )
diff --git a/tests/sensors/config/sun.py b/tests/sensors/config/sun.py
index f68a1a9..ef37a1b 100644
--- a/tests/sensors/config/sun.py
+++ b/tests/sensors/config/sun.py
@@ -1,4 +1,5 @@
"""Test config models for sun rules."""
+
import unittest
import HABApp
@@ -11,127 +12,88 @@
class TestTemperatureDifferenceItems(tests.helper.test_case_base.TestCaseBase):
- """Test TemperatureDifferenceItems."""
-
- def test_validate_temperature_items(self):
- """Test validate_temperature_items."""
+ """Test TemperatureDifferenceItems."""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_3", None)
+ def test_validate_temperature_items(self) -> None:
+ """Test validate_temperature_items."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_3", None)
- # no item is given
- with self.assertRaises(pydantic.ValidationError):
- habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=[], output="Unittest_Output")
+ # no item is given
+ with self.assertRaises(pydantic.ValidationError):
+ habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=[], output="Unittest_Output")
- # single item is given
- with self.assertRaises(pydantic.ValidationError):
- habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1"], output="Unittest_Output")
+ # single item is given
+ with self.assertRaises(pydantic.ValidationError):
+ habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1"], output="Unittest_Output")
- # two items are given
- habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"], output="Unittest_Output")
+ # two items are given
+ habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"], output="Unittest_Output")
- # three items are given
- habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2", "Unittest_Temperature_3"], output="Unittest_Output")
+ # three items are given
+ habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2", "Unittest_Temperature_3"], output="Unittest_Output")
class TestConfigBase(tests.helper.test_case_base.TestCaseBase):
- """Test ConfigBase."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold", None)
-
- def test_validate_threshold(self):
- """Test validate_threshold."""
- # item NOT given | parameter NOT given
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output"
- )
- )
-
- # item NOT given | parameter given
- habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output"
- ),
- parameter=habapp_rules.sensors.config.sun.BrightnessParameter(
- threshold=42
- )
- )
-
- # item given | parameter NOT given
- habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output",
- threshold="Unittest_Threshold"
- )
- )
-
- # item given | parameter given
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
- habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output",
- threshold="Unittest_Threshold"
- ),
- parameter=habapp_rules.sensors.config.sun.BrightnessParameter(
- threshold=42
- )
- )
-
- def test_threshold_property(self):
- """Test threshold property."""
- # with parameter
- config = habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output"
- ),
- parameter=habapp_rules.sensors.config.sun.BrightnessParameter(
- threshold=42
- )
- )
- self.assertEqual(42, config.threshold)
-
- # with item | value is None
- config = habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output",
- threshold="Unittest_Threshold"
- )
- )
- self.assertEqual(float("inf"), config.threshold)
-
- # set value
- tests.helper.oh_item.set_state("Unittest_Threshold", 99)
- self.assertEqual(99, config.threshold)
+ """Test ConfigBase."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold", None)
+
+ def test_validate_threshold(self) -> None:
+ """Test validate_threshold."""
+ # item NOT given | parameter NOT given
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.sensors.config.sun.BrightnessConfig(items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output"))
+
+ # item NOT given | parameter given
+ habapp_rules.sensors.config.sun.BrightnessConfig(items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output"), parameter=habapp_rules.sensors.config.sun.BrightnessParameter(threshold=42))
+
+ # item given | parameter NOT given
+ habapp_rules.sensors.config.sun.BrightnessConfig(items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output", threshold="Unittest_Threshold"))
+
+ # item given | parameter given
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationError):
+ habapp_rules.sensors.config.sun.BrightnessConfig(
+ items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output", threshold="Unittest_Threshold"), parameter=habapp_rules.sensors.config.sun.BrightnessParameter(threshold=42)
+ )
+
+ def test_threshold_property(self) -> None:
+ """Test threshold property."""
+ # with parameter
+ config = habapp_rules.sensors.config.sun.BrightnessConfig(
+ items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output"), parameter=habapp_rules.sensors.config.sun.BrightnessParameter(threshold=42)
+ )
+ self.assertEqual(42, config.threshold)
+
+ # with item | value is None
+ config = habapp_rules.sensors.config.sun.BrightnessConfig(items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output", threshold="Unittest_Threshold"))
+ self.assertEqual(float("inf"), config.threshold)
+
+ # set value
+ tests.helper.oh_item.set_state("Unittest_Threshold", 99)
+ self.assertEqual(99, config.threshold)
class TestSunPositionWindow(unittest.TestCase):
- """Tests cases for testing the sun position filter."""
+ """Tests cases for testing the sun position filter."""
- def test_init(self):
- """Test __init__"""
- # normal init
- expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
- self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20))
+ def test_init(self) -> None:
+ """Test __init__."""
+ # normal init
+ expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
+ self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20))
- # init without elevation
- expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 0, 90)
- self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(10, 80))
+ # init without elevation
+ expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 0, 90)
+ self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(10, 80))
- # init with min > max
- expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
- self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(80, 10, 20, 2))
+ # init with min > max
+ expected_result = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
+ self.assertEqual(expected_result, habapp_rules.sensors.config.sun.SunPositionWindow(80, 10, 20, 2))
diff --git a/tests/sensors/current_switch.py b/tests/sensors/current_switch.py
index 20b9202..7eeb77f 100644
--- a/tests/sensors/current_switch.py
+++ b/tests/sensors/current_switch.py
@@ -1,13 +1,14 @@
"""Test power rules."""
+
import collections
import unittest.mock
import HABApp.rule.rule
-import eascheduler.jobs.job_countdown
+import HABApp.rule.scheduler.job_ctrl
-import habapp_rules.sensors.current_switch
import habapp_rules.core.exceptions
import habapp_rules.core.state_machine_rule
+import habapp_rules.sensors.current_switch
import habapp_rules.system
import tests.helper.graph_machines
import tests.helper.oh_item
@@ -16,97 +17,100 @@
from habapp_rules.sensors.config.current_switch import CurrentSwitchConfig, CurrentSwitchItems, CurrentSwitchParameter
-# pylint: disable=protected-access,no-member,too-many-public-methods
class TestCurrentSwitch(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing CurrentSwitch rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Current", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_extended", None)
-
- self._rule_1 = habapp_rules.sensors.current_switch.CurrentSwitch(CurrentSwitchConfig(
- items=CurrentSwitchItems(
- current="Unittest_Current",
- switch="Unittest_Switch_1",
- )))
-
- self._rule_2 = habapp_rules.sensors.current_switch.CurrentSwitch(CurrentSwitchConfig(
- items=CurrentSwitchItems(
- current="Unittest_Current",
- switch="Unittest_Switch_2",
- ),
- parameter=CurrentSwitchParameter(threshold=1),
- ))
-
- self._rule_extended = habapp_rules.sensors.current_switch.CurrentSwitch(CurrentSwitchConfig(
- items=CurrentSwitchItems(
- current="Unittest_Current",
- switch="Unittest_Switch_extended",
- ),
- parameter=CurrentSwitchParameter(extended_time=60)
- ))
-
- def test_init(self):
- """Test __init__."""
- tests.helper.oh_item.assert_value("Unittest_Switch_1", None)
- tests.helper.oh_item.assert_value("Unittest_Switch_2", None)
- tests.helper.oh_item.assert_value("Unittest_Switch_extended", None)
-
- self.assertIsNone(self._rule_1._extended_countdown)
- self.assertIsNone(self._rule_2._extended_countdown)
- self.assertIsInstance(self._rule_extended._extended_countdown, eascheduler.jobs.job_countdown.CountdownJob)
- self.assertIsNone(self._rule_extended._extended_countdown.remaining())
-
- def test_current_changed_without_extended_time(self):
- """Test current changed without extended time."""
- TestCase = collections.namedtuple("TestCase", "current, expected_1, expected_2")
-
- test_cases = [
- TestCase(0, "OFF", "OFF"),
- TestCase(0.2, "OFF", "OFF"),
- TestCase(0.201, "ON", "OFF"),
- TestCase(1, "ON", "OFF"),
- TestCase(1.001, "ON", "ON"),
- TestCase(1.001, "ON", "ON"),
-
- TestCase(1, "ON", "OFF"),
- TestCase(0.200, "OFF", "OFF"),
- TestCase(0, "OFF", "OFF"),
-
- TestCase(-10000, "OFF", "OFF"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.item_state_change_event("Unittest_Current", test_case.current)
-
- tests.helper.oh_item.assert_value("Unittest_Switch_1", test_case.expected_1)
- tests.helper.oh_item.assert_value("Unittest_Switch_2", test_case.expected_2)
-
- def test_current_changed_with_extended_time(self):
- """Test current changed with extended time."""
- with unittest.mock.patch.object(self._rule_extended, "_extended_countdown") as countdown_mock:
- # below threshold
- countdown_mock.remaining.return_value = None
- tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.1)
- tests.helper.oh_item.assert_value("Unittest_Switch_extended", None)
- countdown_mock.stop.assert_not_called()
- countdown_mock.reset.assert_not_called()
-
- # above threshold
- tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.3)
- tests.helper.oh_item.assert_value("Unittest_Switch_extended", "ON")
- countdown_mock.stop.assert_called_once()
- countdown_mock.reset.assert_not_called()
-
- # below threshold
- countdown_mock.stop.reset_mock()
- tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.1)
- tests.helper.oh_item.assert_value("Unittest_Switch_extended", "ON")
- countdown_mock.stop.assert_not_called()
- countdown_mock.reset.assert_called_once()
+ """Tests cases for testing CurrentSwitch rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Current", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_extended", None)
+
+ self._rule_1 = habapp_rules.sensors.current_switch.CurrentSwitch(
+ CurrentSwitchConfig(
+ items=CurrentSwitchItems(
+ current="Unittest_Current",
+ switch="Unittest_Switch_1",
+ )
+ )
+ )
+
+ self._rule_2 = habapp_rules.sensors.current_switch.CurrentSwitch(
+ CurrentSwitchConfig(
+ items=CurrentSwitchItems(
+ current="Unittest_Current",
+ switch="Unittest_Switch_2",
+ ),
+ parameter=CurrentSwitchParameter(threshold=1),
+ )
+ )
+
+ self._rule_extended = habapp_rules.sensors.current_switch.CurrentSwitch(
+ CurrentSwitchConfig(
+ items=CurrentSwitchItems(
+ current="Unittest_Current",
+ switch="Unittest_Switch_extended",
+ ),
+ parameter=CurrentSwitchParameter(extended_time=60),
+ )
+ )
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ tests.helper.oh_item.assert_value("Unittest_Switch_1", None)
+ tests.helper.oh_item.assert_value("Unittest_Switch_2", None)
+ tests.helper.oh_item.assert_value("Unittest_Switch_extended", None)
+
+ self.assertIsNone(self._rule_1._extended_countdown)
+ self.assertIsNone(self._rule_2._extended_countdown)
+ self.assertIsInstance(self._rule_extended._extended_countdown, HABApp.rule.scheduler.job_ctrl.CountdownJobControl)
+ self.assertIsNone(self._rule_extended._extended_countdown.next_run_datetime)
+
+ def test_current_changed_without_extended_time(self) -> None:
+ """Test current changed without extended time."""
+ TestCase = collections.namedtuple("TestCase", "current, expected_1, expected_2")
+
+ test_cases = [
+ TestCase(0, "OFF", "OFF"),
+ TestCase(0.2, "OFF", "OFF"),
+ TestCase(0.201, "ON", "OFF"),
+ TestCase(1, "ON", "OFF"),
+ TestCase(1.001, "ON", "ON"),
+ TestCase(1.001, "ON", "ON"),
+ TestCase(1, "ON", "OFF"),
+ TestCase(0.200, "OFF", "OFF"),
+ TestCase(0, "OFF", "OFF"),
+ TestCase(-10000, "OFF", "OFF"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", test_case.current)
+
+ tests.helper.oh_item.assert_value("Unittest_Switch_1", test_case.expected_1)
+ tests.helper.oh_item.assert_value("Unittest_Switch_2", test_case.expected_2)
+
+ def test_current_changed_with_extended_time(self) -> None:
+ """Test current changed with extended time."""
+ with unittest.mock.patch.object(self._rule_extended, "_extended_countdown") as countdown_mock:
+ # below threshold
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.1)
+ tests.helper.oh_item.assert_value("Unittest_Switch_extended", None)
+ countdown_mock.stop.assert_not_called()
+ countdown_mock.reset.assert_not_called()
+
+ # above threshold
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.3)
+ tests.helper.oh_item.assert_value("Unittest_Switch_extended", "ON")
+ countdown_mock.stop.assert_called_once()
+ countdown_mock.reset.assert_not_called()
+
+ # below threshold
+ countdown_mock.stop.reset_mock()
+ tests.helper.oh_item.item_state_change_event("Unittest_Current", 0.1)
+ tests.helper.oh_item.assert_value("Unittest_Switch_extended", "ON")
+ countdown_mock.stop.assert_not_called()
+ countdown_mock.reset.assert_called_once()
diff --git a/tests/sensors/dwd.py b/tests/sensors/dwd.py
index ca6bead..eead90c 100644
--- a/tests/sensors/dwd.py
+++ b/tests/sensors/dwd.py
@@ -1,7 +1,7 @@
"""Test DWD rules."""
+
import collections
import datetime
-import os
import pathlib
import sys
import unittest.mock
@@ -18,230 +18,214 @@
class TestDwdItems(tests.helper.test_case_base.TestCaseBase):
- """Tests for DwdItems."""
+ """Tests for DwdItems."""
- def setUp(self):
- tests.helper.test_case_base.TestCaseBase.setUp(self)
+ def setUp(self) -> None:
+ """Setup tests."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_description", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_type", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_severity", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_start_time", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_end_time", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_description", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_type", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_severity", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_start_time", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_end_time", None)
- self._test_dataclass = habapp_rules.sensors.dwd.DwdItems.from_prefix("I26_99_warning_1")
+ self._test_dataclass = habapp_rules.sensors.dwd.DwdItems.from_prefix("I26_99_warning_1")
- def test_severity_as_int(self):
- """Test severity_as_int."""
- TestCase = collections.namedtuple("TestCase", "str_value, expected_int")
+ def test_severity_as_int(self) -> None:
+ """Test severity_as_int."""
+ TestCase = collections.namedtuple("TestCase", "str_value, expected_int")
- test_cases = [
- TestCase("NULL", 0),
- TestCase("Minor", 1),
- TestCase("Moderate", 2),
- TestCase("Severe", 3),
- TestCase("Extreme", 4),
- TestCase("UNKNOWN", 0),
- TestCase(None, 0),
- ]
+ test_cases = [
+ TestCase("NULL", 0),
+ TestCase("Minor", 1),
+ TestCase("Moderate", 2),
+ TestCase("Severe", 3),
+ TestCase("Extreme", 4),
+ TestCase("UNKNOWN", 0),
+ TestCase(None, 0),
+ ]
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("I26_99_warning_1_severity", test_case.str_value)
- self.assertEqual(test_case.expected_int, self._test_dataclass.severity_as_int)
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("I26_99_warning_1_severity", test_case.str_value)
+ self.assertEqual(test_case.expected_int, self._test_dataclass.severity_as_int)
-# pylint: disable=protected-access
class TestDwdWindAlarm(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests for DwdWindAlarm."""
-
- def setUp(self):
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Wind_Alarm_1_state", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Wind_Alarm_2_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Hand_Timeout", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_description", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_type", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_severity", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_start_time", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_end_time", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_description", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_type", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_severity", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_2_start_time", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_2_end_time", None)
-
- config_1 = habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm_1",
- manual="Unittest_Manual_1",
- state="H_Unittest_Wind_Alarm_1_state"
- ),
- parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
- hand_timeout=12 * 3600,
- number_dwd_objects=2
- )
- )
-
- config_2 = habapp_rules.sensors.config.dwd.WindAlarmConfig(
- items=habapp_rules.sensors.config.dwd.WindAlarmItems(
- wind_alarm="Unittest_Wind_Alarm_2",
- manual="Unittest_Manual_2",
- hand_timeout="Unittest_Hand_Timeout",
- state="Unittest_Wind_Alarm_2_state"
- ),
- parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(
- number_dwd_objects=2
- )
- )
-
- self._wind_alarm_rule_1 = habapp_rules.sensors.dwd.DwdWindAlarm(config_1)
- self._wind_alarm_rule_2 = habapp_rules.sensors.dwd.DwdWindAlarm(config_2)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "DWD_WindAlarm"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self._wind_alarm_rule_1.states,
- transitions=self._wind_alarm_rule_1.trans,
- initial=self._wind_alarm_rule_1.state,
- show_conditions=True)
-
- graph.get_graph().draw(picture_dir / "DWD_Wind_Alarm.png", format="png", prog="dot")
-
- def test_set_timeouts(self):
- """Test _set_timeouts."""
- self.assertEqual(12 * 3600, self._wind_alarm_rule_1.state_machine.get_state("Hand").timeout)
- self.assertEqual(24 * 3600, self._wind_alarm_rule_2.state_machine.get_state("Hand").timeout)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Hand_Timeout", 2000)
- self.assertEqual(2000, self._wind_alarm_rule_2.state_machine.get_state("Hand").timeout)
-
- def test_get_initial_state(self):
- """Test _get_initial_state."""
- TestCase = collections.namedtuple("TestCase", "manual, wind_alarm_active, expected_state")
-
- test_cases = [
- TestCase("OFF", False, "Auto_Off"),
- TestCase("OFF", True, "Auto_On"),
- TestCase("ON", False, "Manual"),
- TestCase("ON", True, "Manual"),
- ]
-
- with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as wind_alarm_active_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Manual_1", test_case.manual)
- wind_alarm_active_mock.return_value = test_case.wind_alarm_active
-
- self.assertEqual(test_case.expected_state, self._wind_alarm_rule_1._get_initial_state())
-
- def test_manual(self):
- """Test manual"""
- # from Auto
- self.assertEqual("Auto_Off", self._wind_alarm_rule_1.state)
- self.assertEqual("Auto_Off", self._wind_alarm_rule_2.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "ON")
- self.assertEqual("Manual", self._wind_alarm_rule_1.state)
- self.assertEqual("Manual", self._wind_alarm_rule_2.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "OFF")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "OFF")
- self.assertEqual("Auto_Off", self._wind_alarm_rule_1.state)
- self.assertEqual("Auto_Off", self._wind_alarm_rule_2.state)
-
- # from Hand
- tests.helper.oh_item.item_state_change_event("Unittest_Wind_Alarm_1", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Wind_Alarm_2", "ON")
- self.assertEqual("Hand", self._wind_alarm_rule_1.state)
- self.assertEqual("Hand", self._wind_alarm_rule_2.state)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "ON")
- self.assertEqual("Manual", self._wind_alarm_rule_1.state)
- self.assertEqual("Manual", self._wind_alarm_rule_2.state)
-
- def test_wind_alarm_active(self):
- """Test _wind_alarm_active."""
- TestCase = collections.namedtuple("TestCase", "type_1, description_1, severity_1 start_time_1, end_time_1, type_2, description_2, severity_2 start_time_2, end_time_2, expected_result")
-
- now = datetime.datetime.now()
- start_active = now + datetime.timedelta(hours=-1)
- end_active = now + datetime.timedelta(hours=1)
- start_not_active = now + datetime.timedelta(hours=1)
- end_not_active = now + datetime.timedelta(hours=-2)
-
- test_cases = [
- # TestCase(None, None, None, None, None, None, None, None, None, None, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Minor", start_not_active, end_not_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Minor", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Moderate", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "Wind speed above 100 km/h", "Moderate", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 100 km/h", "Moderate", start_active, end_active, True),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed very high", "Moderate", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 100 km/h", "Minor", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 10 km/h", "Extreme", start_active, end_active, False),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed between 5 km/h and 100 km/h", "Moderate", start_active, end_active, True),
- TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed between 5 km/h and 100 km/h", "Moderate", start_not_active, end_not_active, False),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("I26_99_warning_1_description", test_case.description_1)
- tests.helper.oh_item.set_state("I26_99_warning_1_type", test_case.type_1)
- tests.helper.oh_item.set_state("I26_99_warning_1_severity", test_case.severity_1)
- tests.helper.oh_item.set_state("I26_99_warning_1_start_time", test_case.start_time_1)
- tests.helper.oh_item.set_state("I26_99_warning_1_end_time", test_case.end_time_1)
-
- tests.helper.oh_item.set_state("I26_99_warning_2_description", test_case.description_2)
- tests.helper.oh_item.set_state("I26_99_warning_2_type", test_case.type_2)
- tests.helper.oh_item.set_state("I26_99_warning_2_severity", test_case.severity_2)
- tests.helper.oh_item.set_state("I26_99_warning_2_start_time", test_case.start_time_2)
- tests.helper.oh_item.set_state("I26_99_warning_2_end_time", test_case.end_time_2)
-
- self.assertEqual(test_case.expected_result, self._wind_alarm_rule_1._wind_alarm_active())
-
- def test_cyclic_check(self):
- """Test _cyclic_check."""
- # Manual / Hand should not trigger any test
- with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as check_wind_alarm_mock:
- for state in ("Manual", "Hand"):
- self._wind_alarm_rule_1.state = state
- self._wind_alarm_rule_1._cb_cyclic_check()
- check_wind_alarm_mock.assert_not_called()
-
- # Auto will trigger check and send if needed
- TestCase = collections.namedtuple("TestCase", "initial_state, wind_alarm_active, expected_state")
-
- test_cases = [
- TestCase("Auto_Off", False, "Auto_Off"),
- TestCase("Auto_Off", True, "Auto_On"),
- TestCase("Auto_On", True, "Auto_On"),
- TestCase("Auto_On", False, "Auto_Off"),
- ]
-
- with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as check_wind_alarm_mock:
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- self._wind_alarm_rule_1.state = test_case.initial_state
- check_wind_alarm_mock.return_value = test_case.wind_alarm_active
-
- self._wind_alarm_rule_1._cb_cyclic_check()
-
- self.assertEqual(test_case.expected_state, self._wind_alarm_rule_1.state)
- tests.helper.oh_item.assert_value("Unittest_Wind_Alarm_1", "ON" if test_case.expected_state == "Auto_On" else "OFF")
+ """Tests for DwdWindAlarm."""
+
+ def setUp(self) -> None:
+ """Setup tests."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Wind_Alarm_1_state", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Wind_Alarm_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Manual_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Wind_Alarm_2_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Hand_Timeout", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_description", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_type", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_1_severity", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_start_time", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_1_end_time", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_description", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_type", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "I26_99_warning_2_severity", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_2_start_time", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.DatetimeItem, "I26_99_warning_2_end_time", None)
+
+ config_1 = habapp_rules.sensors.config.dwd.WindAlarmConfig(
+ items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm_1", manual="Unittest_Manual_1", state="H_Unittest_Wind_Alarm_1_state"),
+ parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(hand_timeout=12 * 3600, number_dwd_objects=2),
+ )
+
+ config_2 = habapp_rules.sensors.config.dwd.WindAlarmConfig(
+ items=habapp_rules.sensors.config.dwd.WindAlarmItems(wind_alarm="Unittest_Wind_Alarm_2", manual="Unittest_Manual_2", hand_timeout="Unittest_Hand_Timeout", state="Unittest_Wind_Alarm_2_state"),
+ parameter=habapp_rules.sensors.config.dwd.WindAlarmParameter(number_dwd_objects=2),
+ )
+
+ self._wind_alarm_rule_1 = habapp_rules.sensors.dwd.DwdWindAlarm(config_1)
+ self._wind_alarm_rule_2 = habapp_rules.sensors.dwd.DwdWindAlarm(config_2)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "DWD_WindAlarm"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
+ model=tests.helper.graph_machines.FakeModel(), states=self._wind_alarm_rule_1.states, transitions=self._wind_alarm_rule_1.trans, initial=self._wind_alarm_rule_1.state, show_conditions=True
+ )
+
+ graph.get_graph().draw(picture_dir / "DWD_Wind_Alarm.png", format="png", prog="dot")
+
+ def test_set_timeouts(self) -> None:
+ """Test _set_timeouts."""
+ self.assertEqual(12 * 3600, self._wind_alarm_rule_1.state_machine.get_state("Hand").timeout)
+ self.assertEqual(24 * 3600, self._wind_alarm_rule_2.state_machine.get_state("Hand").timeout)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Hand_Timeout", 2000)
+ self.assertEqual(2000, self._wind_alarm_rule_2.state_machine.get_state("Hand").timeout)
+
+ def test_get_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ TestCase = collections.namedtuple("TestCase", "manual, wind_alarm_active, expected_state")
+
+ test_cases = [
+ TestCase("OFF", False, "Auto_Off"),
+ TestCase("OFF", True, "Auto_On"),
+ TestCase("ON", False, "Manual"),
+ TestCase("ON", True, "Manual"),
+ ]
+
+ with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as wind_alarm_active_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Manual_1", test_case.manual)
+ wind_alarm_active_mock.return_value = test_case.wind_alarm_active
+
+ self.assertEqual(test_case.expected_state, self._wind_alarm_rule_1._get_initial_state())
+
+ def test_manual(self) -> None:
+ """Test manual."""
+ # from Auto
+ self.assertEqual("Auto_Off", self._wind_alarm_rule_1.state)
+ self.assertEqual("Auto_Off", self._wind_alarm_rule_2.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "ON")
+ self.assertEqual("Manual", self._wind_alarm_rule_1.state)
+ self.assertEqual("Manual", self._wind_alarm_rule_2.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "OFF")
+ self.assertEqual("Auto_Off", self._wind_alarm_rule_1.state)
+ self.assertEqual("Auto_Off", self._wind_alarm_rule_2.state)
+
+ # from Hand
+ tests.helper.oh_item.item_state_change_event("Unittest_Wind_Alarm_1", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Wind_Alarm_2", "ON")
+ self.assertEqual("Hand", self._wind_alarm_rule_1.state)
+ self.assertEqual("Hand", self._wind_alarm_rule_2.state)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_1", "ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Manual_2", "ON")
+ self.assertEqual("Manual", self._wind_alarm_rule_1.state)
+ self.assertEqual("Manual", self._wind_alarm_rule_2.state)
+
+ def test_wind_alarm_active(self) -> None:
+ """Test _wind_alarm_active."""
+ TestCase = collections.namedtuple("TestCase", "type_1, description_1, severity_1 start_time_1, end_time_1, type_2, description_2, severity_2 start_time_2, end_time_2, expected_result")
+
+ now = datetime.datetime.now()
+ start_active = now + datetime.timedelta(hours=-1)
+ end_active = now + datetime.timedelta(hours=1)
+ start_not_active = now + datetime.timedelta(hours=1)
+ end_not_active = now + datetime.timedelta(hours=-2)
+
+ test_cases = [
+ TestCase(None, None, None, None, None, None, None, None, None, None, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Minor", start_not_active, end_not_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Minor", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "SUN is appearing at 100 km/h", "Moderate", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "SUN", "Wind speed above 100 km/h", "Moderate", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 100 km/h", "Moderate", start_active, end_active, True),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed very high", "Moderate", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 100 km/h", "Minor", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed above 10 km/h", "Extreme", start_active, end_active, False),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed between 5 km/h and 100 km/h", "Moderate", start_active, end_active, True),
+ TestCase("FROST", "Frost is appearing at 100 km/h", "Minor", start_not_active, end_not_active, "WIND", "Wind speed between 5 km/h and 100 km/h", "Moderate", start_not_active, end_not_active, False),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("I26_99_warning_1_description", test_case.description_1)
+ tests.helper.oh_item.set_state("I26_99_warning_1_type", test_case.type_1)
+ tests.helper.oh_item.set_state("I26_99_warning_1_severity", test_case.severity_1)
+ tests.helper.oh_item.set_state("I26_99_warning_1_start_time", test_case.start_time_1)
+ tests.helper.oh_item.set_state("I26_99_warning_1_end_time", test_case.end_time_1)
+
+ tests.helper.oh_item.set_state("I26_99_warning_2_description", test_case.description_2)
+ tests.helper.oh_item.set_state("I26_99_warning_2_type", test_case.type_2)
+ tests.helper.oh_item.set_state("I26_99_warning_2_severity", test_case.severity_2)
+ tests.helper.oh_item.set_state("I26_99_warning_2_start_time", test_case.start_time_2)
+ tests.helper.oh_item.set_state("I26_99_warning_2_end_time", test_case.end_time_2)
+
+ self.assertEqual(test_case.expected_result, self._wind_alarm_rule_1._wind_alarm_active())
+
+ def test_cyclic_check(self) -> None:
+ """Test _cyclic_check."""
+ # Manual / Hand should not trigger any test
+ with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as check_wind_alarm_mock:
+ for state in ("Manual", "Hand"):
+ self._wind_alarm_rule_1.state = state
+ self._wind_alarm_rule_1._cb_cyclic_check()
+ check_wind_alarm_mock.assert_not_called()
+
+ # Auto will trigger check and send if needed
+ TestCase = collections.namedtuple("TestCase", "initial_state, wind_alarm_active, expected_state")
+
+ test_cases = [
+ TestCase("Auto_Off", False, "Auto_Off"),
+ TestCase("Auto_Off", True, "Auto_On"),
+ TestCase("Auto_On", True, "Auto_On"),
+ TestCase("Auto_On", False, "Auto_Off"),
+ ]
+
+ with unittest.mock.patch.object(self._wind_alarm_rule_1, "_wind_alarm_active") as check_wind_alarm_mock:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self._wind_alarm_rule_1.state = test_case.initial_state
+ check_wind_alarm_mock.return_value = test_case.wind_alarm_active
+
+ self._wind_alarm_rule_1._cb_cyclic_check()
+
+ self.assertEqual(test_case.expected_state, self._wind_alarm_rule_1.state)
+ tests.helper.oh_item.assert_value("Unittest_Wind_Alarm_1", "ON" if test_case.expected_state == "Auto_On" else "OFF")
diff --git a/tests/sensors/humidity.py b/tests/sensors/humidity.py
index eb32173..feabd3c 100644
--- a/tests/sensors/humidity.py
+++ b/tests/sensors/humidity.py
@@ -1,6 +1,6 @@
"""Tests for motion sensors."""
+
import collections
-import os
import pathlib
import sys
import unittest
@@ -19,160 +19,142 @@
import tests.helper.timer
-# pylint: disable=no-member, protected-access, too-many-public-methods
class TestMotion(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing motion sensors rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Humidity", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Output_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Custom_Name", None)
-
- config = habapp_rules.sensors.config.humidity.HumiditySwitchConfig(
- items=habapp_rules.sensors.config.humidity.HumiditySwitchItems(
- humidity="Unittest_Humidity",
- output="Unittest_Output",
- state="H_Unittest_Output_state"
- )
- )
-
- self.humidity = habapp_rules.sensors.humidity.HumiditySwitch(config)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Humidity_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- humidity_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.humidity.states,
- transitions=self.humidity.trans,
- initial=self.humidity.state,
- show_conditions=True)
-
- humidity_graph.get_graph().draw(picture_dir / "Humidity.png", format="png", prog="dot")
-
- def test_init(self):
- """Test init."""
- full_config = habapp_rules.sensors.config.humidity.HumiditySwitchConfig(
- items=habapp_rules.sensors.config.humidity.HumiditySwitchItems(
- humidity="Unittest_Humidity",
- output="Unittest_Output",
- state="Custom_Name"
- ),
- parameter=habapp_rules.sensors.config.humidity.HumiditySwitchParameter(
- absolute_threshold=70,
- extended_time=42
- )
- )
-
- humidity = habapp_rules.sensors.humidity.HumiditySwitch(full_config)
- self.assertEqual(70, humidity._config.parameter.absolute_threshold)
- self.assertEqual(42, humidity.state_machine.get_state("on_Extended").timeout)
- self.assertEqual("Custom_Name", humidity._item_state.name)
-
- def test_get_initial_state(self):
- """Test get_initial_state."""
- TestCase = collections.namedtuple("TestCase", "humidity_value, expected_state")
-
- test_cases = [
- TestCase(None, "off"),
- TestCase(64, "off"),
- TestCase(65, "on"),
- TestCase(66, "on"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Humidity", test_case.humidity_value)
- self.assertEqual(test_case.expected_state, self.humidity._get_initial_state())
-
- def test_check_high_humidity(self):
- """Test check_high_humidity."""
- TestCase = collections.namedtuple("TestCase", "item_value,given_value, expected_result")
-
- test_cases = [
- # False | False -> False
- TestCase(None, None, False),
- TestCase(None, 64, False),
- TestCase(64, None, False),
- TestCase(64, 64, False),
-
- # False | True -> True
- TestCase(None, 65, True),
- TestCase(64, 65, True),
-
- # True | False -> False
- TestCase(65, None, True),
- TestCase(65, 64, False),
-
- # True | True -> True
- TestCase(65, 65, True),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Humidity", test_case.item_value)
- self.assertEqual(test_case.expected_result, self.humidity._check_high_humidity(test_case.given_value))
-
- def test_cb_humidity(self):
- """Test _cb_humidity"""
- with (unittest.mock.patch.object(self.humidity, "high_humidity_start") as start_mock,
- unittest.mock.patch.object(self.humidity, "high_humidity_end") as end_mock,
- unittest.mock.patch.object(self.humidity, "_check_high_humidity", return_value=True) as check_mock):
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 99)
- check_mock.assert_called_once_with(99)
- start_mock.assert_called_once()
- end_mock.assert_not_called()
-
- with (unittest.mock.patch.object(self.humidity, "high_humidity_start") as start_mock,
- unittest.mock.patch.object(self.humidity, "high_humidity_end") as end_mock,
- unittest.mock.patch.object(self.humidity, "_check_high_humidity", return_value=False) as check_mock):
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 42)
- check_mock.assert_called_once_with(42)
- start_mock.assert_not_called()
- end_mock.assert_called_once()
-
- def test_states(self):
- """Test states."""
- self.assertEqual("off", self.humidity.state)
-
- # some humidity changes below threshold
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 64)
- self.assertEqual("off", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 10)
- self.assertEqual("off", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
-
- # some humidity changes above threshold
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 65)
- self.assertEqual("on_HighHumidity", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "ON")
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 100)
- self.assertEqual("on_HighHumidity", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "ON")
-
- # humidity below threshold again
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 50)
- self.assertEqual("on_Extended", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "ON")
-
- # humidity above threshold again
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 70)
- self.assertEqual("on_HighHumidity", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "ON")
-
- # humidity below threshold again and timeout
- tests.helper.oh_item.item_state_event("Unittest_Humidity", 64)
- self.assertEqual("on_Extended", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "ON")
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual("off", self.humidity.state)
- tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
+ """Tests cases for testing motion sensors rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Humidity", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Unittest_Output_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Custom_Name", None)
+
+ config = habapp_rules.sensors.config.humidity.HumiditySwitchConfig(items=habapp_rules.sensors.config.humidity.HumiditySwitchItems(humidity="Unittest_Humidity", output="Unittest_Output", state="H_Unittest_Output_state"))
+
+ self.humidity = habapp_rules.sensors.humidity.HumiditySwitch(config)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Humidity"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ humidity_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.humidity.states, transitions=self.humidity.trans, initial=self.humidity.state, show_conditions=True)
+
+ humidity_graph.get_graph().draw(picture_dir / "Humidity.png", format="png", prog="dot")
+
+ def test_init(self) -> None:
+ """Test init."""
+ full_config = habapp_rules.sensors.config.humidity.HumiditySwitchConfig(
+ items=habapp_rules.sensors.config.humidity.HumiditySwitchItems(humidity="Unittest_Humidity", output="Unittest_Output", state="Custom_Name"),
+ parameter=habapp_rules.sensors.config.humidity.HumiditySwitchParameter(absolute_threshold=70, extended_time=42),
+ )
+
+ humidity = habapp_rules.sensors.humidity.HumiditySwitch(full_config)
+ self.assertEqual(70, humidity._config.parameter.absolute_threshold)
+ self.assertEqual(42, humidity.state_machine.get_state("on_Extended").timeout)
+ self.assertEqual("Custom_Name", humidity._item_state.name)
+
+ def test_get_initial_state(self) -> None:
+ """Test get_initial_state."""
+ TestCase = collections.namedtuple("TestCase", "humidity_value, expected_state")
+
+ test_cases = [
+ TestCase(None, "off"),
+ TestCase(64, "off"),
+ TestCase(65, "on"),
+ TestCase(66, "on"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Humidity", test_case.humidity_value)
+ self.assertEqual(test_case.expected_state, self.humidity._get_initial_state())
+
+ def test_check_high_humidity(self) -> None:
+ """Test check_high_humidity."""
+ TestCase = collections.namedtuple("TestCase", "item_value,given_value, expected_result")
+
+ test_cases = [
+ # False | False -> False
+ TestCase(None, None, False),
+ TestCase(None, 64, False),
+ TestCase(64, None, False),
+ TestCase(64, 64, False),
+ # False | True -> True
+ TestCase(None, 65, True),
+ TestCase(64, 65, True),
+ # True | False -> False
+ TestCase(65, None, True),
+ TestCase(65, 64, False),
+ # True | True -> True
+ TestCase(65, 65, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Humidity", test_case.item_value)
+ self.assertEqual(test_case.expected_result, self.humidity._check_high_humidity(test_case.given_value))
+
+ def test_cb_humidity(self) -> None:
+ """Test _cb_humidity."""
+ with (
+ unittest.mock.patch.object(self.humidity, "high_humidity_start") as start_mock,
+ unittest.mock.patch.object(self.humidity, "high_humidity_end") as end_mock,
+ unittest.mock.patch.object(self.humidity, "_check_high_humidity", return_value=True) as check_mock,
+ ):
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 99)
+ check_mock.assert_called_once_with(99)
+ start_mock.assert_called_once()
+ end_mock.assert_not_called()
+
+ with (
+ unittest.mock.patch.object(self.humidity, "high_humidity_start") as start_mock,
+ unittest.mock.patch.object(self.humidity, "high_humidity_end") as end_mock,
+ unittest.mock.patch.object(self.humidity, "_check_high_humidity", return_value=False) as check_mock,
+ ):
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 42)
+ check_mock.assert_called_once_with(42)
+ start_mock.assert_not_called()
+ end_mock.assert_called_once()
+
+ def test_states(self) -> None:
+ """Test states."""
+ self.assertEqual("off", self.humidity.state)
+
+ # some humidity changes below threshold
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 64)
+ self.assertEqual("off", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 10)
+ self.assertEqual("off", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
+
+ # some humidity changes above threshold
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 65)
+ self.assertEqual("on_HighHumidity", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "ON")
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 100)
+ self.assertEqual("on_HighHumidity", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "ON")
+
+ # humidity below threshold again
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 50)
+ self.assertEqual("on_Extended", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "ON")
+
+ # humidity above threshold again
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 70)
+ self.assertEqual("on_HighHumidity", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "ON")
+
+ # humidity below threshold again and timeout
+ tests.helper.oh_item.item_state_event("Unittest_Humidity", 64)
+ self.assertEqual("on_Extended", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "ON")
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual("off", self.humidity.state)
+ tests.helper.oh_item.assert_value("Unittest_Output", "OFF")
diff --git a/tests/sensors/motion.py b/tests/sensors/motion.py
index 0ed0614..48d8c48 100644
--- a/tests/sensors/motion.py
+++ b/tests/sensors/motion.py
@@ -1,6 +1,6 @@
"""Tests for motion sensors."""
+
import collections
-import os
import pathlib
import sys
import unittest
@@ -18,408 +18,393 @@
import tests.helper.test_case_base
-# pylint: disable=no-member, protected-access, too-many-public-methods
class TestMotion(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing motion sensors rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_min_raw", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_min_filtered", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_raw", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_filtered", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_lock", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness_Threshold", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Motion_Unittest_Motion_min_raw_state", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", None)
-
- config_min = habapp_rules.sensors.config.motion.MotionConfig(
- items=habapp_rules.sensors.config.motion.MotionItems(
- motion_raw="Unittest_Motion_min_raw",
- motion_filtered="Unittest_Motion_min_filtered",
- state="H_Motion_Unittest_Motion_min_raw_state"
- )
- )
-
- config_max = habapp_rules.sensors.config.motion.MotionConfig(
- items=habapp_rules.sensors.config.motion.MotionItems(
- motion_raw="Unittest_Motion_max_raw",
- motion_filtered="Unittest_Motion_max_filtered",
- brightness="Unittest_Brightness",
- brightness_threshold="Unittest_Brightness_Threshold",
- sleep_state="Unittest_Sleep_state",
- lock="Unittest_Motion_max_lock",
- state="CustomState"
- ),
- parameter=habapp_rules.sensors.config.motion.MotionParameter(
- extended_motion_time=5
- )
- )
-
- self.motion_min = habapp_rules.sensors.motion.Motion(config_min)
- self.motion_max = habapp_rules.sensors.motion.Motion(config_max)
-
- def test__init__(self):
- """Test __init__."""
- expected_states = [
- {"name": "Locked"},
- {"name": "SleepLocked"},
- {"name": "PostSleepLocked", "timeout": 99, "on_timeout": "timeout_post_sleep_locked"},
- {"name": "Unlocked", "initial": "Init", "children": [
- {"name": "Init"},
- {"name": "Wait"},
- {"name": "Motion"},
- {"name": "MotionExtended", "timeout": 99, "on_timeout": "timeout_motion_extended"},
- {"name": "TooBright"}]}]
- self.assertEqual(expected_states, self.motion_min.states)
-
- expected_trans = [
- {"trigger": "lock_on", "source": ["Unlocked", "SleepLocked", "PostSleepLocked"], "dest": "Locked"},
- {"trigger": "lock_off", "source": "Locked", "dest": "Unlocked", "unless": "_sleep_active"},
- {"trigger": "lock_off", "source": "Locked", "dest": "SleepLocked", "conditions": "_sleep_active"},
- {"trigger": "sleep_started", "source": ["Unlocked", "PostSleepLocked"], "dest": "SleepLocked"},
- {"trigger": "sleep_end", "source": "SleepLocked", "dest": "Unlocked", "unless": "_post_sleep_lock_configured"},
- {"trigger": "sleep_end", "source": "SleepLocked", "dest": "PostSleepLocked", "conditions": "_post_sleep_lock_configured"},
- {"trigger": "timeout_post_sleep_locked", "source": "PostSleepLocked", "dest": "Unlocked", "unless": "_raw_motion_active"},
- {"trigger": "motion_off", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
- {"trigger": "motion_on", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
- {"trigger": "motion_on", "source": "Unlocked_Wait", "dest": "Unlocked_Motion"},
- {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_MotionExtended", "conditions": "_motion_extended_configured"},
- {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_Wait", "unless": "_motion_extended_configured"},
- {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Wait", "unless": "_brightness_over_threshold"},
- {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_TooBright", "conditions": "_brightness_over_threshold"},
- {"trigger": "motion_on", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Motion"},
- {"trigger": "brightness_over_threshold", "source": "Unlocked_Wait", "dest": "Unlocked_TooBright"},
- {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Wait", "unless": "_raw_motion_active"},
- {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Motion", "conditions": "_raw_motion_active"}
- ]
- self.assertEqual(expected_trans, self.motion_min.trans)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- picture_dir = pathlib.Path(__file__).parent / "Motion_States"
- if not picture_dir.is_dir():
- os.makedirs(picture_dir)
-
- motion_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(
- model=tests.helper.graph_machines.FakeModel(),
- states=self.motion_min.states,
- transitions=self.motion_min.trans,
- initial=self.motion_min.state,
- show_conditions=True)
-
- motion_graph.get_graph().draw(picture_dir / "Motion.png", format="png", prog="dot")
-
- def test_initial_state(self):
- """Test _get_initial_state."""
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 100)
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 1000)
-
- TestCase = collections.namedtuple("TestCase", "locked, sleep_state, brightness, motion_raw, expected_state_max, expected_state_min")
-
- test_cases = [
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=False, expected_state_max="Unlocked_Wait", expected_state_min="Unlocked_Wait"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=True, expected_state_max="Unlocked_Motion", expected_state_min="Unlocked_Motion"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=False, expected_state_max="Unlocked_TooBright", expected_state_min="Unlocked_Wait"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=True, expected_state_max="Unlocked_TooBright", expected_state_min="Unlocked_Motion"),
-
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=False, expected_state_max="SleepLocked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=True, expected_state_max="SleepLocked", expected_state_min="Unlocked_Motion"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=False, expected_state_max="SleepLocked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=True, expected_state_max="SleepLocked", expected_state_min="Unlocked_Motion"),
-
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
-
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
- TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Motion_max_lock", "ON" if test_case.locked else "OFF")
- tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_state)
- tests.helper.oh_item.set_state("Unittest_Brightness", test_case.brightness)
- tests.helper.oh_item.set_state("Unittest_Motion_max_raw", "ON" if test_case.motion_raw else "OFF")
- tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON" if test_case.motion_raw else "OFF")
-
- self.assertEqual(test_case.expected_state_max, self.motion_max._get_initial_state("test"))
- self.assertEqual(test_case.expected_state_min, self.motion_min._get_initial_state("test"))
-
- def test_raw_motion_active(self):
- """test _raw_motion_active"""
- tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON")
- self.assertTrue(self.motion_min._raw_motion_active())
-
- tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "OFF")
- self.assertFalse(self.motion_min._raw_motion_active())
-
- def test_get_brightness_threshold(self):
- """test _get_brightness_threshold"""
- # value of threshold item
- self.assertEqual(float("inf"), self.motion_max._get_brightness_threshold())
-
- # value given as parameter
- self.motion_max._config.parameter.brightness_threshold = 800
- self.assertEqual(800, self.motion_max._get_brightness_threshold())
-
- def test_get_brightness_threshold_exceptions(self):
- """test exceptions of _get_brightness_threshold"""
- self.motion_max._config.items.brightness_threshold = None
- with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesException):
- self.motion_max._get_brightness_threshold()
-
- def test_initial_unlock_state(self):
- """test initial state of unlock state."""
- self.assertEqual(float("inf"), self.motion_max._get_brightness_threshold())
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 100)
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 1000)
- self.assertEqual(1000, self.motion_max._get_brightness_threshold())
-
- TestCase = collections.namedtuple("TestCase", "brightness_value, motion_raw, expected_state_min, expected_state_max")
-
- test_cases = [
- TestCase(100, False, expected_state_min="Unlocked_Wait", expected_state_max="Unlocked_Wait"),
- TestCase(100, True, expected_state_min="Unlocked_Motion", expected_state_max="Unlocked_Motion"),
- TestCase(2000, False, expected_state_min="Unlocked_Wait", expected_state_max="Unlocked_TooBright"),
- TestCase(2000, True, expected_state_min="Unlocked_Motion", expected_state_max="Unlocked_TooBright"),
- ]
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Brightness", test_case.brightness_value)
- tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON" if test_case.motion_raw else "OFF")
- tests.helper.oh_item.set_state("Unittest_Motion_max_raw", "ON" if test_case.motion_raw else "OFF")
-
- self.motion_min.to_Unlocked()
- self.motion_max.to_Unlocked()
-
- self.assertEqual(test_case.expected_state_min, self.motion_min.state)
- self.assertEqual(test_case.expected_state_max, self.motion_max.state)
-
- def test_lock(self):
- """Test if lock is activated from all states."""
- for state in self._get_state_names(self.motion_max.states):
- tests.helper.oh_item.set_state("Unittest_Motion_max_lock", "OFF")
- self.motion_max.state = state
- tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "ON", "OFF")
- self.assertEqual("Locked", self.motion_max.state)
-
- def test_motion_extended_configured(self):
- """Test _motion_extended_configured"""
- self.motion_max._config.parameter.extended_motion_time = -1
- self.assertFalse(self.motion_max._motion_extended_configured())
-
- self.motion_max._config.parameter.extended_motion_time = 0
- self.assertFalse(self.motion_max._motion_extended_configured())
-
- self.motion_max._config.parameter.extended_motion_time = 1
- self.assertTrue(self.motion_max._motion_extended_configured())
-
- def test_post_sleep_lock_configured(self):
- """Test _post_sleep_lock_configured"""
- self.motion_max._config.parameter.post_sleep_lock_time = -1
- self.assertFalse(self.motion_max._post_sleep_lock_configured())
-
- self.motion_max._config.parameter.post_sleep_lock_time = 0
- self.assertFalse(self.motion_max._post_sleep_lock_configured())
-
- self.motion_max._config.parameter.post_sleep_lock_time = 1
- self.assertTrue(self.motion_max._post_sleep_lock_configured())
-
- def test_sleep_active(self):
- """Test _sleep_active"""
- tests.helper.oh_item.set_state("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
- self.assertFalse(self.motion_max._sleep_active())
-
- tests.helper.oh_item.set_state("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
- self.assertTrue(self.motion_max._sleep_active())
-
- def test_transitions_locked(self):
- """Test leaving transitions of locked state."""
- # to Unlocked
- self.motion_max.state = "Locked"
- with unittest.mock.patch.object(self.motion_max, "_sleep_active", return_value=False):
- tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "OFF", "ON")
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- # to SleepLocked
- self.motion_max.state = "Locked"
- with unittest.mock.patch.object(self.motion_max, "_sleep_active", return_value=True):
- tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "OFF", "ON")
- self.assertEqual("SleepLocked", self.motion_max.state)
-
- def test_transitions_sleep_locked(self):
- """Test leaving transitions of sleep locked state."""
- # to Unlocked
- self.motion_max.state = "SleepLocked"
- with unittest.mock.patch.object(self.motion_max, "_post_sleep_lock_configured", return_value=False):
- self.motion_max.sleep_end()
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- # to PostSleepLocked
- self.motion_max.state = "SleepLocked"
- with unittest.mock.patch.object(self.motion_max, "_post_sleep_lock_configured", return_value=True):
- self.motion_max.sleep_end()
- self.assertEqual("PostSleepLocked", self.motion_max.state)
-
- def test_transitions_post_sleep_locked(self):
- """Test leaving transitions of post sleep locked state."""
- # to Unlocked | motion not active
- self.motion_max.state = "PostSleepLocked"
- with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=False):
- self.motion_max.timeout_post_sleep_locked()
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- # no change after timeout and motion
- self.motion_max.state = "PostSleepLocked"
- with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=True):
- self.motion_max.timeout_post_sleep_locked()
- self.assertEqual("PostSleepLocked", self.motion_max.state)
-
- # reset timer if motion off
- self.motion_max.state = "PostSleepLocked"
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "OFF", "ON")
- self.assertEqual("PostSleepLocked", self.motion_max.state)
- self.assertEqual(1, self.transitions_timer_mock.call_count)
-
- # reset timer if motion on
- self.motion_max.state = "PostSleepLocked"
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "ON", "OFF")
- self.assertEqual("PostSleepLocked", self.motion_max.state)
- self.assertEqual(1, self.transitions_timer_mock.call_count)
-
- # sleep starts during post sleep
- self.motion_max.state = "PostSleepLocked"
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
- self.assertEqual("SleepLocked", self.motion_max.state)
-
- def test_unlocked_wait(self):
- """Test leaving transitions of Unlocked_Wait state."""
- # to motion
- self.motion_max.state = "Unlocked_Wait"
- self.motion_max.motion_on()
- self.assertEqual("Unlocked_Motion", self.motion_max.state)
-
- # to TooBright
- self.motion_max.state = "Unlocked_Wait"
- self.motion_max.brightness_over_threshold()
- self.assertEqual("Unlocked_TooBright", self.motion_max.state)
-
- def test_unlocked_motion(self):
- """Test leaving transitions of Unlocked_Motion state."""
- # motion off | extended active
- self.motion_max.state = "Unlocked_Motion"
- with unittest.mock.patch.object(self.motion_max, "_motion_extended_configured", return_value=True):
- self.motion_max.motion_off()
- self.assertEqual("Unlocked_MotionExtended", self.motion_max.state)
-
- # motion off | extended not active
- self.motion_max.state = "Unlocked_Motion"
- with unittest.mock.patch.object(self.motion_max, "_motion_extended_configured", return_value=False):
- self.motion_max.motion_off()
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- def test_unlocked_motion_extended(self):
- """Test leaving transitions of Unlocked_MotionExtended state."""
- # timeout | brightness over threshold
- self.motion_max.state = "Unlocked_MotionExtended"
- with unittest.mock.patch.object(self.motion_max, "_brightness_over_threshold", return_value=True):
- self.motion_max.timeout_motion_extended()
- self.assertEqual("Unlocked_TooBright", self.motion_max.state)
-
- # timeout | brightness below threshold
- self.motion_max.state = "Unlocked_MotionExtended"
- with unittest.mock.patch.object(self.motion_max, "_brightness_over_threshold", return_value=False):
- self.motion_max.timeout_motion_extended()
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- # motion on
- self.motion_max.state = "Unlocked_MotionExtended"
- self.motion_max.motion_on()
- self.assertEqual("Unlocked_Motion", self.motion_max.state)
-
- def test_unlocked_too_bright(self):
- """Test leaving transitions of Unlocked_TooBright state."""
- # motion not active
- self.motion_max.state = "Unlocked_TooBright"
- with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=False):
- self.motion_max.brightness_below_threshold()
- self.assertEqual("Unlocked_Wait", self.motion_max.state)
-
- # motion active
- self.motion_max.state = "Unlocked_TooBright"
- with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=True):
- self.motion_max.brightness_below_threshold()
- self.assertEqual("Unlocked_Motion", self.motion_max.state)
-
- def test_check_brightness(self):
- """Test _check_brightness."""
- with unittest.mock.patch.object(self.motion_max._hysteresis_switch, "get_output", return_value=True), \
- unittest.mock.patch.object(self.motion_max, "brightness_over_threshold"), \
- unittest.mock.patch.object(self.motion_max, "brightness_below_threshold"):
- self.motion_max._check_brightness()
- self.motion_max.brightness_over_threshold.assert_called_once()
- self.motion_max.brightness_below_threshold.assert_not_called()
-
- with unittest.mock.patch.object(self.motion_max._hysteresis_switch, "get_output", return_value=False), \
- unittest.mock.patch.object(self.motion_max, "brightness_over_threshold"), \
- unittest.mock.patch.object(self.motion_max, "brightness_below_threshold"):
- self.motion_max._check_brightness()
- self.motion_max.brightness_over_threshold.assert_not_called()
- self.motion_max.brightness_below_threshold.assert_called_once()
-
- def test_cb_brightness_threshold_change(self):
- """Test _cb_threshold_change."""
- with unittest.mock.patch.object(self.motion_max._hysteresis_switch, "set_threshold_on"), unittest.mock.patch.object(self.motion_max, "_check_brightness"):
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 42)
- self.motion_max._hysteresis_switch.set_threshold_on.assert_called_once_with(42)
- self.motion_max._check_brightness.assert_called_once()
-
- def test_cb_motion_raw(self):
- """Test _cb_motion_raw"""
- with unittest.mock.patch.object(self.motion_max, "motion_on"), unittest.mock.patch.object(self.motion_max, "motion_off"):
- tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "ON", "OFF")
- self.motion_max.motion_on.assert_called_once()
- self.motion_max.motion_off.assert_not_called()
-
- with unittest.mock.patch.object(self.motion_max, "motion_on"), unittest.mock.patch.object(self.motion_max, "motion_off"):
- tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "OFF", "ON")
- self.motion_max.motion_on.assert_not_called()
- self.motion_max.motion_off.assert_called_once()
-
- def test_cb_brightness_change(self):
- """Test _cb_threshold_change."""
- with unittest.mock.patch.object(self.motion_max, "_check_brightness"):
- tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 42)
- self.motion_max._check_brightness.assert_called_once()
-
- def test_cb_sleep(self):
- """Test _cb_sleep"""
- for state in habapp_rules.system.SleepState:
- with unittest.mock.patch.object(self.motion_max, "sleep_started"), unittest.mock.patch.object(self.motion_max, "sleep_end"):
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", state.value)
- if state == habapp_rules.system.SleepState.SLEEPING:
- self.motion_max.sleep_started.assert_called_once()
- self.motion_max.sleep_end.assert_not_called()
-
- elif state == habapp_rules.system.SleepState.AWAKE:
- self.motion_max.sleep_started.assert_not_called()
- self.motion_max.sleep_end.assert_called_once()
-
- else:
- self.motion_max.sleep_started.assert_not_called()
- self.motion_max.sleep_end.assert_not_called()
+ """Tests cases for testing motion sensors rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_min_raw", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_min_filtered", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_raw", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_filtered", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Motion_max_lock", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Sleep_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness_Threshold", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Motion_Unittest_Motion_min_raw_state", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", None)
+
+ config_min = habapp_rules.sensors.config.motion.MotionConfig(
+ items=habapp_rules.sensors.config.motion.MotionItems(motion_raw="Unittest_Motion_min_raw", motion_filtered="Unittest_Motion_min_filtered", state="H_Motion_Unittest_Motion_min_raw_state")
+ )
+
+ config_max = habapp_rules.sensors.config.motion.MotionConfig(
+ items=habapp_rules.sensors.config.motion.MotionItems(
+ motion_raw="Unittest_Motion_max_raw",
+ motion_filtered="Unittest_Motion_max_filtered",
+ brightness="Unittest_Brightness",
+ brightness_threshold="Unittest_Brightness_Threshold",
+ sleep_state="Unittest_Sleep_state",
+ lock="Unittest_Motion_max_lock",
+ state="CustomState",
+ ),
+ parameter=habapp_rules.sensors.config.motion.MotionParameter(extended_motion_time=5),
+ )
+
+ self.motion_min = habapp_rules.sensors.motion.Motion(config_min)
+ self.motion_max = habapp_rules.sensors.motion.Motion(config_max)
+
+ def test__init__(self) -> None:
+ """Test __init__."""
+ expected_states = [
+ {"name": "Locked"},
+ {"name": "SleepLocked"},
+ {"name": "PostSleepLocked", "timeout": 99, "on_timeout": "timeout_post_sleep_locked"},
+ {"name": "Unlocked", "initial": "Init", "children": [{"name": "Init"}, {"name": "Wait"}, {"name": "Motion"}, {"name": "MotionExtended", "timeout": 99, "on_timeout": "timeout_motion_extended"}, {"name": "TooBright"}]},
+ ]
+ self.assertEqual(expected_states, self.motion_min.states)
+
+ expected_trans = [
+ {"trigger": "lock_on", "source": ["Unlocked", "SleepLocked", "PostSleepLocked"], "dest": "Locked"},
+ {"trigger": "lock_off", "source": "Locked", "dest": "Unlocked", "unless": "_sleep_active"},
+ {"trigger": "lock_off", "source": "Locked", "dest": "SleepLocked", "conditions": "_sleep_active"},
+ {"trigger": "sleep_started", "source": ["Unlocked", "PostSleepLocked"], "dest": "SleepLocked"},
+ {"trigger": "sleep_end", "source": "SleepLocked", "dest": "Unlocked", "unless": "_post_sleep_lock_configured"},
+ {"trigger": "sleep_end", "source": "SleepLocked", "dest": "PostSleepLocked", "conditions": "_post_sleep_lock_configured"},
+ {"trigger": "timeout_post_sleep_locked", "source": "PostSleepLocked", "dest": "Unlocked", "unless": "_raw_motion_active"},
+ {"trigger": "motion_off", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
+ {"trigger": "motion_on", "source": "PostSleepLocked", "dest": "PostSleepLocked"},
+ {"trigger": "motion_on", "source": "Unlocked_Wait", "dest": "Unlocked_Motion"},
+ {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_MotionExtended", "conditions": "_motion_extended_configured"},
+ {"trigger": "motion_off", "source": "Unlocked_Motion", "dest": "Unlocked_Wait", "unless": "_motion_extended_configured"},
+ {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Wait", "unless": "_brightness_over_threshold"},
+ {"trigger": "timeout_motion_extended", "source": "Unlocked_MotionExtended", "dest": "Unlocked_TooBright", "conditions": "_brightness_over_threshold"},
+ {"trigger": "motion_on", "source": "Unlocked_MotionExtended", "dest": "Unlocked_Motion"},
+ {"trigger": "brightness_over_threshold", "source": "Unlocked_Wait", "dest": "Unlocked_TooBright"},
+ {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Wait", "unless": "_raw_motion_active"},
+ {"trigger": "brightness_below_threshold", "source": "Unlocked_TooBright", "dest": "Unlocked_Motion", "conditions": "_raw_motion_active"},
+ ]
+ self.assertEqual(expected_trans, self.motion_min.trans)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Motion"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+
+ motion_graph = tests.helper.graph_machines.HierarchicalGraphMachineTimer(model=tests.helper.graph_machines.FakeModel(), states=self.motion_min.states, transitions=self.motion_min.trans, initial=self.motion_min.state, show_conditions=True)
+
+ motion_graph.get_graph().draw(picture_dir / "Motion.png", format="png", prog="dot")
+
+ def test_initial_state(self) -> None:
+ """Test _get_initial_state."""
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 100)
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 1000)
+
+ TestCase = collections.namedtuple("TestCase", "locked, sleep_state, brightness, motion_raw, expected_state_max, expected_state_min")
+
+ test_cases = [
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=False, expected_state_max="Unlocked_Wait", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=True, expected_state_max="Unlocked_Motion", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=False, expected_state_max="Unlocked_TooBright", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=True, expected_state_max="Unlocked_TooBright", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=False, expected_state_max="SleepLocked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=True, expected_state_max="SleepLocked", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=False, expected_state_max="SleepLocked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=False, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=True, expected_state_max="SleepLocked", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.AWAKE.value, brightness=1500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=False, expected_state_max="Locked", expected_state_min="Unlocked_Wait"),
+ TestCase(locked=True, sleep_state=habapp_rules.system.SleepState.SLEEPING.value, brightness=1500, motion_raw=True, expected_state_max="Locked", expected_state_min="Unlocked_Motion"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Motion_max_lock", "ON" if test_case.locked else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", test_case.sleep_state)
+ tests.helper.oh_item.set_state("Unittest_Brightness", test_case.brightness)
+ tests.helper.oh_item.set_state("Unittest_Motion_max_raw", "ON" if test_case.motion_raw else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON" if test_case.motion_raw else "OFF")
+
+ self.assertEqual(test_case.expected_state_max, self.motion_max._get_initial_state("test"))
+ self.assertEqual(test_case.expected_state_min, self.motion_min._get_initial_state("test"))
+
+ def test_raw_motion_active(self) -> None:
+ """Test _raw_motion_active."""
+ tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON")
+ self.assertTrue(self.motion_min._raw_motion_active())
+
+ tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "OFF")
+ self.assertFalse(self.motion_min._raw_motion_active())
+
+ def test_get_brightness_threshold(self) -> None:
+ """Test _get_brightness_threshold."""
+ # value of threshold item
+ self.assertEqual(float("inf"), self.motion_max._get_brightness_threshold())
+
+ # value given as parameter
+ self.motion_max._config.parameter.brightness_threshold = 800
+ self.assertEqual(800, self.motion_max._get_brightness_threshold())
+
+ def test_get_brightness_threshold_exceptions(self) -> None:
+ """Test exceptions of _get_brightness_threshold."""
+ self.motion_max._config.items.brightness_threshold = None
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesError):
+ self.motion_max._get_brightness_threshold()
+
+ def test_initial_unlock_state(self) -> None:
+ """Test initial state of unlock state."""
+ self.assertEqual(float("inf"), self.motion_max._get_brightness_threshold())
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 100)
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 1000)
+ self.assertEqual(1000, self.motion_max._get_brightness_threshold())
+
+ TestCase = collections.namedtuple("TestCase", "brightness_value, motion_raw, expected_state_min, expected_state_max")
+
+ test_cases = [
+ TestCase(100, False, expected_state_min="Unlocked_Wait", expected_state_max="Unlocked_Wait"),
+ TestCase(100, True, expected_state_min="Unlocked_Motion", expected_state_max="Unlocked_Motion"),
+ TestCase(2000, False, expected_state_min="Unlocked_Wait", expected_state_max="Unlocked_TooBright"),
+ TestCase(2000, True, expected_state_min="Unlocked_Motion", expected_state_max="Unlocked_TooBright"),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Brightness", test_case.brightness_value)
+ tests.helper.oh_item.set_state("Unittest_Motion_min_raw", "ON" if test_case.motion_raw else "OFF")
+ tests.helper.oh_item.set_state("Unittest_Motion_max_raw", "ON" if test_case.motion_raw else "OFF")
+
+ self.motion_min.to_Unlocked()
+ self.motion_max.to_Unlocked()
+
+ self.assertEqual(test_case.expected_state_min, self.motion_min.state)
+ self.assertEqual(test_case.expected_state_max, self.motion_max.state)
+
+ def test_lock(self) -> None:
+ """Test if lock is activated from all states."""
+ for state in self._get_state_names(self.motion_max.states):
+ tests.helper.oh_item.set_state("Unittest_Motion_max_lock", "OFF")
+ self.motion_max.state = state
+ tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "ON", "OFF")
+ self.assertEqual("Locked", self.motion_max.state)
+
+ def test_motion_extended_configured(self) -> None:
+ """Test _motion_extended_configured."""
+ self.motion_max._config.parameter.extended_motion_time = -1
+ self.assertFalse(self.motion_max._motion_extended_configured())
+
+ self.motion_max._config.parameter.extended_motion_time = 0
+ self.assertFalse(self.motion_max._motion_extended_configured())
+
+ self.motion_max._config.parameter.extended_motion_time = 1
+ self.assertTrue(self.motion_max._motion_extended_configured())
+
+ def test_post_sleep_lock_configured(self) -> None:
+ """Test _post_sleep_lock_configured."""
+ self.motion_max._config.parameter.post_sleep_lock_time = -1
+ self.assertFalse(self.motion_max._post_sleep_lock_configured())
+
+ self.motion_max._config.parameter.post_sleep_lock_time = 0
+ self.assertFalse(self.motion_max._post_sleep_lock_configured())
+
+ self.motion_max._config.parameter.post_sleep_lock_time = 1
+ self.assertTrue(self.motion_max._post_sleep_lock_configured())
+
+ def test_sleep_active(self) -> None:
+ """Test _sleep_active."""
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", habapp_rules.system.SleepState.AWAKE.value)
+ self.assertFalse(self.motion_max._sleep_active())
+
+ tests.helper.oh_item.set_state("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertTrue(self.motion_max._sleep_active())
+
+ def test_transitions_locked(self) -> None:
+ """Test leaving transitions of locked state."""
+ # to Unlocked
+ self.motion_max.state = "Locked"
+ with unittest.mock.patch.object(self.motion_max, "_sleep_active", return_value=False):
+ tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "OFF", "ON")
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ # to SleepLocked
+ self.motion_max.state = "Locked"
+ with unittest.mock.patch.object(self.motion_max, "_sleep_active", return_value=True):
+ tests.helper.oh_item.send_command("Unittest_Motion_max_lock", "OFF", "ON")
+ self.assertEqual("SleepLocked", self.motion_max.state)
+
+ def test_transitions_sleep_locked(self) -> None:
+ """Test leaving transitions of sleep locked state."""
+ # to Unlocked
+ self.motion_max.state = "SleepLocked"
+ with unittest.mock.patch.object(self.motion_max, "_post_sleep_lock_configured", return_value=False):
+ self.motion_max.sleep_end()
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ # to PostSleepLocked
+ self.motion_max.state = "SleepLocked"
+ with unittest.mock.patch.object(self.motion_max, "_post_sleep_lock_configured", return_value=True):
+ self.motion_max.sleep_end()
+ self.assertEqual("PostSleepLocked", self.motion_max.state)
+
+ def test_transitions_post_sleep_locked(self) -> None:
+ """Test leaving transitions of post sleep locked state."""
+ # to Unlocked | motion not active
+ self.motion_max.state = "PostSleepLocked"
+ with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=False):
+ self.motion_max.timeout_post_sleep_locked()
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ # no change after timeout and motion
+ self.motion_max.state = "PostSleepLocked"
+ with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=True):
+ self.motion_max.timeout_post_sleep_locked()
+ self.assertEqual("PostSleepLocked", self.motion_max.state)
+
+ # reset timer if motion off
+ self.motion_max.state = "PostSleepLocked"
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "OFF", "ON")
+ self.assertEqual("PostSleepLocked", self.motion_max.state)
+ self.assertEqual(1, self.transitions_timer_mock.call_count)
+
+ # reset timer if motion on
+ self.motion_max.state = "PostSleepLocked"
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "ON", "OFF")
+ self.assertEqual("PostSleepLocked", self.motion_max.state)
+ self.assertEqual(1, self.transitions_timer_mock.call_count)
+
+ # sleep starts during post sleep
+ self.motion_max.state = "PostSleepLocked"
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", habapp_rules.system.SleepState.SLEEPING.value)
+ self.assertEqual("SleepLocked", self.motion_max.state)
+
+ def test_unlocked_wait(self) -> None:
+ """Test leaving transitions of Unlocked_Wait state."""
+ # to motion
+ self.motion_max.state = "Unlocked_Wait"
+ self.motion_max.motion_on()
+ self.assertEqual("Unlocked_Motion", self.motion_max.state)
+
+ # to TooBright
+ self.motion_max.state = "Unlocked_Wait"
+ self.motion_max.brightness_over_threshold()
+ self.assertEqual("Unlocked_TooBright", self.motion_max.state)
+
+ def test_unlocked_motion(self) -> None:
+ """Test leaving transitions of Unlocked_Motion state."""
+ # motion off | extended active
+ self.motion_max.state = "Unlocked_Motion"
+ with unittest.mock.patch.object(self.motion_max, "_motion_extended_configured", return_value=True):
+ self.motion_max.motion_off()
+ self.assertEqual("Unlocked_MotionExtended", self.motion_max.state)
+
+ # motion off | extended not active
+ self.motion_max.state = "Unlocked_Motion"
+ with unittest.mock.patch.object(self.motion_max, "_motion_extended_configured", return_value=False):
+ self.motion_max.motion_off()
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ def test_unlocked_motion_extended(self) -> None:
+ """Test leaving transitions of Unlocked_MotionExtended state."""
+ # timeout | brightness over threshold
+ self.motion_max.state = "Unlocked_MotionExtended"
+ with unittest.mock.patch.object(self.motion_max, "_brightness_over_threshold", return_value=True):
+ self.motion_max.timeout_motion_extended()
+ self.assertEqual("Unlocked_TooBright", self.motion_max.state)
+
+ # timeout | brightness below threshold
+ self.motion_max.state = "Unlocked_MotionExtended"
+ with unittest.mock.patch.object(self.motion_max, "_brightness_over_threshold", return_value=False):
+ self.motion_max.timeout_motion_extended()
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ # motion on
+ self.motion_max.state = "Unlocked_MotionExtended"
+ self.motion_max.motion_on()
+ self.assertEqual("Unlocked_Motion", self.motion_max.state)
+
+ def test_unlocked_too_bright(self) -> None:
+ """Test leaving transitions of Unlocked_TooBright state."""
+ # motion not active
+ self.motion_max.state = "Unlocked_TooBright"
+ with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=False):
+ self.motion_max.brightness_below_threshold()
+ self.assertEqual("Unlocked_Wait", self.motion_max.state)
+
+ # motion active
+ self.motion_max.state = "Unlocked_TooBright"
+ with unittest.mock.patch.object(self.motion_max, "_raw_motion_active", return_value=True):
+ self.motion_max.brightness_below_threshold()
+ self.assertEqual("Unlocked_Motion", self.motion_max.state)
+
+ def test_check_brightness(self) -> None:
+ """Test _check_brightness."""
+ with (
+ unittest.mock.patch.object(self.motion_max._hysteresis_switch, "get_output", return_value=True),
+ unittest.mock.patch.object(self.motion_max, "brightness_over_threshold"),
+ unittest.mock.patch.object(self.motion_max, "brightness_below_threshold"),
+ ):
+ self.motion_max._check_brightness()
+ self.motion_max.brightness_over_threshold.assert_called_once()
+ self.motion_max.brightness_below_threshold.assert_not_called()
+
+ with (
+ unittest.mock.patch.object(self.motion_max._hysteresis_switch, "get_output", return_value=False),
+ unittest.mock.patch.object(self.motion_max, "brightness_over_threshold"),
+ unittest.mock.patch.object(self.motion_max, "brightness_below_threshold"),
+ ):
+ self.motion_max._check_brightness()
+ self.motion_max.brightness_over_threshold.assert_not_called()
+ self.motion_max.brightness_below_threshold.assert_called_once()
+
+ def test_cb_brightness_threshold_change(self) -> None:
+ """Test _cb_threshold_change."""
+ with unittest.mock.patch.object(self.motion_max._hysteresis_switch, "set_threshold_on"), unittest.mock.patch.object(self.motion_max, "_check_brightness"):
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness_Threshold", 42)
+ self.motion_max._hysteresis_switch.set_threshold_on.assert_called_once_with(42)
+ self.motion_max._check_brightness.assert_called_once()
+
+ def test_cb_motion_raw(self) -> None:
+ """Test _cb_motion_raw."""
+ with unittest.mock.patch.object(self.motion_max, "motion_on"), unittest.mock.patch.object(self.motion_max, "motion_off"):
+ tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "ON", "OFF")
+ self.motion_max.motion_on.assert_called_once()
+ self.motion_max.motion_off.assert_not_called()
+
+ with unittest.mock.patch.object(self.motion_max, "motion_on"), unittest.mock.patch.object(self.motion_max, "motion_off"):
+ tests.helper.oh_item.item_state_change_event("Unittest_Motion_max_raw", "OFF", "ON")
+ self.motion_max.motion_on.assert_not_called()
+ self.motion_max.motion_off.assert_called_once()
+
+ def test_cb_brightness_change(self) -> None:
+ """Test _cb_threshold_change."""
+ with unittest.mock.patch.object(self.motion_max, "_check_brightness"):
+ tests.helper.oh_item.item_state_change_event("Unittest_Brightness", 42)
+ self.motion_max._check_brightness.assert_called_once()
+
+ def test_cb_sleep(self) -> None:
+ """Test _cb_sleep."""
+ for state in habapp_rules.system.SleepState:
+ with unittest.mock.patch.object(self.motion_max, "sleep_started"), unittest.mock.patch.object(self.motion_max, "sleep_end"):
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep_state", state.value)
+ if state == habapp_rules.system.SleepState.SLEEPING:
+ self.motion_max.sleep_started.assert_called_once()
+ self.motion_max.sleep_end.assert_not_called()
+
+ elif state == habapp_rules.system.SleepState.AWAKE:
+ self.motion_max.sleep_started.assert_not_called()
+ self.motion_max.sleep_end.assert_called_once()
+
+ else:
+ self.motion_max.sleep_started.assert_not_called()
+ self.motion_max.sleep_end.assert_not_called()
diff --git a/tests/sensors/sun.py b/tests/sensors/sun.py
index 53cba17..bf3fdb1 100644
--- a/tests/sensors/sun.py
+++ b/tests/sensors/sun.py
@@ -1,4 +1,5 @@
"""Tests for sun sensors."""
+
import collections
import unittest.mock
@@ -11,277 +12,250 @@
import tests.helper.test_case_base
-# pylint: disable=no-member, protected-access, too-many-public-methods
class TestSensorTemperatureDifference(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing sun sensor 'temp_diff' rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Temperature", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold_Temperature", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Temperature_diff_for_Unittest_Output_Temperature", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Temperature_diff_for_Unittest_Output_Temperature_filtered", None)
-
- config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
- items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(
- temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"],
- output="Unittest_Output_Temperature",
- threshold="Unittest_Threshold_Temperature"
- )
- )
-
- with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
- self._sensor = habapp_rules.sensors.sun.SensorTemperatureDifference(config)
-
- def test_init(self):
- """Test __init__."""
- self.assertEqual(float("inf"), self._sensor._hysteresis_switch._threshold)
- self.assertEqual("H_Temperature_diff_for_Unittest_Output_Temperature", self._sensor._item_temp_diff.name)
-
- def test_init_with_fixed_threshold(self):
- """Test __init__ with fixed threshold value."""
- config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
- items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(
- temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"],
- output="Unittest_Output_Temperature"
- ),
- parameter=habapp_rules.sensors.config.sun.TemperatureDifferenceParameter(
- threshold=42
- )
- )
-
- with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
- sensor = habapp_rules.sensors.sun.SensorTemperatureDifference(config)
- self.assertEqual(42, sensor._hysteresis_switch._threshold)
-
- def test_cb_threshold(self):
- """Test _cb_threshold"""
- tests.helper.oh_item.item_state_change_event("Unittest_Threshold_Temperature", 20)
- self.assertEqual(20, self._sensor._hysteresis_switch._threshold)
-
- def test_temp_diff(self):
- """Test if temperature difference is calculated correctly."""
- temp_diff_item = HABApp.openhab.items.OpenhabItem.get_item("H_Temperature_diff_for_Unittest_Output_Temperature")
- self.assertEqual(None, temp_diff_item.value)
-
- # update temperature 1
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_1", 20)
- self.assertEqual(None, temp_diff_item.value)
-
- # update temperature 2
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", 21)
- self.assertEqual(1, temp_diff_item.value)
-
- # update temperature 2
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", 18)
- self.assertEqual(2, temp_diff_item.value)
-
- # update temperature 1
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_1", -20)
- self.assertEqual(38, temp_diff_item.value)
-
- # update temperature 2
- tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", -25)
- self.assertEqual(5, temp_diff_item.value)
-
- def test_threshold_behavior(self):
- """Test overall behavior"""
- output_item = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_Temperature")
- self.assertEqual(None, output_item.value)
-
- # set threshold to 10
- self._sensor._hysteresis_switch.set_threshold_on(10)
-
- # update temp_diff to 10
- tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 10)
- self.assertEqual("ON", output_item.value)
-
- # update temp_diff to 9.9
- tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 9.9)
- self.assertEqual("OFF", output_item.value)
-
- # update temp_diff to 8
- tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 8)
- self.assertEqual("OFF", output_item.value)
+ """Tests cases for testing sun sensor 'temp_diff' rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Temperature", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold_Temperature", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Temperature_diff_for_Unittest_Output_Temperature", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Temperature_diff_for_Unittest_Output_Temperature_filtered", None)
+
+ config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
+ items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"], output="Unittest_Output_Temperature", threshold="Unittest_Threshold_Temperature")
+ )
+
+ with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
+ self._sensor = habapp_rules.sensors.sun.SensorTemperatureDifference(config)
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ self.assertEqual(float("inf"), self._sensor._hysteresis_switch._threshold)
+ self.assertEqual("H_Temperature_diff_for_Unittest_Output_Temperature", self._sensor._item_temp_diff.name)
+
+ def test_init_with_fixed_threshold(self) -> None:
+ """Test __init__ with fixed threshold value."""
+ config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
+ items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(temperatures=["Unittest_Temperature_1", "Unittest_Temperature_2"], output="Unittest_Output_Temperature"),
+ parameter=habapp_rules.sensors.config.sun.TemperatureDifferenceParameter(threshold=42),
+ )
+
+ with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
+ sensor = habapp_rules.sensors.sun.SensorTemperatureDifference(config)
+ self.assertEqual(42, sensor._hysteresis_switch._threshold)
+
+ def test_cb_threshold(self) -> None:
+ """Test _cb_threshold."""
+ tests.helper.oh_item.item_state_change_event("Unittest_Threshold_Temperature", 20)
+ self.assertEqual(20, self._sensor._hysteresis_switch._threshold)
+
+ def test_temp_diff(self) -> None:
+ """Test if temperature difference is calculated correctly."""
+ temp_diff_item = HABApp.openhab.items.OpenhabItem.get_item("H_Temperature_diff_for_Unittest_Output_Temperature")
+ self.assertEqual(None, temp_diff_item.value)
+
+ # update temperature 1
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_1", 20)
+ self.assertEqual(None, temp_diff_item.value)
+
+ # update temperature 2
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", 21)
+ self.assertEqual(1, temp_diff_item.value)
+
+ # update temperature 2
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", 18)
+ self.assertEqual(2, temp_diff_item.value)
+
+ # update temperature 1
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_1", -20)
+ self.assertEqual(38, temp_diff_item.value)
+
+ # update temperature 2
+ tests.helper.oh_item.item_state_change_event("Unittest_Temperature_2", -25)
+ self.assertEqual(5, temp_diff_item.value)
+
+ def test_threshold_behavior(self) -> None:
+ """Test overall behavior."""
+ output_item = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_Temperature")
+ self.assertEqual(None, output_item.value)
+
+ # set threshold to 10
+ self._sensor._hysteresis_switch.set_threshold_on(10)
+
+ # update temp_diff to 10
+ tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 10)
+ self.assertEqual("ON", output_item.value)
+
+ # update temp_diff to 9.9
+ tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 9.9)
+ self.assertEqual("OFF", output_item.value)
+
+ # update temp_diff to 8
+ tests.helper.oh_item.item_state_change_event("H_Temperature_diff_for_Unittest_Output_Temperature_filtered", 8)
+ self.assertEqual("OFF", output_item.value)
class TestSensorBrightness(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing sun sensor 'brightness' rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Brightness", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold_Brightness", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Unittest_Brightness_filtered", None)
-
- config = habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output_Brightness",
- threshold="Unittest_Threshold_Brightness"
- )
- )
-
- with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
- self._sensor = habapp_rules.sensors.sun.SensorBrightness(config)
-
- def test_init(self):
- """Test __init__."""
- self.assertEqual(float("inf"), self._sensor._hysteresis_switch._threshold)
-
- def test_init_with_fixed_threshold(self):
- """Test __init__ with fixed threshold value."""
- config = habapp_rules.sensors.config.sun.BrightnessConfig(
- items=habapp_rules.sensors.config.sun.BrightnessItems(
- brightness="Unittest_Brightness",
- output="Unittest_Output_Brightness",
- ),
- parameter=habapp_rules.sensors.config.sun.BrightnessParameter(
- threshold=42
- )
- )
-
- with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
- sensor = habapp_rules.sensors.sun.SensorBrightness(config)
- self.assertEqual(42, sensor._hysteresis_switch._threshold)
-
- def test_cb_threshold(self):
- """Test _cb_threshold."""
- tests.helper.oh_item.item_state_change_event("Unittest_Threshold_Brightness", 42000)
- self.assertEqual(42000, self._sensor._hysteresis_switch._threshold)
-
- def test_threshold_behavior(self):
- """Test overall behavior"""
- output_item = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_Brightness")
- self.assertEqual(None, output_item.value)
-
- # set threshold to 1000
- self._sensor._hysteresis_switch.set_threshold_on(1000)
-
- # update temp_diff to 1000
- tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 1000)
- self.assertEqual("ON", output_item.value)
-
- # update temp_diff to 999
- tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 999)
- self.assertEqual("OFF", output_item.value)
-
- # update temp_diff to 800
- tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 800)
- self.assertEqual("OFF", output_item.value)
+ """Tests cases for testing sun sensor 'brightness' rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Brightness", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_Brightness", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Threshold_Brightness", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "H_Unittest_Brightness_filtered", None)
+
+ config = habapp_rules.sensors.config.sun.BrightnessConfig(items=habapp_rules.sensors.config.sun.BrightnessItems(brightness="Unittest_Brightness", output="Unittest_Output_Brightness", threshold="Unittest_Threshold_Brightness"))
+
+ with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
+ self._sensor = habapp_rules.sensors.sun.SensorBrightness(config)
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ self.assertEqual(float("inf"), self._sensor._hysteresis_switch._threshold)
+
+ def test_init_with_fixed_threshold(self) -> None:
+ """Test __init__ with fixed threshold value."""
+ config = habapp_rules.sensors.config.sun.BrightnessConfig(
+ items=habapp_rules.sensors.config.sun.BrightnessItems(
+ brightness="Unittest_Brightness",
+ output="Unittest_Output_Brightness",
+ ),
+ parameter=habapp_rules.sensors.config.sun.BrightnessParameter(threshold=42),
+ )
+
+ with unittest.mock.patch("HABApp.openhab.interface_sync.item_exists", return_value=True), unittest.mock.patch("habapp_rules.common.filter.ExponentialFilter"):
+ sensor = habapp_rules.sensors.sun.SensorBrightness(config)
+ self.assertEqual(42, sensor._hysteresis_switch._threshold)
+
+ def test_cb_threshold(self) -> None:
+ """Test _cb_threshold."""
+ tests.helper.oh_item.item_state_change_event("Unittest_Threshold_Brightness", 42000)
+ self.assertEqual(42000, self._sensor._hysteresis_switch._threshold)
+
+ def test_threshold_behavior(self) -> None:
+ """Test overall behavior."""
+ output_item = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_Brightness")
+ self.assertEqual(None, output_item.value)
+
+ # set threshold to 1000
+ self._sensor._hysteresis_switch.set_threshold_on(1000)
+
+ # update temp_diff to 1000
+ tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 1000)
+ self.assertEqual("ON", output_item.value)
+
+ # update temp_diff to 999
+ tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 999)
+ self.assertEqual("OFF", output_item.value)
+
+ # update temp_diff to 800
+ tests.helper.oh_item.item_state_change_event("H_Unittest_Brightness_filtered", 800)
+ self.assertEqual("OFF", output_item.value)
class TestSunPositionFilter(tests.helper.test_case_base.TestCaseBase):
- """Tests cases for testing the sun position filter."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_1", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_2", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_2", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Azimuth", 1000)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", 1000)
-
- self.position_window_1 = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
- self.position_window_2 = habapp_rules.sensors.config.sun.SunPositionWindow(100, 120)
-
- config_1 = habapp_rules.sensors.config.sun.SunPositionConfig(
- items=habapp_rules.sensors.config.sun.SunPositionItems(
- azimuth="Unittest_Azimuth",
- elevation="Unittest_Elevation",
- input="Unittest_Input_1",
- output="Unittest_Output_1",
- ),
- parameter=habapp_rules.sensors.config.sun.SunPositionParameter(
- sun_position_window=self.position_window_1
- )
- )
- config_2 = habapp_rules.sensors.config.sun.SunPositionConfig(
- items=habapp_rules.sensors.config.sun.SunPositionItems(
- azimuth="Unittest_Azimuth",
- elevation="Unittest_Elevation",
- input="Unittest_Input_2",
- output="Unittest_Output_2",
- ),
- parameter=habapp_rules.sensors.config.sun.SunPositionParameter(
- sun_position_window=[self.position_window_1, self.position_window_2]
- )
- )
-
- self._filter_1 = habapp_rules.sensors.sun.SunPositionFilter(config_1)
- self._filter_2 = habapp_rules.sensors.sun.SunPositionFilter(config_2)
-
- def test_init(self):
- """Test __init__"""
- self.assertEqual([self.position_window_1], self._filter_1._config.parameter.sun_position_windows)
- self.assertEqual([self.position_window_1, self.position_window_2], self._filter_2._config.parameter.sun_position_windows)
-
- def test_filter(self):
- """Test if filter is working correctly."""
- TestCase = collections.namedtuple("TestCase", "azimuth, elevation, input, output_1, output_2")
-
- test_cases = [
- TestCase(0, 0, "OFF", "OFF", "OFF"),
- TestCase(0, 10, "OFF", "OFF", "OFF"),
- TestCase(50, 0, "OFF", "OFF", "OFF"),
- TestCase(50, 10, "OFF", "OFF", "OFF"),
-
- TestCase(0, 0, "ON", "OFF", "OFF"),
- TestCase(0, 10, "ON", "OFF", "OFF"),
- TestCase(50, 0, "ON", "OFF", "OFF"),
- TestCase(50, 10, "ON", "ON", "ON"),
-
- TestCase(0, 0, "OFF", "OFF", "OFF"),
- TestCase(0, 10, "OFF", "OFF", "OFF"),
- TestCase(110, 0, "OFF", "OFF", "OFF"),
- TestCase(110, 10, "OFF", "OFF", "OFF"),
-
- TestCase(0, 0, "ON", "OFF", "OFF"),
- TestCase(0, 10, "ON", "OFF", "OFF"),
- TestCase(110, 0, "ON", "OFF", "ON"),
- TestCase(110, 10, "ON", "OFF", "ON"),
-
- TestCase(50, None, "OFF", "OFF", "OFF"),
- TestCase(None, 10, "OFF", "OFF", "OFF"),
- TestCase(None, None, "OFF", "OFF", "OFF"),
-
- TestCase(50, None, "ON", "ON", "ON"),
- TestCase(None, 10, "ON", "ON", "ON"),
- TestCase(None, None, "ON", "ON", "ON"),
- ]
-
- item_output_1 = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_1")
- item_output_2 = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_2")
-
- with unittest.mock.patch.object(self._filter_1, "_instance_logger") as log_1_mock, unittest.mock.patch.object(self._filter_2, "_instance_logger") as log_2_mock:
- for test_case in test_cases:
- log_1_mock.reset_mock()
- log_2_mock.reset_mock()
-
- tests.helper.oh_item.set_state("Unittest_Input_1", test_case.input)
- tests.helper.oh_item.set_state("Unittest_Input_2", test_case.input)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation)
- tests.helper.oh_item.item_state_change_event("Unittest_Azimuth", test_case.azimuth)
-
- self.assertEqual(test_case.output_1, item_output_1.value)
- self.assertEqual(test_case.output_2, item_output_2.value)
-
- if test_case.azimuth is None or test_case.elevation is None:
- log_1_mock.warning.assert_called_once()
- log_2_mock.warning.assert_called_once()
- else:
- log_1_mock.warning.assert_not_called()
- log_2_mock.warning.assert_not_called()
+ """Tests cases for testing the sun position filter."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_1", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Input_2", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Output_2", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Azimuth", 1000)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Elevation", 1000)
+
+ self.position_window_1 = habapp_rules.sensors.config.sun.SunPositionWindow(10, 80, 2, 20)
+ self.position_window_2 = habapp_rules.sensors.config.sun.SunPositionWindow(100, 120)
+
+ config_1 = habapp_rules.sensors.config.sun.SunPositionConfig(
+ items=habapp_rules.sensors.config.sun.SunPositionItems(
+ azimuth="Unittest_Azimuth",
+ elevation="Unittest_Elevation",
+ input="Unittest_Input_1",
+ output="Unittest_Output_1",
+ ),
+ parameter=habapp_rules.sensors.config.sun.SunPositionParameter(sun_position_window=self.position_window_1),
+ )
+ config_2 = habapp_rules.sensors.config.sun.SunPositionConfig(
+ items=habapp_rules.sensors.config.sun.SunPositionItems(
+ azimuth="Unittest_Azimuth",
+ elevation="Unittest_Elevation",
+ input="Unittest_Input_2",
+ output="Unittest_Output_2",
+ ),
+ parameter=habapp_rules.sensors.config.sun.SunPositionParameter(sun_position_window=[self.position_window_1, self.position_window_2]),
+ )
+
+ self._filter_1 = habapp_rules.sensors.sun.SunPositionFilter(config_1)
+ self._filter_2 = habapp_rules.sensors.sun.SunPositionFilter(config_2)
+
+ def test_init(self) -> None:
+ """Test __init__."""
+ self.assertEqual([self.position_window_1], self._filter_1._config.parameter.sun_position_windows)
+ self.assertEqual([self.position_window_1, self.position_window_2], self._filter_2._config.parameter.sun_position_windows)
+
+ def test_filter(self) -> None:
+ """Test if filter is working correctly."""
+ TestCase = collections.namedtuple("TestCase", "azimuth, elevation, input, output_1, output_2")
+
+ test_cases = [
+ TestCase(0, 0, "OFF", "OFF", "OFF"),
+ TestCase(0, 10, "OFF", "OFF", "OFF"),
+ TestCase(50, 0, "OFF", "OFF", "OFF"),
+ TestCase(50, 10, "OFF", "OFF", "OFF"),
+ TestCase(0, 0, "ON", "OFF", "OFF"),
+ TestCase(0, 10, "ON", "OFF", "OFF"),
+ TestCase(50, 0, "ON", "OFF", "OFF"),
+ TestCase(50, 10, "ON", "ON", "ON"),
+ TestCase(0, 0, "OFF", "OFF", "OFF"),
+ TestCase(0, 10, "OFF", "OFF", "OFF"),
+ TestCase(110, 0, "OFF", "OFF", "OFF"),
+ TestCase(110, 10, "OFF", "OFF", "OFF"),
+ TestCase(0, 0, "ON", "OFF", "OFF"),
+ TestCase(0, 10, "ON", "OFF", "OFF"),
+ TestCase(110, 0, "ON", "OFF", "ON"),
+ TestCase(110, 10, "ON", "OFF", "ON"),
+ TestCase(50, None, "OFF", "OFF", "OFF"),
+ TestCase(None, 10, "OFF", "OFF", "OFF"),
+ TestCase(None, None, "OFF", "OFF", "OFF"),
+ TestCase(50, None, "ON", "ON", "ON"),
+ TestCase(None, 10, "ON", "ON", "ON"),
+ TestCase(None, None, "ON", "ON", "ON"),
+ ]
+
+ item_output_1 = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_1")
+ item_output_2 = HABApp.openhab.items.OpenhabItem.get_item("Unittest_Output_2")
+
+ with unittest.mock.patch.object(self._filter_1, "_instance_logger") as log_1_mock, unittest.mock.patch.object(self._filter_2, "_instance_logger") as log_2_mock:
+ for test_case in test_cases:
+ log_1_mock.reset_mock()
+ log_2_mock.reset_mock()
+
+ tests.helper.oh_item.set_state("Unittest_Input_1", test_case.input)
+ tests.helper.oh_item.set_state("Unittest_Input_2", test_case.input)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Elevation", test_case.elevation)
+ tests.helper.oh_item.item_state_change_event("Unittest_Azimuth", test_case.azimuth)
+
+ self.assertEqual(test_case.output_1, item_output_1.value)
+ self.assertEqual(test_case.output_2, item_output_2.value)
+
+ if test_case.azimuth is None or test_case.elevation is None:
+ log_1_mock.warning.assert_called_once()
+ log_2_mock.warning.assert_called_once()
+ else:
+ log_1_mock.warning.assert_not_called()
+ log_2_mock.warning.assert_not_called()
diff --git a/tests/system/Presence.png b/tests/system/_state_charts/Presence/Presence.png
similarity index 100%
rename from tests/system/Presence.png
rename to tests/system/_state_charts/Presence/Presence.png
diff --git a/tests/system/Sleep.png b/tests/system/_state_charts/Sleep/Sleep.png
similarity index 100%
rename from tests/system/Sleep.png
rename to tests/system/_state_charts/Sleep/Sleep.png
diff --git a/tests/system/item_watchdog.py b/tests/system/item_watchdog.py
new file mode 100644
index 0000000..d61156e
--- /dev/null
+++ b/tests/system/item_watchdog.py
@@ -0,0 +1,42 @@
+"""Tests for Watchdog Rule."""
+
+import unittest.mock
+
+import HABApp.openhab.items
+
+import habapp_rules.system.item_watchdog
+import tests.helper.oh_item
+import tests.helper.test_case_base
+from habapp_rules.system.config.item_watchdog import WatchdogConfig, WatchdogItems, WatchdogParameter
+
+
+class TestWatchdog(tests.helper.test_case_base.TestCaseBase):
+ """Tests for Watchdog Rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Number_Warning", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_Warning", None)
+
+ self._watchdog_number = habapp_rules.system.item_watchdog.ItemWatchdog(WatchdogConfig(items=WatchdogItems(observed="Unittest_Number", warning="Unittest_Number_Warning")))
+
+ self._watchdog_switch = habapp_rules.system.item_watchdog.ItemWatchdog(WatchdogConfig(items=WatchdogItems(observed="Unittest_Switch", warning="Unittest_Switch_Warning"), parameter=WatchdogParameter(timeout=10)))
+
+ def test_cb_observed_state_updated(self) -> None:
+ """Callback which is called if the observed item was updated."""
+ with unittest.mock.patch.object(self._watchdog_number, "_countdown") as number_countdown_mock, unittest.mock.patch.object(self._watchdog_switch, "_countdown") as switch_countdown_mock:
+ tests.helper.oh_item.item_state_event("Unittest_Number", 42)
+ number_countdown_mock.reset.assert_called_once()
+ switch_countdown_mock.reset.assert_not_called()
+ tests.helper.oh_item.assert_value("Unittest_Number_Warning", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Switch_Warning", None)
+
+ tests.helper.oh_item.item_state_event("Unittest_Switch", "OFF")
+ number_countdown_mock.reset.assert_called_once()
+ switch_countdown_mock.reset.assert_called_once()
+ tests.helper.oh_item.assert_value("Unittest_Number_Warning", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Switch_Warning", "OFF")
diff --git a/tests/system/notification.py b/tests/system/notification.py
index fb3c923..ab0c5cc 100644
--- a/tests/system/notification.py
+++ b/tests/system/notification.py
@@ -1,4 +1,5 @@
"""Test notification rules."""
+
import unittest.mock
import HABApp.openhab.items
@@ -12,40 +13,38 @@
class TestNotification(tests.helper.test_case_base.TestCaseBase):
- """Test class for notification"""
+ """Test class for notification."""
- def setUp(self):
- """Set up test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
+ def setUp(self) -> None:
+ """Set up test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_String", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
- self._mail_mock = unittest.mock.MagicMock(spec=multi_notifier.connectors.connector_mail.Mail)
- self._telegram_mock = unittest.mock.MagicMock(spec=multi_notifier.connectors.connector_telegram.Telegram)
+ self._mail_mock = unittest.mock.MagicMock(spec=multi_notifier.connectors.connector_mail.Mail)
+ self._telegram_mock = unittest.mock.MagicMock(spec=multi_notifier.connectors.connector_telegram.Telegram)
- self._mail_rule = habapp_rules.system.notification.SendStateChanged(NotificationConfig(
- items=NotificationItems(target_item=HABApp.openhab.items.OpenhabItem.get_item("Unittest_String")),
- parameter=NotificationParameter(notify_connector=self._mail_mock, recipients="mock@mail.de")
- ))
- self._telegram_rule = habapp_rules.system.notification.SendStateChanged(NotificationConfig(
- items=NotificationItems(target_item=HABApp.openhab.items.OpenhabItem.get_item("Unittest_Switch")),
- parameter=NotificationParameter(notify_connector=self._telegram_mock, recipients="mock_id")
- ))
+ self._mail_rule = habapp_rules.system.notification.SendStateChanged(
+ NotificationConfig(items=NotificationItems(target_item=HABApp.openhab.items.OpenhabItem.get_item("Unittest_String")), parameter=NotificationParameter(notify_connector=self._mail_mock, recipients="mock@mail.de"))
+ )
+ self._telegram_rule = habapp_rules.system.notification.SendStateChanged(
+ NotificationConfig(items=NotificationItems(target_item=HABApp.openhab.items.OpenhabItem.get_item("Unittest_Switch")), parameter=NotificationParameter(notify_connector=self._telegram_mock, recipients="mock_id"))
+ )
- def test_state_changed(self):
- """Test state changed."""
- self._mail_mock.send_message.assert_not_called()
- self._telegram_mock.send_message.assert_not_called()
+ def test_state_changed(self) -> None:
+ """Test state changed."""
+ self._mail_mock.send_message.assert_not_called()
+ self._telegram_mock.send_message.assert_not_called()
- tests.helper.oh_item.item_state_change_event("Unittest_String", "New value")
- self._mail_mock.send_message.assert_called_once_with("mock@mail.de", "Unittest_String changed from None to New value", subject="Unittest_String changed")
+ tests.helper.oh_item.item_state_change_event("Unittest_String", "New value")
+ self._mail_mock.send_message.assert_called_once_with("mock@mail.de", "Unittest_String changed from None to New value", subject="Unittest_String changed")
- tests.helper.oh_item.item_state_change_event("Unittest_String", "Even never value")
- self._mail_mock.send_message.assert_called_with("mock@mail.de", "Unittest_String changed from New value to Even never value", subject="Unittest_String changed")
+ tests.helper.oh_item.item_state_change_event("Unittest_String", "Even never value")
+ self._mail_mock.send_message.assert_called_with("mock@mail.de", "Unittest_String changed from New value to Even never value", subject="Unittest_String changed")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", "ON")
- self._telegram_mock.send_message.assert_called_once_with("mock_id", "Unittest_Switch changed from None to ON")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", "ON")
+ self._telegram_mock.send_message.assert_called_once_with("mock_id", "Unittest_Switch changed from None to ON")
- tests.helper.oh_item.item_state_change_event("Unittest_Switch", "OFF")
- self._telegram_mock.send_message.assert_called_with("mock_id", "Unittest_Switch changed from ON to OFF")
+ tests.helper.oh_item.item_state_change_event("Unittest_Switch", "OFF")
+ self._telegram_mock.send_message.assert_called_with("mock_id", "Unittest_Switch changed from ON to OFF")
diff --git a/tests/system/presence.py b/tests/system/presence.py
index 3adff38..f6db1e0 100644
--- a/tests/system/presence.py
+++ b/tests/system/presence.py
@@ -1,4 +1,5 @@
"""Test Presence rule."""
+
import collections
import pathlib
import sys
@@ -17,429 +18,390 @@
import tests.helper.timer
-# pylint: disable=protected-access
class TestPresence(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing presence rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door1", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door2", "CLOSED")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Leaving", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Phone1", "ON")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Phone2", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Presence_Unittest_Presence_state", "")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Presence", "ON")
-
- config = habapp_rules.system.config.presence.PresenceConfig(
- items=habapp_rules.system.config.presence.PresenceItems(
- presence="Unittest_Presence",
- leaving="Unittest_Leaving",
- outdoor_doors=["Unittest_Door1", "Unittest_Door2"],
- phones=["Unittest_Phone1", "Unittest_Phone2"],
- state="CustomState"
- )
- )
-
- self._presence = habapp_rules.system.presence.Presence(config)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Presence", None)
- tests.helper.oh_item.set_state("Unittest_Door1", None)
- tests.helper.oh_item.set_state("Unittest_Door2", None)
- tests.helper.oh_item.set_state("Unittest_Leaving", None)
- tests.helper.oh_item.set_state("Unittest_Phone1", None)
- tests.helper.oh_item.set_state("Unittest_Phone2", None)
- tests.helper.oh_item.set_state("CustomState", None)
-
- config = habapp_rules.system.config.presence.PresenceConfig(
- items=habapp_rules.system.config.presence.PresenceItems(
- presence="Unittest_Presence",
- leaving="Unittest_Leaving",
- outdoor_doors=["Unittest_Door1", "Unittest_Door2"],
- phones=["Unittest_Phone1", "Unittest_Phone2"],
- state="CustomState"
- )
- )
-
- habapp_rules.system.presence.Presence(config)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- presence_graph = tests.helper.graph_machines.GraphMachineTimer(
- model=self._presence,
- states=self._presence.states,
- transitions=self._presence.trans,
- initial=self._presence.state,
- show_conditions=True)
-
- presence_graph.get_graph().draw(pathlib.Path(__file__).parent / "Presence.png", format="png", prog="dot")
-
- def test_minimal_init(self):
- """Test init with minimal set of arguments."""
- config = habapp_rules.system.config.presence.PresenceConfig(
- items=habapp_rules.system.config.presence.PresenceItems(
- presence="Unittest_Presence",
- leaving="Unittest_Leaving",
- state="CustomState"
- )
- )
-
- presence_min = habapp_rules.system.presence.Presence(config)
-
- self.assertEqual([], presence_min._config.items.phones)
- self.assertEqual([], presence_min._config.items.outdoor_doors)
-
- def test_enums(self):
- """Test if all enums from __init__.py are implemented"""
- implemented_states = list(self._presence.state_machine.states)
- enum_states = [state.value for state in habapp_rules.system.PresenceState] + ["initial"]
- self.assertEqual(len(enum_states), len(implemented_states))
- self.assertTrue(all(state in enum_states for state in implemented_states))
-
- def test__init__(self):
- """Test init."""
- tests.helper.oh_item.assert_value("CustomState", "presence")
- self.assertEqual(self._presence.state, "presence")
-
- def test_get_initial_state(self):
- """Test getting correct initial state."""
- Testcase = collections.namedtuple("Testcase", "presence, outside_doors, leaving, phones, expected_result")
-
- testcases = [
- # presence ON | leaving OFF
- Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=[], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=[], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=[], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- # presence ON | leaving ON
- Testcase(presence="ON", leaving="ON", outside_doors=[], phones=[], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=[], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=[], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="leaving"),
- Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- # presence OFF | leaving OFF
- Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=[], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["ON"], expected_result="presence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["OFF"], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=[], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["OFF"], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=[], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["OFF"], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
-
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="absence"),
- Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
-
- # all None
- Testcase(presence=None, leaving=None, outside_doors=[None, None], phones=[None, None], expected_result="default"),
- ]
-
- for testcase in testcases:
- with self.subTest(testcase=testcase):
- self._presence._config.items.presence.value = testcase.presence
- self._presence._config.items.leaving.value = testcase.leaving
-
- self._presence._config.items.outdoor_doors = [HABApp.openhab.items.ContactItem(f"Unittest_Door{idx}", state) for idx, state in enumerate(testcase.outside_doors)]
- self._presence._config.items.phones = [HABApp.openhab.items.SwitchItem(f"Unittest_Phone{idx}", state) for idx, state in enumerate(testcase.phones)]
-
- self.assertEqual(self._presence._get_initial_state("default"), testcase.expected_result, f"failed testcase: {testcase}")
-
- def test_get_initial_state_extra(self):
- """Test getting correct initial state for special cases."""
- # current state value is long_absence
- self._presence._config.items.presence.value = "OFF"
- self._presence._config.items.leaving.value = "OFF"
- self._presence._config.items.state.value = "long_absence"
- self._presence._config.items.outdoor_doors = []
-
- # no phones
- self._presence._config.items.phones = []
- self.assertEqual(self._presence._get_initial_state("default"), "long_absence")
-
- # with phones
- self._presence._config.items.phones = [HABApp.openhab.items.SwitchItem("Unittest_Phone1")]
- self.assertEqual(self._presence._get_initial_state("default"), "long_absence")
-
- def test_presence_trough_doors(self):
- """Test if outside doors set presence correctly."""
- tests.helper.oh_item.send_command("Unittest_Presence", "OFF")
- self._presence.state_machine.set_state("absence")
- self.assertEqual(self._presence.state, "absence")
-
- tests.helper.oh_item.send_command("Unittest_Door1", "CLOSED", "CLOSED")
- self.assertEqual(self._presence.state, "absence")
-
- tests.helper.oh_item.send_command("Unittest_Door1", "OPEN", "CLOSED")
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.assert_value("Unittest_Presence", "ON")
-
- tests.helper.oh_item.send_command("Unittest_Door1", "OPEN", "CLOSED")
- self.assertEqual(self._presence.state, "presence")
-
- tests.helper.oh_item.send_command("Unittest_Door1", "CLOSED", "CLOSED")
- self.assertEqual(self._presence.state, "presence")
-
- def test_normal_leaving(self):
- """Test if 'normal' leaving works correctly."""
- self._presence.state_machine.set_state("presence")
- self.assertEqual(self._presence.state, "presence")
-
- tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
- self.assertEqual(self._presence.state, "presence")
-
- tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
- self.assertEqual(self._presence.state, "leaving")
- self.transitions_timer_mock.assert_called_with(300, unittest.mock.ANY, args=unittest.mock.ANY)
-
- # call timeout and check if absence is active
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._presence.state, "absence")
-
- # leaving switches to on again -> state should be leaving again
- tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
- self.assertEqual(self._presence.state, "leaving")
-
- # test if also long absence is working
- self._presence.state = "long_absence"
- tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
- self.assertEqual(self._presence.state, "leaving")
-
- def test_abort_leaving(self):
- """Test aborting of leaving state."""
- self._presence.state_machine.set_state("presence")
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.set_state("Unittest_Leaving", "ON")
-
- tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
- self.assertEqual(self._presence.state, "leaving")
- tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
-
- tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.assert_value("Unittest_Leaving", "OFF")
-
- def test_abort_leaving_after_last_phone(self):
- """Test aborting of leaving which was started through last phone leaving."""
- self._presence.state_machine.set_state("presence")
- tests.helper.oh_item.set_state("Unittest_Phone1", "ON")
-
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- tests.helper.timer.call_timeout(self.threading_timer_mock)
- self.assertEqual(self._presence.state, "leaving")
- tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
-
- tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
- self.assertEqual(self._presence.state, "presence")
-
- tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
-
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- tests.helper.timer.call_timeout(self.threading_timer_mock)
- self.assertEqual(self._presence.state, "leaving")
- tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
-
- def test_leaving_with_phones(self):
- """Test if leaving and absence is correct if phones appear/disappear during or after leaving."""
- # set initial states
- tests.helper.oh_item.set_state("Unittest_Phone1", "ON")
- tests.helper.oh_item.set_state("Unittest_Phone2", "OFF")
- self._presence.state_machine.set_state("presence")
- tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
- self.assertEqual(self._presence.state, "leaving")
-
- # leaving on, last phone disappears
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- self.assertEqual(self._presence.state, "leaving")
-
- # leaving on, first phone appears
- tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
-
- # leaving on, second phone appears
- tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
-
- # leaving on, both phones leaving
- self._presence.state_machine.set_state("leaving")
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
- self.assertEqual(self._presence.state, "leaving")
-
- # absence on, one disappears, one stays online
- tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._presence.state, "absence")
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- self.assertEqual(self._presence.state, "absence")
-
- # absence on, two phones disappears
- tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
- self.assertEqual(self._presence.state, "absence")
-
- def test__set_leaving_through_phone(self):
- """Test if leaving_detected is called correctly after timeout of __phone_absence_timer."""
- TestCase = collections.namedtuple("TestCase", "state, leaving_detected_called")
-
- test_cases = [
- TestCase("presence", True),
- TestCase("leaving", False),
- TestCase("absence", False),
- TestCase("long_absence", False)
- ]
-
- for test_case in test_cases:
- with unittest.mock.patch.object(self._presence, "leaving_detected") as leaving_detected_mock:
- self._presence.state = test_case.state
- self._presence._Presence__set_leaving_through_phone()
- self.assertEqual(test_case.leaving_detected_called, leaving_detected_mock.called)
-
- # pylint: disable=no-member
- def test_long_absence(self):
- """Test entering long_absence and leaving it."""
- # set initial state
- self._presence.state_machine.set_state("presence")
- tests.helper.oh_item.set_state("Unittest_Presence", "ON")
-
- # go to absence
- self._presence.absence_detected()
- self.assertEqual(self._presence.state, "absence")
- tests.helper.oh_item.assert_value("Unittest_Presence", "OFF")
-
- # check if timeout started, and stop the mocked timer
- self.transitions_timer_mock.assert_called_with(1.5 * 24 * 3600, unittest.mock.ANY, args=unittest.mock.ANY)
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._presence.state, "long_absence")
- tests.helper.oh_item.assert_value("Unittest_Presence", "OFF")
-
- # check if presence is set after door open
- self._presence._cb_outside_door(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Door1", "OPEN", "CLOSED"))
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.assert_value("Unittest_Presence", "ON")
-
- def test_manual_change(self):
- """Test if change of presence object is setting correct state."""
- # send manual off from presence
- self._presence.state_machine.set_state("presence")
- tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
- self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "OFF", "ON"))
- self.assertEqual(self._presence.state, "absence")
- tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
-
- # send manual off from leaving
- self._presence.state_machine.set_state("leaving")
- tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
- self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "OFF", "ON"))
- self.assertEqual(self._presence.state, "absence")
- tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
-
- # send manual on from absence
- self._presence.state_machine.set_state("absence")
- tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
- self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "ON", "OFF"))
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
-
- # send manual on from long_absence
- self._presence.state_machine.set_state("long_absence")
- tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
- self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "ON", "OFF"))
- self.assertEqual(self._presence.state, "presence")
- tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
-
- def test_phones(self):
- """Test if presence is set correctly through phones."""
- # first phone switches to ON -> presence expected
- self._presence.state_machine.set_state("absence")
- tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
- self.threading_timer_mock.assert_not_called()
-
- # second phone switches to ON -> no change expected
- tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
- self.threading_timer_mock.assert_not_called()
-
- # second phone switches to OFF -> no change expected
- tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
- self.assertEqual(self._presence.state, "presence")
- self.threading_timer_mock.assert_not_called()
-
- # first phone switches to OFF -> timer should be started
- tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
- self.assertEqual(self._presence.state, "presence")
- self.threading_timer_mock.assert_called_once_with(1200, self._presence._Presence__set_leaving_through_phone)
- tests.helper.timer.call_timeout(self.threading_timer_mock)
- self.assertEqual(self._presence.state, "leaving")
-
- # phone appears during leaving -> leaving expected
- tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
- self.assertEqual(self._presence.state, "presence")
- self.assertIsNone(self._presence._Presence__phone_absence_timer)
-
- # timeout is over -> absence expected
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._presence.state, "absence")
-
- def test_on_rule_removed(self):
- """Test on_rule_removed."""
- # timer NOT running
- with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove:
- self._presence.on_rule_removed()
-
- parent_on_remove.assert_called_once()
-
- # timer running
- self._presence._Presence__phone_absence_timer = threading.Timer(42, unittest.mock.MagicMock())
- self._presence._Presence__phone_absence_timer.start()
- with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove:
- self._presence.on_rule_removed()
-
- parent_on_remove.assert_called_once()
- self.assertIsNone(self._presence._Presence__phone_absence_timer)
+ """Tests cases for testing presence rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door1", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.ContactItem, "Unittest_Door2", "CLOSED")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Leaving", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Phone1", "ON")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Phone2", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Presence_Unittest_Presence_state", "")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Presence", "ON")
+
+ config = habapp_rules.system.config.presence.PresenceConfig(
+ items=habapp_rules.system.config.presence.PresenceItems(presence="Unittest_Presence", leaving="Unittest_Leaving", outdoor_doors=["Unittest_Door1", "Unittest_Door2"], phones=["Unittest_Phone1", "Unittest_Phone2"], state="CustomState")
+ )
+
+ self._presence = habapp_rules.system.presence.Presence(config)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Presence", None)
+ tests.helper.oh_item.set_state("Unittest_Door1", None)
+ tests.helper.oh_item.set_state("Unittest_Door2", None)
+ tests.helper.oh_item.set_state("Unittest_Leaving", None)
+ tests.helper.oh_item.set_state("Unittest_Phone1", None)
+ tests.helper.oh_item.set_state("Unittest_Phone2", None)
+ tests.helper.oh_item.set_state("CustomState", None)
+
+ config = habapp_rules.system.config.presence.PresenceConfig(
+ items=habapp_rules.system.config.presence.PresenceItems(presence="Unittest_Presence", leaving="Unittest_Leaving", outdoor_doors=["Unittest_Door1", "Unittest_Door2"], phones=["Unittest_Phone1", "Unittest_Phone2"], state="CustomState")
+ )
+
+ habapp_rules.system.presence.Presence(config)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ presence_graph = tests.helper.graph_machines.GraphMachineTimer(model=self._presence, states=self._presence.states, transitions=self._presence.trans, initial=self._presence.state, show_conditions=True)
+
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Presence"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+ presence_graph.get_graph().draw(picture_dir / "Presence.png", format="png", prog="dot")
+
+ def test_minimal_init(self) -> None:
+ """Test init with minimal set of arguments."""
+ config = habapp_rules.system.config.presence.PresenceConfig(items=habapp_rules.system.config.presence.PresenceItems(presence="Unittest_Presence", leaving="Unittest_Leaving", state="CustomState"))
+
+ presence_min = habapp_rules.system.presence.Presence(config)
+
+ self.assertEqual([], presence_min._config.items.phones)
+ self.assertEqual([], presence_min._config.items.outdoor_doors)
+
+ def test_enums(self) -> None:
+ """Test if all enums from __init__.py are implemented."""
+ implemented_states = list(self._presence.state_machine.states)
+ enum_states = [state.value for state in habapp_rules.system.PresenceState] + ["initial"]
+ self.assertEqual(len(enum_states), len(implemented_states))
+ self.assertTrue(all(state in enum_states for state in implemented_states))
+
+ def test__init__(self) -> None:
+ """Test init."""
+ tests.helper.oh_item.assert_value("CustomState", "presence")
+ self.assertEqual(self._presence.state, "presence")
+
+ def test_get_initial_state(self) -> None:
+ """Test getting correct initial state."""
+ Testcase = collections.namedtuple("Testcase", "presence, outside_doors, leaving, phones, expected_result")
+
+ testcases = [
+ # presence ON | leaving OFF
+ Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=[], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=[], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=[], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ # presence ON | leaving ON
+ Testcase(presence="ON", leaving="ON", outside_doors=[], phones=[], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=[], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=[], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="leaving"),
+ Testcase(presence="ON", leaving="ON", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ # presence OFF | leaving OFF
+ Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=[], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["ON"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["OFF"], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=[], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=[], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["OFF"], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=[], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["OFF"], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN"], phones=["ON", "OFF"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=[], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON"], expected_result="presence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["OFF"], expected_result="absence"),
+ Testcase(presence="OFF", leaving="OFF", outside_doors=["OPEN, CLOSED"], phones=["ON", "OFF"], expected_result="presence"),
+ # all None
+ Testcase(presence=None, leaving=None, outside_doors=[None, None], phones=[None, None], expected_result="default"),
+ ]
+
+ for testcase in testcases:
+ with self.subTest(testcase=testcase):
+ self._presence._config.items.presence.value = testcase.presence
+ self._presence._config.items.leaving.value = testcase.leaving
+
+ self._presence._config.items.outdoor_doors = [HABApp.openhab.items.ContactItem(f"Unittest_Door{idx}", state) for idx, state in enumerate(testcase.outside_doors)]
+ self._presence._config.items.phones = [HABApp.openhab.items.SwitchItem(f"Unittest_Phone{idx}", state) for idx, state in enumerate(testcase.phones)]
+
+ self.assertEqual(self._presence._get_initial_state("default"), testcase.expected_result, f"failed testcase: {testcase}")
+
+ def test_get_initial_state_extra(self) -> None:
+ """Test getting correct initial state for special cases."""
+ # current state value is long_absence
+ self._presence._config.items.presence.value = "OFF"
+ self._presence._config.items.leaving.value = "OFF"
+ self._presence._config.items.state.value = "long_absence"
+ self._presence._config.items.outdoor_doors = []
+
+ # no phones
+ self._presence._config.items.phones = []
+ self.assertEqual(self._presence._get_initial_state("default"), "long_absence")
+
+ # with phones
+ self._presence._config.items.phones = [HABApp.openhab.items.SwitchItem("Unittest_Phone1")]
+ self.assertEqual(self._presence._get_initial_state("default"), "long_absence")
+
+ def test_presence_trough_doors(self) -> None:
+ """Test if outside doors set presence correctly."""
+ tests.helper.oh_item.send_command("Unittest_Presence", "OFF")
+ self._presence.state_machine.set_state("absence")
+ self.assertEqual(self._presence.state, "absence")
+
+ tests.helper.oh_item.send_command("Unittest_Door1", "CLOSED", "CLOSED")
+ self.assertEqual(self._presence.state, "absence")
+
+ tests.helper.oh_item.send_command("Unittest_Door1", "OPEN", "CLOSED")
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.assert_value("Unittest_Presence", "ON")
+
+ tests.helper.oh_item.send_command("Unittest_Door1", "OPEN", "CLOSED")
+ self.assertEqual(self._presence.state, "presence")
+
+ tests.helper.oh_item.send_command("Unittest_Door1", "CLOSED", "CLOSED")
+ self.assertEqual(self._presence.state, "presence")
+
+ def test_normal_leaving(self) -> None:
+ """Test if 'normal' leaving works correctly."""
+ self._presence.state_machine.set_state("presence")
+ self.assertEqual(self._presence.state, "presence")
+
+ tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
+ self.assertEqual(self._presence.state, "presence")
+
+ tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
+ self.assertEqual(self._presence.state, "leaving")
+ self.transitions_timer_mock.assert_called_with(300, unittest.mock.ANY, args=unittest.mock.ANY)
+
+ # call timeout and check if absence is active
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._presence.state, "absence")
+
+ # leaving switches to on again -> state should be leaving again
+ tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
+ self.assertEqual(self._presence.state, "leaving")
+
+ # test if also long absence is working
+ self._presence.state = "long_absence"
+ tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
+ self.assertEqual(self._presence.state, "leaving")
+
+ def test_abort_leaving(self) -> None:
+ """Test aborting of leaving state."""
+ self._presence.state_machine.set_state("presence")
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.set_state("Unittest_Leaving", "ON")
+
+ tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
+ self.assertEqual(self._presence.state, "leaving")
+ tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
+
+ tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.assert_value("Unittest_Leaving", "OFF")
+
+ def test_abort_leaving_after_last_phone(self) -> None:
+ """Test aborting of leaving which was started through last phone leaving."""
+ self._presence.state_machine.set_state("presence")
+ tests.helper.oh_item.set_state("Unittest_Phone1", "ON")
+
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ tests.helper.timer.call_timeout(self.threading_timer_mock)
+ self.assertEqual(self._presence.state, "leaving")
+ tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
+
+ tests.helper.oh_item.send_command("Unittest_Leaving", "OFF", "ON")
+ self.assertEqual(self._presence.state, "presence")
+
+ tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ tests.helper.timer.call_timeout(self.threading_timer_mock)
+ self.assertEqual(self._presence.state, "leaving")
+ tests.helper.oh_item.assert_value("Unittest_Leaving", "ON")
+
+ def test_leaving_with_phones(self) -> None:
+ """Test if leaving and absence is correct if phones appear/disappear during or after leaving."""
+ # set initial states
+ tests.helper.oh_item.set_state("Unittest_Phone1", "ON")
+ tests.helper.oh_item.set_state("Unittest_Phone2", "OFF")
+ self._presence.state_machine.set_state("presence")
+ tests.helper.oh_item.send_command("Unittest_Leaving", "ON", "OFF")
+ self.assertEqual(self._presence.state, "leaving")
+
+ # leaving on, last phone disappears
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ self.assertEqual(self._presence.state, "leaving")
+
+ # leaving on, first phone appears
+ tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+
+ # leaving on, second phone appears
+ tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+
+ # leaving on, both phones leaving
+ self._presence.state_machine.set_state("leaving")
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
+ self.assertEqual(self._presence.state, "leaving")
+
+ # absence on, one disappears, one stays online
+ tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._presence.state, "absence")
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ self.assertEqual(self._presence.state, "absence")
+
+ # absence on, two phones disappears
+ tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
+ self.assertEqual(self._presence.state, "absence")
+
+ def test__set_leaving_through_phone(self) -> None:
+ """Test if leaving_detected is called correctly after timeout of __phone_absence_timer."""
+ TestCase = collections.namedtuple("TestCase", "state, leaving_detected_called")
+
+ test_cases = [TestCase("presence", True), TestCase("leaving", False), TestCase("absence", False), TestCase("long_absence", False)]
+
+ for test_case in test_cases:
+ with unittest.mock.patch.object(self._presence, "leaving_detected") as leaving_detected_mock:
+ self._presence.state = test_case.state
+ self._presence._Presence__set_leaving_through_phone()
+ self.assertEqual(test_case.leaving_detected_called, leaving_detected_mock.called)
+
+ def test_long_absence(self) -> None:
+ """Test entering long_absence and leaving it."""
+ # set initial state
+ self._presence.state_machine.set_state("presence")
+ tests.helper.oh_item.set_state("Unittest_Presence", "ON")
+
+ # go to absence
+ self._presence.absence_detected()
+ self.assertEqual(self._presence.state, "absence")
+ tests.helper.oh_item.assert_value("Unittest_Presence", "OFF")
+
+ # check if timeout started, and stop the mocked timer
+ self.transitions_timer_mock.assert_called_with(1.5 * 24 * 3600, unittest.mock.ANY, args=unittest.mock.ANY)
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._presence.state, "long_absence")
+ tests.helper.oh_item.assert_value("Unittest_Presence", "OFF")
+
+ # check if presence is set after door open
+ self._presence._cb_outside_door(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Door1", "OPEN", "CLOSED"))
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.assert_value("Unittest_Presence", "ON")
+
+ def test_manual_change(self) -> None:
+ """Test if change of presence object is setting correct state."""
+ # send manual off from presence
+ self._presence.state_machine.set_state("presence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
+ self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "OFF", "ON"))
+ self.assertEqual(self._presence.state, "absence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
+
+ # send manual off from leaving
+ self._presence.state_machine.set_state("leaving")
+ tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
+ self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "OFF", "ON"))
+ self.assertEqual(self._presence.state, "absence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
+
+ # send manual on from absence
+ self._presence.state_machine.set_state("absence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
+ self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "ON", "OFF"))
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
+
+ # send manual on from long_absence
+ self._presence.state_machine.set_state("long_absence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "OFF", "ON")
+ self._presence._cb_presence(HABApp.openhab.events.ItemStateChangedEvent("Unittest_Presence", "ON", "OFF"))
+ self.assertEqual(self._presence.state, "presence")
+ tests.helper.oh_item.send_command("Unittest_Presence", "ON", "OFF")
+
+ def test_phones(self) -> None:
+ """Test if presence is set correctly through phones."""
+ # first phone switches to ON -> presence expected
+ self._presence.state_machine.set_state("absence")
+ tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+ self.threading_timer_mock.assert_not_called()
+
+ # second phone switches to ON -> no change expected
+ tests.helper.oh_item.send_command("Unittest_Phone2", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+ self.threading_timer_mock.assert_not_called()
+
+ # second phone switches to OFF -> no change expected
+ tests.helper.oh_item.send_command("Unittest_Phone2", "OFF", "ON")
+ self.assertEqual(self._presence.state, "presence")
+ self.threading_timer_mock.assert_not_called()
+
+ # first phone switches to OFF -> timer should be started
+ tests.helper.oh_item.send_command("Unittest_Phone1", "OFF", "ON")
+ self.assertEqual(self._presence.state, "presence")
+ self.threading_timer_mock.assert_called_once_with(1200, self._presence._Presence__set_leaving_through_phone)
+ tests.helper.timer.call_timeout(self.threading_timer_mock)
+ self.assertEqual(self._presence.state, "leaving")
+
+ # phone appears during leaving -> leaving expected
+ tests.helper.oh_item.send_command("Unittest_Phone1", "ON", "OFF")
+ self.assertEqual(self._presence.state, "presence")
+ self.assertIsNone(self._presence._Presence__phone_absence_timer)
+
+ # timeout is over -> absence expected
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._presence.state, "absence")
+
+ def test_on_rule_removed(self) -> None:
+ """Test on_rule_removed."""
+ # timer NOT running
+ with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove:
+ self._presence.on_rule_removed()
+
+ parent_on_remove.assert_called_once()
+
+ # timer running
+ self._presence._Presence__phone_absence_timer = threading.Timer(42, unittest.mock.MagicMock())
+ self._presence._Presence__phone_absence_timer.start()
+ with unittest.mock.patch("habapp_rules.core.state_machine_rule.StateMachineRule.on_rule_removed") as parent_on_remove:
+ self._presence.on_rule_removed()
+
+ parent_on_remove.assert_called_once()
+ self.assertIsNone(self._presence._Presence__phone_absence_timer)
diff --git a/tests/system/sleep.py b/tests/system/sleep.py
index c87a44d..f50326b 100644
--- a/tests/system/sleep.py
+++ b/tests/system/sleep.py
@@ -1,4 +1,5 @@
"""Test sleep rule."""
+
import collections
import datetime
import pathlib
@@ -17,424 +18,388 @@
import tests.helper.timer
-# pylint: disable=protected-access
class TestSleep(tests.helper.test_case_base.TestCaseBaseStateMachine):
- """Tests cases for testing presence rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep_Request", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Lock", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Lock_Request", "OFF")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Display_Text", "")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Sleep_Unittest_Sleep_state", "")
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
-
- config = habapp_rules.system.config.sleep.SleepConfig(
- items=habapp_rules.system.config.sleep.SleepItems(
- sleep="Unittest_Sleep",
- sleep_request="Unittest_Sleep_Request",
- lock="Unittest_Lock",
- lock_request="Unittest_Lock_Request",
- display_text="Unittest_Display_Text",
- state="CustomState"
- )
- )
-
- self._sleep = habapp_rules.system.sleep.Sleep(config)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Sleep", None)
- tests.helper.oh_item.set_state("Unittest_Sleep_Request", None)
- tests.helper.oh_item.set_state("Unittest_Lock", None)
- tests.helper.oh_item.set_state("Unittest_Lock_Request", None)
- tests.helper.oh_item.set_state("Unittest_Display_Text", None)
- tests.helper.oh_item.set_state("CustomState", None)
-
- config = habapp_rules.system.config.sleep.SleepConfig(
- items=habapp_rules.system.config.sleep.SleepItems(
- sleep="Unittest_Sleep",
- sleep_request="Unittest_Sleep_Request",
- lock="Unittest_Lock",
- lock_request="Unittest_Lock_Request",
- display_text="Unittest_Display_Text",
- state="CustomState"
- )
- )
-
- habapp_rules.system.sleep.Sleep(config)
-
- @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
- def test_create_graph(self): # pragma: no cover
- """Create state machine graph for documentation."""
- presence_graph = tests.helper.graph_machines.GraphMachineTimer(
- model=self._sleep,
- states=self._sleep.states,
- transitions=self._sleep.trans,
- initial=self._sleep.state,
- show_conditions=True
- )
- presence_graph.get_graph().draw(pathlib.Path(__file__).parent / "Sleep.png", format="png", prog="dot")
-
- def test_enums(self):
- """Test if all enums from __init__.py are implemented"""
- implemented_states = list(self._sleep.state_machine.states)
- enum_states = [state.value for state in habapp_rules.system.SleepState] + ["initial"]
- self.assertEqual(len(enum_states), len(implemented_states))
- self.assertTrue(all(state in enum_states for state in implemented_states))
-
- def test__init__(self):
- """Test init of sleep class."""
- TestCase = collections.namedtuple("TestCase", "sleep_request_state, lock_request_state, lock_state")
-
- test_cases = [
- TestCase("OFF", "OFF", "OFF"),
- TestCase("OFF", "ON", "ON"),
- TestCase("ON", "OFF", "OFF"),
- TestCase("ON", "ON", "OFF"),
- ]
-
- config = habapp_rules.system.config.sleep.SleepConfig(
- items=habapp_rules.system.config.sleep.SleepItems(
- sleep="Unittest_Sleep",
- sleep_request="Unittest_Sleep_Request",
- lock="Unittest_Lock",
- lock_request="Unittest_Lock_Request",
- display_text="Unittest_Display_Text",
- state="CustomState"
- )
- )
-
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- tests.helper.oh_item.set_state("Unittest_Sleep_Request", test_case.sleep_request_state)
- tests.helper.oh_item.set_state("Unittest_Lock_Request", test_case.lock_request_state)
-
- sleep = habapp_rules.system.sleep.Sleep(config)
-
- self.assertEqual(sleep.sleep_request_active, test_case.sleep_request_state == "ON", test_case)
- self.assertEqual(sleep.lock_request_active, test_case.lock_request_state == "ON", test_case)
- tests.helper.oh_item.assert_value("Unittest_Sleep", test_case.sleep_request_state)
- tests.helper.oh_item.assert_value("Unittest_Lock", test_case.lock_state)
-
- def test_get_initial_state(self):
- """Test getting initial state."""
- TestCase = collections.namedtuple("TestCase", "sleep_request, lock_request, expected_state")
-
- test_cases = [
- TestCase(sleep_request="OFF", lock_request="OFF", expected_state="awake"),
- TestCase(sleep_request="OFF", lock_request="ON", expected_state="locked"),
- TestCase(sleep_request="ON", lock_request="OFF", expected_state="sleeping"),
- TestCase(sleep_request="ON", lock_request="ON", expected_state="sleeping"),
-
- TestCase(sleep_request=None, lock_request="ON", expected_state="locked"),
- TestCase(sleep_request=None, lock_request="OFF", expected_state="default"),
- TestCase(sleep_request="ON", lock_request=None, expected_state="sleeping"),
- TestCase(sleep_request="OFF", lock_request=None, expected_state="awake"),
-
- TestCase(sleep_request=None, lock_request=None, expected_state="default")
- ]
-
- for test_case in test_cases:
- tests.helper.oh_item.set_state("Unittest_Sleep_Request", test_case.sleep_request)
- tests.helper.oh_item.set_state("Unittest_Lock_Request", test_case.lock_request)
-
- self.assertEqual(self._sleep._get_initial_state("default"), test_case.expected_state, test_case)
-
- def test__get_display_text(self):
- """Test getting display text."""
- TestCase = collections.namedtuple("TestCase", "state, text")
- test_cases = [
- TestCase("awake", "Schlafen"),
- TestCase("pre_sleeping", "Guten Schlaf"),
- TestCase("sleeping", "Aufstehen"),
- TestCase("post_sleeping", "Guten Morgen"),
- TestCase("locked", "Gesperrt"),
- TestCase(None, "")
- ]
-
- for test_case in test_cases:
- self._sleep.state = test_case.state
- self.assertEqual(test_case.text, self._sleep._Sleep__get_display_text())
-
- def test_normal_cycle_all_items(self):
- """Test normal behavior with all items available."""
- # check initial state
- tests.helper.oh_item.assert_value("CustomState", "awake")
-
- # start sleeping
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- self.assertEqual(self._sleep.state, "pre_sleeping")
- tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
- tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
- tests.helper.oh_item.assert_value("Unittest_Display_Text", "Guten Schlaf")
- self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
-
- # pre_sleeping timeout -> sleep
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._sleep.state, "sleeping")
- tests.helper.oh_item.assert_value("CustomState", "sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
- tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Display_Text", "Aufstehen")
-
- # stop sleeping
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
- self.assertEqual(self._sleep.state, "post_sleeping")
- tests.helper.oh_item.assert_value("CustomState", "post_sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
- tests.helper.oh_item.assert_value("Unittest_Display_Text", "Guten Morgen")
- self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
-
- # post_sleeping check if sleep change is ignored
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- self.assertEqual(self._sleep.state, "post_sleeping")
-
- # post_sleeping timeout -> awake
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(self._sleep.state, "awake")
- tests.helper.oh_item.assert_value("CustomState", "awake")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Display_Text", "Schlafen")
-
- def test_lock_transitions(self):
- """Test all transitions from and to locked state."""
- # check correct initial state
- tests.helper.oh_item.assert_value("CustomState", "awake")
- tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
-
- # set lock_request. expected: locked state, lock active, sleep off
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
- tests.helper.oh_item.assert_value("CustomState", "locked")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
-
- # release lock and come back to awake state
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
- tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "awake")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
-
- # set lock_request and shortly after send sleep request -> locked expected
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Sleep_Request", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
- tests.helper.oh_item.assert_value("CustomState", "locked")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
-
- # release lock and jump back to awake
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
- tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "awake")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
-
- # start sleeping
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
-
- # activate lock, remove sleep request and wait all timer -> expected state == locked
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- tests.helper.oh_item.assert_value("CustomState", "sleeping")
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
- tests.helper.oh_item.assert_value("CustomState", "post_sleeping")
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- tests.helper.oh_item.assert_value("CustomState", "locked")
-
- # go back to pre_sleeping and check lock + end sleep in pre_sleeping -> expected state = locked
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "locked")
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
- tests.helper.oh_item.assert_value("CustomState", "awake")
- tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
- tests.helper.oh_item.assert_value("CustomState", "locked")
-
- def test_minimal_items(self):
- """Test Sleeping class with minimal set of items."""
- # delete sleep rule from init
- self._runner.loaded_rules[0]._habapp_ctx.unload_rule()
- self._runner.loaded_rules.clear()
-
- tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Lock")
- tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Lock_Request")
- tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Display_Text")
-
- config = habapp_rules.system.config.sleep.SleepConfig(
- items=habapp_rules.system.config.sleep.SleepItems(
- sleep="Unittest_Sleep",
- sleep_request="Unittest_Sleep_Request",
- state="H_Sleep_Unittest_Sleep_state",
- )
- )
-
- sleep = habapp_rules.system.sleep.Sleep(config)
-
- self.assertIsNone(sleep._config.items.display_text)
- self.assertIsNone(sleep._config.items.lock)
- self.assertIsNone(sleep._config.items.lock_request)
-
- # check initial state
- tests.helper.oh_item.assert_value("CustomState", "awake")
-
- # start sleeping
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
- self.assertEqual(sleep.state, "pre_sleeping")
- tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "pre_sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
- self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
-
- # pre_sleeping timeout -> sleep
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(sleep.state, "sleeping")
- tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
-
- # stop sleeping
- self.transitions_timer_mock.reset_mock()
- tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
- self.assertEqual(sleep.state, "post_sleeping")
- tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "post_sleeping")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
- self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
-
- # post_sleeping timeout -> awake
- tests.helper.timer.call_timeout(self.transitions_timer_mock)
- self.assertEqual(sleep.state, "awake")
- tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "awake")
- tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
-
-
-# pylint: disable=no-member
+ """Tests cases for testing presence rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBaseStateMachine.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep_Request", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Lock", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Lock_Request", "OFF")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "Unittest_Display_Text", "")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "H_Sleep_Unittest_Sleep_state", "")
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.StringItem, "CustomState", "")
+
+ config = habapp_rules.system.config.sleep.SleepConfig(
+ items=habapp_rules.system.config.sleep.SleepItems(sleep="Unittest_Sleep", sleep_request="Unittest_Sleep_Request", lock="Unittest_Lock", lock_request="Unittest_Lock_Request", display_text="Unittest_Display_Text", state="CustomState")
+ )
+
+ self._sleep = habapp_rules.system.sleep.Sleep(config)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Sleep", None)
+ tests.helper.oh_item.set_state("Unittest_Sleep_Request", None)
+ tests.helper.oh_item.set_state("Unittest_Lock", None)
+ tests.helper.oh_item.set_state("Unittest_Lock_Request", None)
+ tests.helper.oh_item.set_state("Unittest_Display_Text", None)
+ tests.helper.oh_item.set_state("CustomState", None)
+
+ config = habapp_rules.system.config.sleep.SleepConfig(
+ items=habapp_rules.system.config.sleep.SleepItems(sleep="Unittest_Sleep", sleep_request="Unittest_Sleep_Request", lock="Unittest_Lock", lock_request="Unittest_Lock_Request", display_text="Unittest_Display_Text", state="CustomState")
+ )
+
+ habapp_rules.system.sleep.Sleep(config)
+
+ @unittest.skipIf(sys.platform != "win32", "Should only run on windows when graphviz is installed")
+ def test_create_graph(self) -> None: # pragma: no cover
+ """Create state machine graph for documentation."""
+ presence_graph = tests.helper.graph_machines.GraphMachineTimer(model=self._sleep, states=self._sleep.states, transitions=self._sleep.trans, initial=self._sleep.state, show_conditions=True)
+
+ picture_dir = pathlib.Path(__file__).parent / "_state_charts" / "Sleep"
+ if not picture_dir.is_dir():
+ picture_dir.mkdir(parents=True)
+ presence_graph.get_graph().draw(picture_dir / "Sleep.png", format="png", prog="dot")
+
+ def test_enums(self) -> None:
+ """Test if all enums from __init__.py are implemented."""
+ implemented_states = list(self._sleep.state_machine.states)
+ enum_states = [state.value for state in habapp_rules.system.SleepState] + ["initial"]
+ self.assertEqual(len(enum_states), len(implemented_states))
+ self.assertTrue(all(state in enum_states for state in implemented_states))
+
+ def test__init__(self) -> None:
+ """Test init of sleep class."""
+ TestCase = collections.namedtuple("TestCase", "sleep_request_state, lock_request_state, lock_state")
+
+ test_cases = [
+ TestCase("OFF", "OFF", "OFF"),
+ TestCase("OFF", "ON", "ON"),
+ TestCase("ON", "OFF", "OFF"),
+ TestCase("ON", "ON", "OFF"),
+ ]
+
+ config = habapp_rules.system.config.sleep.SleepConfig(
+ items=habapp_rules.system.config.sleep.SleepItems(sleep="Unittest_Sleep", sleep_request="Unittest_Sleep_Request", lock="Unittest_Lock", lock_request="Unittest_Lock_Request", display_text="Unittest_Display_Text", state="CustomState")
+ )
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ tests.helper.oh_item.set_state("Unittest_Sleep_Request", test_case.sleep_request_state)
+ tests.helper.oh_item.set_state("Unittest_Lock_Request", test_case.lock_request_state)
+
+ sleep = habapp_rules.system.sleep.Sleep(config)
+
+ self.assertEqual(sleep.sleep_request_active, test_case.sleep_request_state == "ON", test_case)
+ self.assertEqual(sleep.lock_request_active, test_case.lock_request_state == "ON", test_case)
+ tests.helper.oh_item.assert_value("Unittest_Sleep", test_case.sleep_request_state)
+ tests.helper.oh_item.assert_value("Unittest_Lock", test_case.lock_state)
+
+ def test_get_initial_state(self) -> None:
+ """Test getting initial state."""
+ TestCase = collections.namedtuple("TestCase", "sleep_request, lock_request, expected_state")
+
+ test_cases = [
+ TestCase(sleep_request="OFF", lock_request="OFF", expected_state="awake"),
+ TestCase(sleep_request="OFF", lock_request="ON", expected_state="locked"),
+ TestCase(sleep_request="ON", lock_request="OFF", expected_state="sleeping"),
+ TestCase(sleep_request="ON", lock_request="ON", expected_state="sleeping"),
+ TestCase(sleep_request=None, lock_request="ON", expected_state="locked"),
+ TestCase(sleep_request=None, lock_request="OFF", expected_state="default"),
+ TestCase(sleep_request="ON", lock_request=None, expected_state="sleeping"),
+ TestCase(sleep_request="OFF", lock_request=None, expected_state="awake"),
+ TestCase(sleep_request=None, lock_request=None, expected_state="default"),
+ ]
+
+ for test_case in test_cases:
+ tests.helper.oh_item.set_state("Unittest_Sleep_Request", test_case.sleep_request)
+ tests.helper.oh_item.set_state("Unittest_Lock_Request", test_case.lock_request)
+
+ self.assertEqual(self._sleep._get_initial_state("default"), test_case.expected_state, test_case)
+
+ def test__get_display_text(self) -> None:
+ """Test getting display text."""
+ TestCase = collections.namedtuple("TestCase", "state, text")
+ test_cases = [TestCase("awake", "Schlafen"), TestCase("pre_sleeping", "Guten Schlaf"), TestCase("sleeping", "Aufstehen"), TestCase("post_sleeping", "Guten Morgen"), TestCase("locked", "Gesperrt"), TestCase(None, "")]
+
+ for test_case in test_cases:
+ self._sleep.state = test_case.state
+ self.assertEqual(test_case.text, self._sleep._Sleep__get_display_text())
+
+ def test_normal_cycle_all_items(self) -> None:
+ """Test normal behavior with all items available."""
+ # check initial state
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+
+ # start sleeping
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ self.assertEqual(self._sleep.state, "pre_sleeping")
+ tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Display_Text", "Guten Schlaf")
+ self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
+
+ # pre_sleeping timeout -> sleep
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._sleep.state, "sleeping")
+ tests.helper.oh_item.assert_value("CustomState", "sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Display_Text", "Aufstehen")
+
+ # stop sleeping
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
+ self.assertEqual(self._sleep.state, "post_sleeping")
+ tests.helper.oh_item.assert_value("CustomState", "post_sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Display_Text", "Guten Morgen")
+ self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
+
+ # post_sleeping check if sleep change is ignored
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ self.assertEqual(self._sleep.state, "post_sleeping")
+
+ # post_sleeping timeout -> awake
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(self._sleep.state, "awake")
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Display_Text", "Schlafen")
+
+ def test_lock_transitions(self) -> None:
+ """Test all transitions from and to locked state."""
+ # check correct initial state
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
+
+ # set lock_request. expected: locked state, lock active, sleep off
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
+ tests.helper.oh_item.assert_value("CustomState", "locked")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+
+ # release lock and come back to awake state
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+
+ # set lock_request and shortly after send sleep request -> locked expected
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Sleep_Request", "OFF")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "ON")
+ tests.helper.oh_item.assert_value("CustomState", "locked")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+
+ # release lock and jump back to awake
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Lock", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+
+ # start sleeping
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
+
+ # activate lock, remove sleep request and wait all timer -> expected state == locked
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "pre_sleeping")
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ tests.helper.oh_item.assert_value("CustomState", "sleeping")
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
+ tests.helper.oh_item.assert_value("CustomState", "post_sleeping")
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ tests.helper.oh_item.assert_value("CustomState", "locked")
+
+ # go back to pre_sleeping and check lock + end sleep in pre_sleeping -> expected state = locked
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "locked")
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "OFF", "ON")
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+ tests.helper.oh_item.send_command("Unittest_Lock_Request", "ON", "OFF")
+ tests.helper.oh_item.assert_value("CustomState", "locked")
+
+ def test_minimal_items(self) -> None:
+ """Test Sleeping class with minimal set of items."""
+ # delete sleep rule from init
+ self._runner.loaded_rules[0]._habapp_ctx.unload_rule()
+ self._runner.loaded_rules.clear()
+
+ tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Lock")
+ tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Lock_Request")
+ tests.helper.oh_item.remove_mocked_item_by_name("Unittest_Display_Text")
+
+ config = habapp_rules.system.config.sleep.SleepConfig(
+ items=habapp_rules.system.config.sleep.SleepItems(
+ sleep="Unittest_Sleep",
+ sleep_request="Unittest_Sleep_Request",
+ state="H_Sleep_Unittest_Sleep_state",
+ )
+ )
+
+ sleep = habapp_rules.system.sleep.Sleep(config)
+
+ self.assertIsNone(sleep._config.items.display_text)
+ self.assertIsNone(sleep._config.items.lock)
+ self.assertIsNone(sleep._config.items.lock_request)
+
+ # check initial state
+ tests.helper.oh_item.assert_value("CustomState", "awake")
+
+ # start sleeping
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "ON", "OFF")
+ self.assertEqual(sleep.state, "pre_sleeping")
+ tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "pre_sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
+ self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
+
+ # pre_sleeping timeout -> sleep
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(sleep.state, "sleeping")
+ tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "ON")
+
+ # stop sleeping
+ self.transitions_timer_mock.reset_mock()
+ tests.helper.oh_item.send_command("Unittest_Sleep_Request", "OFF", "ON")
+ self.assertEqual(sleep.state, "post_sleeping")
+ tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "post_sleeping")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+ self.transitions_timer_mock.assert_called_with(3, unittest.mock.ANY, args=unittest.mock.ANY)
+
+ # post_sleeping timeout -> awake
+ tests.helper.timer.call_timeout(self.transitions_timer_mock)
+ self.assertEqual(sleep.state, "awake")
+ tests.helper.oh_item.assert_value("H_Sleep_Unittest_Sleep_state", "awake")
+ tests.helper.oh_item.assert_value("Unittest_Sleep", "OFF")
+
+
class TestLinkSleep(tests.helper.test_case_base.TestCaseBase):
- """Test LinkSleep."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep1", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep2_req", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep3_req", None)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep4", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep5_req", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep6_req", None)
-
- config_full_day = habapp_rules.system.config.sleep.LinkSleepConfig(
- items=habapp_rules.system.config.sleep.LinkSleepItems(
- sleep_master="Unittest_Sleep1",
- sleep_request_slaves=["Unittest_Sleep2_req", "Unittest_Sleep3_req"],
- )
- )
-
- config_night = habapp_rules.system.config.sleep.LinkSleepConfig(
- items=habapp_rules.system.config.sleep.LinkSleepItems(
- sleep_master="Unittest_Sleep4",
- sleep_request_slaves=["Unittest_Sleep5_req", "Unittest_Sleep6_req"],
- ),
- parameter=habapp_rules.system.config.sleep.LinkSleepParameter(
- start_time=datetime.time(22),
- end_time=datetime.time(10),
- )
- )
-
- self._link_full_day = habapp_rules.system.sleep.LinkSleep(config_full_day)
- self._link_night = habapp_rules.system.sleep.LinkSleep(config_night)
-
- def test_init_with_feedback(self):
- """Test init with feedback item"""
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Link_Active", None)
- config = habapp_rules.system.config.sleep.LinkSleepConfig(
- items=habapp_rules.system.config.sleep.LinkSleepItems(
- sleep_master="Unittest_Sleep1",
- sleep_request_slaves=["Unittest_Sleep2_req", "Unittest_Sleep3_req"],
- link_active_feedback="Unittest_Link_Active",
- )
- )
-
- rule = habapp_rules.system.sleep.LinkSleep(config)
-
- self.assertEqual("Unittest_Link_Active", rule._config.items.link_active_feedback.name)
-
- def test_check_time_in_window(self):
- """test check_time_in_window."""
- TestCase = collections.namedtuple("TestCase", "start, end, now, expected_result")
-
- test_cases = [
- # full day
- TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(0, 0), True),
- TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(12), True),
- TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(23, 59), True),
-
- # range during day
- TestCase(datetime.time(10), datetime.time(16), datetime.time(0, 0), False),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(9, 59), False),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(10), True),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(10, 1), True),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(15, 59), True),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(16), True),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(16, 1), False),
- TestCase(datetime.time(10), datetime.time(16), datetime.time(23, 59), False),
-
- # range over midnight day
- TestCase(datetime.time(22), datetime.time(5), datetime.time(12), False),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(21, 59), False),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(22), True),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(22, 1), True),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(0), True),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(4, 59), True),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(5), True),
- TestCase(datetime.time(22), datetime.time(5), datetime.time(5, 1), False),
- ]
-
- with unittest.mock.patch("datetime.datetime") as datetime_mock:
- now_mock = unittest.mock.MagicMock()
- datetime_mock.now.return_value = now_mock
- for test_case in test_cases:
- with self.subTest(test_case=test_case):
- now_mock.time.return_value = test_case.now
-
- self._link_full_day._config.parameter.link_time_start = test_case.start
- self._link_full_day._config.parameter.link_time_end = test_case.end
-
- self.assertEqual(test_case.expected_result, self._link_full_day._check_time_in_window())
-
- def test_cb_master(self):
- """Test _cb_master"""
- # during active time
- with unittest.mock.patch.object(self._link_full_day, "_check_time_in_window", return_value=True):
- tests.helper.oh_item.assert_value("Unittest_Sleep2_req", None)
- tests.helper.oh_item.assert_value("Unittest_Sleep3_req", None)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep1", "ON")
- tests.helper.oh_item.assert_value("Unittest_Sleep2_req", "ON")
- tests.helper.oh_item.assert_value("Unittest_Sleep3_req", "ON")
-
- # during inactive time
- with unittest.mock.patch.object(self._link_night, "_check_time_in_window", return_value=False):
- tests.helper.oh_item.assert_value("Unittest_Sleep5_req", None)
- tests.helper.oh_item.assert_value("Unittest_Sleep6_req", None)
-
- tests.helper.oh_item.item_state_change_event("Unittest_Sleep4", "ON")
- tests.helper.oh_item.assert_value("Unittest_Sleep5_req", None)
- tests.helper.oh_item.assert_value("Unittest_Sleep6_req", None)
-
- def test_set_link_active_feedback(self):
- """Test _set_link_active_feedback."""
- with unittest.mock.patch.object(self._link_full_day._config.items, "link_active_feedback") as item_link_active_mock:
- self._link_full_day._set_link_active_feedback("ON")
- item_link_active_mock.oh_send_command.assert_called_once_with("ON")
-
- with unittest.mock.patch.object(self._link_full_day._config.items, "link_active_feedback") as item_link_active_mock:
- self._link_full_day._set_link_active_feedback("OFF")
- item_link_active_mock.oh_send_command.assert_called_once_with("OFF")
+ """Test LinkSleep."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep2_req", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep3_req", None)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep4", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep5_req", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Sleep6_req", None)
+
+ config_full_day = habapp_rules.system.config.sleep.LinkSleepConfig(
+ items=habapp_rules.system.config.sleep.LinkSleepItems(
+ sleep_master="Unittest_Sleep1",
+ sleep_request_slaves=["Unittest_Sleep2_req", "Unittest_Sleep3_req"],
+ )
+ )
+
+ config_night = habapp_rules.system.config.sleep.LinkSleepConfig(
+ items=habapp_rules.system.config.sleep.LinkSleepItems(
+ sleep_master="Unittest_Sleep4",
+ sleep_request_slaves=["Unittest_Sleep5_req", "Unittest_Sleep6_req"],
+ ),
+ parameter=habapp_rules.system.config.sleep.LinkSleepParameter(
+ start_time=datetime.time(22),
+ end_time=datetime.time(10),
+ ),
+ )
+
+ self._link_full_day = habapp_rules.system.sleep.LinkSleep(config_full_day)
+ self._link_night = habapp_rules.system.sleep.LinkSleep(config_night)
+
+ def test_init_with_feedback(self) -> None:
+ """Test init with feedback item."""
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Link_Active", None)
+ config = habapp_rules.system.config.sleep.LinkSleepConfig(
+ items=habapp_rules.system.config.sleep.LinkSleepItems(
+ sleep_master="Unittest_Sleep1",
+ sleep_request_slaves=["Unittest_Sleep2_req", "Unittest_Sleep3_req"],
+ link_active_feedback="Unittest_Link_Active",
+ )
+ )
+
+ rule = habapp_rules.system.sleep.LinkSleep(config)
+
+ self.assertEqual("Unittest_Link_Active", rule._config.items.link_active_feedback.name)
+
+ def test_check_time_in_window(self) -> None:
+ """Test check_time_in_window."""
+ TestCase = collections.namedtuple("TestCase", "start, end, now, expected_result")
+
+ test_cases = [
+ # full day
+ TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(0, 0), True),
+ TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(12), True),
+ TestCase(datetime.time(0), datetime.time(23, 59), datetime.time(23, 59), True),
+ # range during day
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(0, 0), False),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(9, 59), False),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(10), True),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(10, 1), True),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(15, 59), True),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(16), True),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(16, 1), False),
+ TestCase(datetime.time(10), datetime.time(16), datetime.time(23, 59), False),
+ # range over midnight day
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(12), False),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(21, 59), False),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(22), True),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(22, 1), True),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(0), True),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(4, 59), True),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(5), True),
+ TestCase(datetime.time(22), datetime.time(5), datetime.time(5, 1), False),
+ ]
+
+ with unittest.mock.patch("datetime.datetime") as datetime_mock:
+ now_mock = unittest.mock.MagicMock()
+ datetime_mock.now.return_value = now_mock
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ now_mock.time.return_value = test_case.now
+
+ self._link_full_day._config.parameter.link_time_start = test_case.start
+ self._link_full_day._config.parameter.link_time_end = test_case.end
+
+ self.assertEqual(test_case.expected_result, self._link_full_day._check_time_in_window())
+
+ def test_cb_master(self) -> None:
+ """Test _cb_master."""
+ # during active time
+ with unittest.mock.patch.object(self._link_full_day, "_check_time_in_window", return_value=True):
+ tests.helper.oh_item.assert_value("Unittest_Sleep2_req", None)
+ tests.helper.oh_item.assert_value("Unittest_Sleep3_req", None)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep1", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Sleep2_req", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Sleep3_req", "ON")
+
+ # during inactive time
+ with unittest.mock.patch.object(self._link_night, "_check_time_in_window", return_value=False):
+ tests.helper.oh_item.assert_value("Unittest_Sleep5_req", None)
+ tests.helper.oh_item.assert_value("Unittest_Sleep6_req", None)
+
+ tests.helper.oh_item.item_state_change_event("Unittest_Sleep4", "ON")
+ tests.helper.oh_item.assert_value("Unittest_Sleep5_req", None)
+ tests.helper.oh_item.assert_value("Unittest_Sleep6_req", None)
+
+ def test_set_link_active_feedback(self) -> None:
+ """Test _set_link_active_feedback."""
+ with unittest.mock.patch.object(self._link_full_day._config.items, "link_active_feedback") as item_link_active_mock:
+ self._link_full_day._set_link_active_feedback("ON")
+ item_link_active_mock.oh_send_command.assert_called_once_with("ON")
+
+ with unittest.mock.patch.object(self._link_full_day._config.items, "link_active_feedback") as item_link_active_mock:
+ self._link_full_day._set_link_active_feedback("OFF")
+ item_link_active_mock.oh_send_command.assert_called_once_with("OFF")
diff --git a/tests/system/summer_winter.py b/tests/system/summer_winter.py
index c9b1bd8..044f265 100644
--- a/tests/system/summer_winter.py
+++ b/tests/system/summer_winter.py
@@ -1,4 +1,5 @@
"""Tests for SummerWinter Rule."""
+
import collections
import datetime
import logging
@@ -14,200 +15,192 @@
import tests.helper.test_case_base
-# pylint: disable=protected-access
class TestSummerWinter(tests.helper.test_case_base.TestCaseBase):
- """Tests for SummerWinter Rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature", 0)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
-
- config = habapp_rules.system.config.summer_winter.SummerWinterConfig(
- items=habapp_rules.system.config.summer_winter.SummerWinterItems(
- outside_temperature="Unittest_Temperature",
- summer="Unittest_Summer"
- )
- )
-
- self._summer_winter = habapp_rules.system.summer_winter.SummerWinter(config)
-
- def test_init_with_none(self):
- """Test __init__ with None values."""
- tests.helper.oh_item.set_state("Unittest_Temperature", None)
- tests.helper.oh_item.set_state("Unittest_Summer", None)
-
- config = habapp_rules.system.config.summer_winter.SummerWinterConfig(
- items=habapp_rules.system.config.summer_winter.SummerWinterItems(
- outside_temperature="Unittest_Temperature",
- summer="Unittest_Summer"
- )
- )
-
- habapp_rules.system.summer_winter.SummerWinter(config)
-
- def test__get_weighted_mean(self):
- """Test normal function of wighted_mean"""
- self._summer_winter._config.parameter.persistence_service = "persist_name"
- TestCase = collections.namedtuple("TestCase", "now, expected_day, temperatures, expected_mean")
-
- test_cases = [
- TestCase(now=datetime.datetime(2050, 1, 1, 17), expected_day=datetime.datetime(2049, 12, 31), temperatures=[[8], [18], [14]], expected_mean=13),
- TestCase(now=datetime.datetime(2050, 1, 1, 23), expected_day=datetime.datetime(2050, 1, 1), temperatures=[[8, 99], [18, 100, 100], [14]], expected_mean=13),
- TestCase(now=datetime.datetime(2050, 1, 1, 22, 59), expected_day=datetime.datetime(2049, 12, 31), temperatures=[[8], [18], [14]], expected_mean=13),
- ]
-
- with unittest.mock.patch.object(self._summer_winter._config.items, "outside_temperature", spec=HABApp.openhab.items.NumberItem) as outside_temp_mock:
- for test_case in test_cases:
- outside_temp_mock.get_persistence_data.reset_mock()
-
- # set current time
- self._summer_winter._SummerWinter__now = test_case.now
-
- # get historical temperatures as HABApp type and set the return to the mock item
- history_temperatures = []
- for temp_list in test_case.temperatures:
- temp_history = HABApp.openhab.definitions.rest.persistence.ItemHistoryResp(name="some_name", data=[])
- for idx, temp in enumerate(temp_list):
- temp_history.data.append(HABApp.openhab.definitions.rest.persistence.DataPoint(time=idx * 123456, state=str(temp)))
- history_temperatures.append(HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData.from_resp(temp_history))
- outside_temp_mock.get_persistence_data.side_effect = history_temperatures
-
- # call weighted mean and check if result is the expected mean temperature
- self.assertTrue(self._summer_winter._SummerWinter__get_weighted_mean(0), test_case.expected_mean)
-
- # check if call of get_persistence_data was correct
- self.assertEqual(outside_temp_mock.get_persistence_data.call_count, 3)
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=7), end_time=test_case.expected_day + datetime.timedelta(hours=8))
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=14), end_time=test_case.expected_day + datetime.timedelta(hours=15))
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=22), end_time=test_case.expected_day + datetime.timedelta(hours=23))
-
- # call with days_in_past = 2
- outside_temp_mock.get_persistence_data.side_effect = history_temperatures
- self._summer_winter._SummerWinter__get_weighted_mean(2)
-
- # check if call of get_persistence_data was correct
- self.assertEqual(outside_temp_mock.get_persistence_data.call_count, 6)
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=7), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=8))
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=14), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=15))
- outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=22), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=23))
-
- def test__get_weighted_mean_exception(self):
- """Test normal function of wighted_mean"""
- with unittest.mock.patch.object(self._summer_winter._config.items, "outside_temperature", spec=HABApp.openhab.items.NumberItem) as outside_temp_mock, self.assertRaises(habapp_rules.system.summer_winter.SummerWinterException) as context:
- outside_temp_mock.get_persistence_data.return_value = HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData.from_resp(HABApp.openhab.definitions.rest.persistence.ItemHistoryResp(name="some_name", data=[]))
- self._summer_winter._SummerWinter__get_weighted_mean(0)
- self.assertIn("No data for", str(context.exception))
-
- def test__is_summer(self):
- """Test if __is_summer method is detecting summer/winter correctly."""
- self._summer_winter._config.parameter.days = 4
- self._summer_winter._SummerWinter__get_weighted_mean = unittest.mock.MagicMock()
-
- # check if __get_wighted_mean was called correctly
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [3, 3.4, 3.6, 4]
- self.assertFalse(self._summer_winter._SummerWinter__is_summer())
- self.assertEqual(self._summer_winter._SummerWinter__get_weighted_mean.call_count, 4)
- self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(0)
- self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(1)
- self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(2)
- self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(3)
-
- # check if summer is returned if greater than threshold
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, 16, 16, 17.1]
- self.assertTrue(self._summer_winter._SummerWinter__is_summer())
-
- # check if winter is returned if smaller / equal than threshold
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, 16, 16, 14]
- self.assertFalse(self._summer_winter._SummerWinter__is_summer())
-
- # check if exceptions are handled correctly (single Exception)
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, habapp_rules.system.summer_winter.SummerWinterException("not found"), 16.1, 18.0]
- self.assertTrue(self._summer_winter._SummerWinter__is_summer())
-
- # check if exceptions are handled correctly (single valid value)
- exc = habapp_rules.system.summer_winter.SummerWinterException("not found")
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [exc, exc, 16.1, exc]
- self.assertTrue(self._summer_winter._SummerWinter__is_summer())
-
- # check if exceptions are handled correctly (no value)
- exc = habapp_rules.system.summer_winter.SummerWinterException("not found")
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [exc, exc, exc, exc]
- with self.assertRaises(habapp_rules.system.summer_winter.SummerWinterException):
- self._summer_winter._SummerWinter__is_summer()
-
- def test__is_summer_with_hysteresis(self):
- """Test summer / winter with hysteresis."""
- TestCase = collections.namedtuple("TestCase", "temperature_values, summer_value, expected_summer")
-
- test_cases = [
- TestCase([15.74] * 5, False, False),
- TestCase([15.75] * 5, False, False),
- TestCase([15.76] * 5, False, False),
- TestCase([16.24] * 5, False, False),
- TestCase([16.25] * 5, False, True),
- TestCase([16.26] * 5, False, True),
-
- TestCase([15.74] * 5, True, False),
- TestCase([15.75] * 5, True, True),
- TestCase([15.76] * 5, True, True),
- TestCase([16.24] * 5, True, True),
- TestCase([16.25] * 5, True, True),
- TestCase([16.26] * 5, True, True),
- ]
-
- self._summer_winter._SummerWinter__get_weighted_mean = unittest.mock.MagicMock()
-
- for test_case in test_cases:
- self._summer_winter._SummerWinter__get_weighted_mean.side_effect = test_case.temperature_values
- self._summer_winter._hysteresis_switch._on_off_state = test_case.summer_value
- self.assertEqual(test_case.expected_summer, self._summer_winter._SummerWinter__is_summer())
-
- def test_cb_update_summer(self):
- """Test correct functionality of summer check callback."""
- with unittest.mock.patch.object(self._summer_winter, "_SummerWinter__is_summer") as is_summer_mock, \
- unittest.mock.patch.object(self._summer_winter._config.items, "last_check", spec=HABApp.openhab.items.datetime_item.DatetimeItem) as last_check_mock:
- # switch from winter to summer
- is_summer_mock.return_value = True
- self._summer_winter._cb_update_summer()
- tests.helper.oh_item.assert_value("Unittest_Summer", "ON")
- self.assertEqual(1, last_check_mock.oh_send_command.call_count)
-
- # already summer
- is_summer_mock.return_value = True
- with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
- summer_item.value = "ON"
- self._summer_winter._cb_update_summer()
- summer_item.oh_send_command.assert_called_once()
- self.assertEqual(2, last_check_mock.oh_send_command.call_count)
-
- # switch back to winter
- is_summer_mock.return_value = False
- self._summer_winter._cb_update_summer()
- tests.helper.oh_item.assert_value("Unittest_Summer", "OFF")
- self.assertEqual(3, last_check_mock.oh_send_command.call_count)
-
- # already winter
- is_summer_mock.return_value = False
- with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
- summer_item.value = "OFF"
- self._summer_winter._cb_update_summer()
- summer_item.oh_send_command.assert_called_once()
- self.assertEqual(4, last_check_mock.oh_send_command.call_count)
-
- # already winter | no last_check item -> send_command should not be triggered
- is_summer_mock.return_value = False
- with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
- self._summer_winter._config.items.last_check = None
- summer_item.value = "OFF"
- self._summer_winter._cb_update_summer()
- summer_item.oh_send_command.assert_called_once()
- self.assertEqual(4, last_check_mock.oh_send_command.call_count)
-
- # exception from __is_summer
- with unittest.mock.patch.object(self._summer_winter, "_SummerWinter__is_summer", side_effect=habapp_rules.system.summer_winter.SummerWinterException("No update")), \
- unittest.mock.patch.object(self._summer_winter, "_instance_logger", spec=logging.Logger) as logger_mock:
- self._summer_winter._cb_update_summer()
- logger_mock.error.assert_called_once()
+ """Tests for SummerWinter Rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Temperature", 0)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Summer", "OFF")
+
+ config = habapp_rules.system.config.summer_winter.SummerWinterConfig(items=habapp_rules.system.config.summer_winter.SummerWinterItems(outside_temperature="Unittest_Temperature", summer="Unittest_Summer"))
+
+ self._summer_winter = habapp_rules.system.summer_winter.SummerWinter(config)
+
+ def test_init_with_none(self) -> None:
+ """Test __init__ with None values."""
+ tests.helper.oh_item.set_state("Unittest_Temperature", None)
+ tests.helper.oh_item.set_state("Unittest_Summer", None)
+
+ config = habapp_rules.system.config.summer_winter.SummerWinterConfig(items=habapp_rules.system.config.summer_winter.SummerWinterItems(outside_temperature="Unittest_Temperature", summer="Unittest_Summer"))
+
+ habapp_rules.system.summer_winter.SummerWinter(config)
+
+ def test__get_weighted_mean(self) -> None:
+ """Test normal function of wighted_mean."""
+ self._summer_winter._config.parameter.persistence_service = "persist_name"
+ TestCase = collections.namedtuple("TestCase", "now, expected_day, temperatures, expected_mean")
+
+ test_cases = [
+ TestCase(now=datetime.datetime(2050, 1, 1, 17), expected_day=datetime.datetime(2049, 12, 31), temperatures=[[8], [18], [14]], expected_mean=13),
+ TestCase(now=datetime.datetime(2050, 1, 1, 23), expected_day=datetime.datetime(2050, 1, 1), temperatures=[[8, 99], [18, 100, 100], [14]], expected_mean=13),
+ TestCase(now=datetime.datetime(2050, 1, 1, 22, 59), expected_day=datetime.datetime(2049, 12, 31), temperatures=[[8], [18], [14]], expected_mean=13),
+ ]
+
+ with unittest.mock.patch.object(self._summer_winter._config.items, "outside_temperature", spec=HABApp.openhab.items.NumberItem) as outside_temp_mock:
+ for test_case in test_cases:
+ outside_temp_mock.get_persistence_data.reset_mock()
+
+ # set current time
+ self._summer_winter._SummerWinter__now = test_case.now
+
+ # get historical temperatures as HABApp type and set the return to the mock item
+ history_temperatures = []
+ for temp_list in test_case.temperatures:
+ temp_history = HABApp.openhab.definitions.rest.persistence.ItemHistoryResp(name="some_name", data=[])
+ for idx, temp in enumerate(temp_list):
+ temp_history.data.append(HABApp.openhab.definitions.rest.persistence.DataPoint(time=idx * 123456, state=str(temp)))
+ history_temperatures.append(HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData.from_resp(temp_history))
+ outside_temp_mock.get_persistence_data.side_effect = history_temperatures
+
+ # call weighted mean and check if result is the expected mean temperature
+ self.assertTrue(self._summer_winter._SummerWinter__get_weighted_mean(0), test_case.expected_mean)
+
+ # check if call of get_persistence_data was correct
+ self.assertEqual(outside_temp_mock.get_persistence_data.call_count, 3)
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=7), end_time=test_case.expected_day + datetime.timedelta(hours=8))
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=14), end_time=test_case.expected_day + datetime.timedelta(hours=15))
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(hours=22), end_time=test_case.expected_day + datetime.timedelta(hours=23))
+
+ # call with days_in_past = 2
+ outside_temp_mock.get_persistence_data.side_effect = history_temperatures
+ self._summer_winter._SummerWinter__get_weighted_mean(2)
+
+ # check if call of get_persistence_data was correct
+ self.assertEqual(outside_temp_mock.get_persistence_data.call_count, 6)
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=7), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=8))
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=14), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=15))
+ outside_temp_mock.get_persistence_data.assert_any_call(persistence="persist_name", start_time=test_case.expected_day + datetime.timedelta(days=-2, hours=22), end_time=test_case.expected_day + datetime.timedelta(days=-2, hours=23))
+
+ def test__get_weighted_mean_exception(self) -> None:
+ """Test normal function of wighted_mean."""
+ with unittest.mock.patch.object(self._summer_winter._config.items, "outside_temperature", spec=HABApp.openhab.items.NumberItem) as outside_temp_mock, self.assertRaises(habapp_rules.system.summer_winter.SummerWinterError) as context:
+ outside_temp_mock.get_persistence_data.return_value = HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData.from_resp(HABApp.openhab.definitions.rest.persistence.ItemHistoryResp(name="some_name", data=[]))
+ self._summer_winter._SummerWinter__get_weighted_mean(0)
+ self.assertIn("No data for", str(context.exception))
+
+ def test__is_summer(self) -> None:
+ """Test if __is_summer method is detecting summer/winter correctly."""
+ self._summer_winter._config.parameter.days = 4
+ self._summer_winter._SummerWinter__get_weighted_mean = unittest.mock.MagicMock()
+
+ # check if __get_wighted_mean was called correctly
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [3, 3.4, 3.6, 4]
+ self.assertFalse(self._summer_winter._SummerWinter__is_summer())
+ self.assertEqual(self._summer_winter._SummerWinter__get_weighted_mean.call_count, 4)
+ self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(0)
+ self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(1)
+ self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(2)
+ self._summer_winter._SummerWinter__get_weighted_mean.assert_any_call(3)
+
+ # check if summer is returned if greater than threshold
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, 16, 16, 17.1]
+ self.assertTrue(self._summer_winter._SummerWinter__is_summer())
+
+ # check if winter is returned if smaller / equal than threshold
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, 16, 16, 14]
+ self.assertFalse(self._summer_winter._SummerWinter__is_summer())
+
+ # check if exceptions are handled correctly (single Exception)
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [16, habapp_rules.system.summer_winter.SummerWinterError("not found"), 16.1, 18.0]
+ self.assertTrue(self._summer_winter._SummerWinter__is_summer())
+
+ # check if exceptions are handled correctly (single valid value)
+ exc = habapp_rules.system.summer_winter.SummerWinterError("not found")
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [exc, exc, 16.1, exc]
+ self.assertTrue(self._summer_winter._SummerWinter__is_summer())
+
+ # check if exceptions are handled correctly (no value)
+ exc = habapp_rules.system.summer_winter.SummerWinterError("not found")
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = [exc, exc, exc, exc]
+ with self.assertRaises(habapp_rules.system.summer_winter.SummerWinterError):
+ self._summer_winter._SummerWinter__is_summer()
+
+ def test__is_summer_with_hysteresis(self) -> None:
+ """Test summer / winter with hysteresis."""
+ TestCase = collections.namedtuple("TestCase", "temperature_values, summer_value, expected_summer")
+
+ test_cases = [
+ TestCase([15.74] * 5, False, False),
+ TestCase([15.75] * 5, False, False),
+ TestCase([15.76] * 5, False, False),
+ TestCase([16.24] * 5, False, False),
+ TestCase([16.25] * 5, False, True),
+ TestCase([16.26] * 5, False, True),
+ TestCase([15.74] * 5, True, False),
+ TestCase([15.75] * 5, True, True),
+ TestCase([15.76] * 5, True, True),
+ TestCase([16.24] * 5, True, True),
+ TestCase([16.25] * 5, True, True),
+ TestCase([16.26] * 5, True, True),
+ ]
+
+ self._summer_winter._SummerWinter__get_weighted_mean = unittest.mock.MagicMock()
+
+ for test_case in test_cases:
+ self._summer_winter._SummerWinter__get_weighted_mean.side_effect = test_case.temperature_values
+ self._summer_winter._hysteresis_switch._on_off_state = test_case.summer_value
+ self.assertEqual(test_case.expected_summer, self._summer_winter._SummerWinter__is_summer())
+
+ def test_cb_update_summer(self) -> None:
+ """Test correct functionality of summer check callback."""
+ with (
+ unittest.mock.patch.object(self._summer_winter, "_SummerWinter__is_summer") as is_summer_mock,
+ unittest.mock.patch.object(self._summer_winter._config.items, "last_check", spec=HABApp.openhab.items.datetime_item.DatetimeItem) as last_check_mock,
+ ):
+ # switch from winter to summer
+ is_summer_mock.return_value = True
+ self._summer_winter._cb_update_summer()
+ tests.helper.oh_item.assert_value("Unittest_Summer", "ON")
+ self.assertEqual(1, last_check_mock.oh_send_command.call_count)
+
+ # already summer
+ is_summer_mock.return_value = True
+ with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
+ summer_item.value = "ON"
+ self._summer_winter._cb_update_summer()
+ summer_item.oh_send_command.assert_called_once()
+ self.assertEqual(2, last_check_mock.oh_send_command.call_count)
+
+ # switch back to winter
+ is_summer_mock.return_value = False
+ self._summer_winter._cb_update_summer()
+ tests.helper.oh_item.assert_value("Unittest_Summer", "OFF")
+ self.assertEqual(3, last_check_mock.oh_send_command.call_count)
+
+ # already winter
+ is_summer_mock.return_value = False
+ with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
+ summer_item.value = "OFF"
+ self._summer_winter._cb_update_summer()
+ summer_item.oh_send_command.assert_called_once()
+ self.assertEqual(4, last_check_mock.oh_send_command.call_count)
+
+ # already winter | no last_check item -> send_command should not be triggered
+ is_summer_mock.return_value = False
+ with unittest.mock.patch.object(self._summer_winter._config.items, "summer") as summer_item:
+ self._summer_winter._config.items.last_check = None
+ summer_item.value = "OFF"
+ self._summer_winter._cb_update_summer()
+ summer_item.oh_send_command.assert_called_once()
+ self.assertEqual(4, last_check_mock.oh_send_command.call_count)
+
+ # exception from __is_summer
+ with (
+ unittest.mock.patch.object(self._summer_winter, "_SummerWinter__is_summer", side_effect=habapp_rules.system.summer_winter.SummerWinterError("No update")),
+ unittest.mock.patch.object(self._summer_winter, "_instance_logger", spec=logging.Logger) as logger_mock,
+ ):
+ self._summer_winter._cb_update_summer()
+ logger_mock.exception.assert_called_once()
diff --git a/tests/system/watchdog.py b/tests/system/watchdog.py
deleted file mode 100644
index b840306..0000000
--- a/tests/system/watchdog.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""Tests for Watchdog Rule."""
-import unittest.mock
-
-import HABApp.openhab.items
-
-import habapp_rules.system.watchdog
-import tests.helper.oh_item
-import tests.helper.test_case_base
-from habapp_rules.system.config.watchdog import WatchdogConfig, WatchdogItems, WatchdogParameter
-
-
-class TestWatchdog(tests.helper.test_case_base.TestCaseBase):
- """Tests for Watchdog Rule."""
-
- def setUp(self) -> None:
- """Setup test case."""
- tests.helper.test_case_base.TestCaseBase.setUp(self)
-
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Unittest_Number", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Number_Warning", None)
- tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Unittest_Switch_Warning", None)
-
- self._watchdog_number = habapp_rules.system.watchdog.Watchdog(WatchdogConfig(
- items=WatchdogItems(
- observed="Unittest_Number",
- warning="Unittest_Number_Warning"
- )
- ))
-
- self._watchdog_switch = habapp_rules.system.watchdog.Watchdog(WatchdogConfig(
- items=WatchdogItems(
- observed="Unittest_Switch",
- warning="Unittest_Switch_Warning"
- ),
- parameter=WatchdogParameter(
- timeout=10
- )
- ))
-
- def test_cb_observed_state_updated(self):
- """Callback which is called if the observed item was updated."""
-
- with unittest.mock.patch.object(self._watchdog_number, "_countdown") as number_countdown_mock, unittest.mock.patch.object(self._watchdog_switch, "_countdown") as switch_countdown_mock:
- tests.helper.oh_item.item_state_event("Unittest_Number", 42)
- number_countdown_mock.reset.assert_called_once()
- switch_countdown_mock.reset.assert_not_called()
- tests.helper.oh_item.assert_value("Unittest_Number_Warning", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Switch_Warning", None)
-
- tests.helper.oh_item.item_state_event("Unittest_Switch", "OFF")
- number_countdown_mock.reset.assert_called_once()
- switch_countdown_mock.reset.assert_called_once()
- tests.helper.oh_item.assert_value("Unittest_Number_Warning", "OFF")
- tests.helper.oh_item.assert_value("Unittest_Switch_Warning", "OFF")
diff --git a/version_check.py b/version_check.py
new file mode 100644
index 0000000..37c6407
--- /dev/null
+++ b/version_check.py
@@ -0,0 +1,103 @@
+import json
+import logging
+import pathlib
+import sys
+
+import requests
+
+import habapp_rules
+
+POSITIVE_RESPONSE_CODE = 200
+VERSION_LENGTH = 3
+
+logging.basicConfig(
+ level=logging.INFO, # Set the logging level (INFO, DEBUG, etc.)
+ format="%(levelname)s: %(message)s", # Log message format
+ handlers=[logging.StreamHandler(sys.stdout)], # Use sys.stdout for logs
+)
+
+LOGGER = logging.getLogger(__name__)
+
+
+class VersionCheck:
+ """Checker to asure that version was updated."""
+
+ def __init__(self, current_version: str, pypi_pkg_name: str, changelog_path: str | None = None) -> None:
+ """Create checker.
+
+ Args:
+ current_version: Version of the current branch. E.g 1.1.0
+ pypi_pkg_name: Name of the PyPi package
+ changelog_path: path to changelog file
+ """
+ self.current_version = current_version
+ self.pypi_pkg_name = pypi_pkg_name
+ self.changelog_path = pathlib.Path(changelog_path) if changelog_path else None
+
+ def __get_pypi_version(self) -> str:
+ """Get the newest version from PyPi.
+
+ Returns:
+ Returns the newest version which is released on PyPi
+
+ Raises:
+ ConnectionError: if no response was received
+ """
+ result = requests.get(f"https://pypi.org/pypi/{self.pypi_pkg_name}/json", timeout=10)
+ if result.status_code != POSITIVE_RESPONSE_CODE:
+ msg = "no response!"
+ raise ConnectionError(msg)
+
+ return json.loads(result.text)["info"]["version"]
+
+ @staticmethod
+ def __str_to_version(version: str) -> int:
+ """Get a integer representation for a given version as string.
+
+ Args:
+ version: Version as string. E.g. 1.1.0
+
+ Returns:
+ A integer representation of the given version
+
+ Raises:
+ AttributeError: if the format of the given version is not correct
+ """
+ version_parts = version.split(".")
+
+ if len(version_parts) != VERSION_LENGTH or not all(value.isdigit() for value in version_parts):
+ msg = f"The format of the given version ({version}) is not correct. Version must have the following format X.X.X"
+ raise AttributeError(msg)
+
+ return int(version_parts[0]) * 1000000 + int(version_parts[1]) * 1000 + int(version_parts[2])
+
+ def check_version(self) -> int:
+ """Check if version of the current branch is higher than the newest PyPi version.
+
+ Returns:
+ 0 if branch version is higher than PyPi version. If not -1
+ """
+ passed = True
+
+ pypi_version = self.__get_pypi_version()
+ branch_version = self.current_version
+
+ LOGGER.info(f"PyPi version: {pypi_version} | branch version: {branch_version}")
+
+ if not self.__str_to_version(branch_version) > self.__str_to_version(pypi_version):
+ passed = False
+ LOGGER.error("Increase version of branch!")
+
+ if self.changelog_path:
+ with self.changelog_path.open("r") as changelog_file:
+ first_line = changelog_file.readline()
+
+ if branch_version not in first_line:
+ LOGGER.error(f"Current version '{branch_version}' must be mentioned in the first line of changelog.md!")
+ passed = False
+
+ return 0 if passed else -1
+
+
+if __name__ == "__main__":
+ sys.exit(VersionCheck(habapp_rules.__version__, "habapp_rules", "changelog.md").check_version())