Skip to content

Commit

Permalink
Merge pull request #86 from nobbi1991/bathroom_light
Browse files Browse the repository at this point in the history
Bathroom light
  • Loading branch information
nobbi1991 authored Nov 29, 2024
2 parents 2dafd29 + be69989 commit 19a9d10
Show file tree
Hide file tree
Showing 34 changed files with 701 additions and 24 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/update_pre_commit_hooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Update pre-commit hooks

on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # Runs every Sunday at midnight UTC
push:
branches:
- main # Runs when changes are pushed to the main branch

jobs:
update-pre-commit-hooks:
runs-on: ubuntu-latest

steps:
# Checkout the repository
- name: Checkout repository
uses: actions/checkout@v2

# Install pre-commit
- name: Install pre-commit
run: |
python -m pip install --upgrade pip
pip install pre-commit
- name: Run pre-commit autoupdate
# Run pre-commit autoupdate to update the hook versions
run: pre-commit autoupdate

# Create a new branch and push changes (if the branch does not exist)
- name: Create branch and push updates
run: |
# Check if the branch exists, if not, create it
git fetch origin
BRANCH_NAME="pre-commit-updates"
if git rev-parse --verify origin/$BRANCH_NAME; then
echo "Branch $BRANCH_NAME exists, switching to it."
git checkout $BRANCH_NAME
else
echo "Branch $BRANCH_NAME does not exist, creating it."
git checkout -b $BRANCH_NAME
fi
# Add the updated .pre-commit-config.yaml file and commit changes
git add .pre-commit-config.yaml
git commit -m 'Update pre-commit hook versions'
# Push the changes to the branch
git push -u origin $BRANCH_NAME
7 changes: 7 additions & 0 deletions .idea/ruff.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ repos:
- id: fix-byte-order-marker
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
rev: v0.8.1
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/google/yamlfmt
rev: v0.13.0
rev: v0.14.0
hooks:
- id: yamlfmt
args: [-conf, .yamlfmt.yaml]
Expand All @@ -39,7 +39,7 @@ repos:
hooks:
- id: shfmt
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.18
rev: 0.7.19
hooks:
- id: mdformat

Expand Down
12 changes: 12 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Version 7.1.0 - dd.11.2024

# Features

- added rule `habapp_rules.actors.light_bathroom.BathroomLight` to control bathroom light
- added the python version to `habapp_rules.core.version.SetVersions`
- improved `habapp_rules.core.timeout_list`

# Bugfix

- added additional wait time to `habapp_rules.actors.energy_save_switch.EnergySaveSwitch` when switch is in wait for current state and current falls below threshold.

# Version 7.0.1 - 25.11.2024

# Bugfix
Expand Down
2 changes: 1 addition & 1 deletion habapp_rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

import pytz

__version__ = "7.0.1"
__version__ = "7.1.0"
BASE_PATH = pathlib.Path(__file__).parent.parent.resolve()
TIMEZONE = pytz.timezone("Europe/Berlin")
1 change: 1 addition & 0 deletions habapp_rules/actors/config/energy_save_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class EnergySaveSwitchParameter(habapp_rules.core.pydantic_base.ParameterBase):
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.")
extended_wait_for_current_time: int = pydantic.Field(60, description="Extended time to wait time before switch off the relay in seconds. If current goes above threshold, it will jump back to ON state.", gt=0)


class EnergySaveSwitchConfig(habapp_rules.core.pydantic_base.ConfigBase):
Expand Down
39 changes: 39 additions & 0 deletions habapp_rules/actors/config/light_bathroom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import HABApp
import pydantic

from habapp_rules.core.pydantic_base import ConfigBase, ItemBase, ParameterBase


class BathroomLightItems(ItemBase):
"""Items for bathroom light."""

# lights
light_main: HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="main light item")
light_main_color: HABApp.openhab.items.NumberItem = pydantic.Field(..., description="main light color (Kelvin)")
light_main_hcl: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="set HCL mode from KNX actor active for main light")
light_mirror: HABApp.openhab.items.DimmerItem = pydantic.Field(..., description="mirror light item")

# environment
sleeping_state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="sleeping state item")
presence_state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="presence state item")

# state machine
manual: HABApp.openhab.items.SwitchItem = pydantic.Field(..., description="item to switch to manual mode and disable the automatic functions")
state: HABApp.openhab.items.StringItem = pydantic.Field(..., description="item to store the current state of the state machine")


class BathroomLightParameter(ParameterBase):
"""Parameter for bathroom light."""

color_mirror_sync: float = pydantic.Field(4000, description="color temperature for the mirror")
min_brightness_mirror_sync: int = pydantic.Field(80, description="minimum brightness for main light if main and mirror light is ON")
color_night: int = pydantic.Field(2600, description="color temperature for night mode")
brightness_night: int = pydantic.Field(40, description="brightness for night mode")
extended_sleep_time: int = pydantic.Field(15 * 60, description="additional sleep time in seconds", gt=0)


class BathroomLightConfig(ConfigBase):
"""Config for bathroom light."""

items: BathroomLightItems = pydantic.Field(..., description="items for the switch")
parameter: BathroomLightParameter = pydantic.Field(BathroomLightParameter(), description="parameter for the switch")
2 changes: 1 addition & 1 deletion habapp_rules/actors/config/shading.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import copy

import HABApp.openhab.items # noqa: TCH002
import HABApp.openhab.items # noqa: TC002
import pydantic
import typing_extensions

Expand Down
16 changes: 13 additions & 3 deletions habapp_rules/actors/energy_save_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,26 @@ class EnergySaveSwitch(habapp_rules.core.state_machine_rule.StateMachineRule):
{"name": "On"},
{"name": "Off"},
{"name": "WaitCurrent"},
{"name": "WaitCurrentExtended", "timeout": 0, "on_timeout": ["extended_wait_timeout"]},
],
},
]

trans: typing.ClassVar = [
# manual
{"trigger": "manual_on", "source": ["Auto", "Hand"], "dest": "Manual"},
{"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
# hand
{"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"},
# sleeping presence conditions
{"trigger": "on_conditions_met", "source": ["Auto_Off", "Auto_WaitCurrent", "Auto_WaitCurrentExtended"], "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"},
# switch off
{"trigger": "current_below_threshold", "source": "Auto_WaitCurrent", "dest": "Auto_WaitCurrentExtended"},
{"trigger": "current_above_threshold", "source": "Auto_WaitCurrentExtended", "dest": "Auto_WaitCurrent"},
{"trigger": "extended_wait_timeout", "source": "Auto_WaitCurrentExtended", "dest": "Auto_Off"},
{"trigger": "max_on_countdown", "source": ["Auto_On", "Auto_WaitCurrent", "Hand"], "dest": "Auto_Off"},
]

Expand All @@ -74,7 +81,6 @@ def __init__(self, config: habapp_rules.actors.config.energy_save_switch.EnergyS

# 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
Expand All @@ -100,6 +106,7 @@ def __init__(self, config: habapp_rules.actors.config.energy_save_switch.EnergyS
def _set_timeouts(self) -> None:
"""Set timeouts."""
self.state_machine.states["Hand"].timeout = self._config.parameter.hand_timeout or 0
self.state_machine.states["Auto"].states["WaitCurrentExtended"].timeout = self._config.parameter.extended_wait_for_current_time

def _get_initial_state(self, default_value: str = "") -> str: # noqa: ARG002
"""Get initial state of state machine.
Expand Down Expand Up @@ -228,3 +235,6 @@ def _cb_current_changed(self, _: HABApp.openhab.events.ItemStateChangedEvent) ->
"""Callback which is triggered if the current value changed."""
if self.state == "Auto_WaitCurrent" and not self._current_above_threshold():
self.current_below_threshold()

if self.state == "Auto_WaitCurrentExtended" and self._current_above_threshold():
self.current_above_threshold()
169 changes: 169 additions & 0 deletions habapp_rules/actors/light_bathroom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""Bathroom light rules."""

import logging
import time
import typing

import HABApp

import habapp_rules.actors.config.energy_save_switch
import habapp_rules.actors.config.light_bathroom
import habapp_rules.actors.state_observer
import habapp_rules.core.helper
import habapp_rules.core.logger
import habapp_rules.core.state_machine_rule
import habapp_rules.system

LOGGER = logging.getLogger(__name__)


class BathroomLight(habapp_rules.core.state_machine_rule.StateMachineRule):
"""Bathroom light rule."""

states: typing.ClassVar = [
{"name": "Manual"},
{
"name": "Auto",
"initial": "Init",
"children": [
{"name": "Init"},
{"name": "Off"},
{"name": "On", "initial": "Init", "children": [{"name": "Init"}, {"name": "MainDay"}, {"name": "MainNight"}, {"name": "MainAndMirror"}]},
],
},
]

trans: typing.ClassVar = [
# manual
{"trigger": "manual_on", "source": "Auto", "dest": "Manual"},
{"trigger": "manual_off", "source": "Manual", "dest": "Auto"},
# switch on
{"trigger": "hand_on", "source": "Auto_Off", "dest": "Auto_On"},
# mirror
{"trigger": "mirror_on", "source": ["Auto_On_MainDay", "Auto_On_MainNight"], "dest": "Auto_On_MainAndMirror"},
{"trigger": "mirror_off", "source": "Auto_On_MainAndMirror", "dest": "Auto_On_MainDay", "conditions": "_is_day"},
{"trigger": "mirror_off", "source": "Auto_On_MainAndMirror", "dest": "Auto_On_MainNight", "unless": "_is_day"},
# off
{"trigger": "hand_off", "source": "Auto_On", "dest": "Auto_Off"},
{"trigger": "sleep_started", "source": "Auto_On", "dest": "Auto_Off"},
{"trigger": "leaving", "source": "Auto_On", "dest": "Auto_Off"},
]

def __init__(self, config: habapp_rules.actors.config.light_bathroom.BathroomLightConfig) -> None:
"""Init rule.
Args:
config: Config of bathroom light 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.light_main.name)

self._sleep_end_time = 0
self._light_main_observer = habapp_rules.actors.state_observer.StateObserverDimmer(self._config.items.light_main.name, cb_on=self._cb_hand_on, cb_off=self._cb_hand_off, value_tolerance=5)

# 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_state(self._get_initial_state())

# callbacks
self._config.items.manual.listen_event(self._cb_manual, HABApp.openhab.events.ItemStateChangedEventFilter())
self._config.items.light_mirror.listen_event(self._cb_mirror, HABApp.openhab.events.ItemStateChangedEventFilter())
self._config.items.sleeping_state.listen_event(self._cb_sleeping_state, HABApp.openhab.events.ItemStateChangedEventFilter())
self._config.items.presence_state.listen_event(self._cb_presence_state, HABApp.openhab.events.ItemStateChangedEventFilter())

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 "Manual" if self._config.items.manual.is_on() else "Auto"

def on_enter_Auto_Init(self) -> None: # noqa: N802
"""Callback, which is called on enter of Auto_Init state."""
if self._config.items.light_main.is_on():
self.to_Auto_On()
else:
self.to_Auto_Off()

def on_enter_Auto_On_Init(self) -> None: # noqa: N802
"""Callback, which is called on enter of Auto_On_Init state."""
if self._mirror_is_on():
self.to_Auto_On_MainAndMirror()
elif self._is_day():
self.to_Auto_On_MainDay()
else:
self.to_Auto_On_MainNight()

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_outputs()
self._previous_state = self.state

def _set_outputs(self) -> None:
if self.state == "Manual":
return

if self.state == "Auto_Off":
habapp_rules.core.helper.send_if_different(self._config.items.light_mirror, "OFF")
if self._light_main_observer.value:
self._light_main_observer.send_command(0)
elif self.state == "Auto_On_MainDay":
habapp_rules.core.helper.send_if_different(self._config.items.light_main_hcl, "ON")
elif self.state == "Auto_On_MainNight":
self._light_main_observer.send_command(self._config.parameter.brightness_night)
habapp_rules.core.helper.send_if_different(self._config.items.light_main_color, self._config.parameter.color_night)
elif self.state == "Auto_On_MainAndMirror":
habapp_rules.core.helper.send_if_different(self._config.items.light_main_color, self._config.parameter.color_mirror_sync)
new_brightness = max(self._config.parameter.min_brightness_mirror_sync, self._light_main_observer.value)
self._light_main_observer.send_command(new_brightness)

def _is_day(self) -> bool:
if self._config.items.sleeping_state.value != habapp_rules.system.SleepState.AWAKE.value:
return False

return time.time() - self._sleep_end_time > self._config.parameter.extended_sleep_time

def _mirror_is_on(self) -> bool:
return self._config.items.light_mirror.is_on()

def _cb_manual(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
if event.value == "ON":
self.manual_on()
else:
self.manual_off()

def _cb_hand_on(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
self.hand_on()

def _cb_hand_off(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
self.hand_off()

def _cb_mirror(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None: # noqa: ARG002
if self._config.items.light_mirror.is_on():
self.mirror_on()
else:
self.mirror_off()

def _cb_sleeping_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
if event.value == habapp_rules.system.SleepState.PRE_SLEEPING.value:
self.sleep_started()

if event.value == habapp_rules.system.SleepState.AWAKE.value:
self._sleep_end_time = time.time()

def _cb_presence_state(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
if event.value == habapp_rules.system.PresenceState.LEAVING.value:
self.leaving()
Loading

0 comments on commit 19a9d10

Please sign in to comment.