Reusable Home Automation rules.
Project description
Zone API - an alternative approach to writing rules
In OpenHab, items are defined in a flat manner in the .items files under the /etc/openhab/items folder. They are typically linked to a channel exposed by the underlying hardware. This flat structure has an impact on how rules (whether in Xtend or Jython) are organized. As there is no higher level abstraction, rules tend to listen to changes from the specific devices. When the rules need to interact with multiple devices of the same type, they can utilize the group concept. An example of good usage of group is to turn off all lights. By linking all smart lights to a group switch, turning off all the lights can be done by changing the state of the group switch to OFF.
What is more tricky is when rules need to interact with different devices within the same area. The typical solution is to group unrelated items that belong to the same zone either by using a naming pattern, or by dedicated groups. For example, the light switch and motion sensor in the Foyer area can be named like this: "FF_Foyer_Light", and "FF_Foyer_MotionSensor". When a sensor is triggered, the zone can be derived from the name of the triggering item, and other devices/sensors can be retrieved using that naming convention. This works but as there is not sufficient abstraction, the rules are highly coupled to the naming pattern.
The Zone API provides another approach. It is a layer above the devices / sensors. Each ZoneManager (i.e. a house) contains multiple zones (i.e. rooms), and each zone contains multiple devices. Each zone is associated with a set of actions that are triggered by certain events. The usual OpenHab events are routed in this manner:
OpenHab events --> ZoneManager --> Zones --> Actions
The actions operate on the abstract devices and do not concern about the naming of the items or the underlying hardware. They replace the traditional OpenHab rules. Actions can be unit-tested with various levels of mocking.
Most importantly, it enables reusing of action logics. There is no need to reinvent the wheels for common rules such as turning on/off the lights. All ones need to do is to populate the zones and devices / sensors, and the applicable actions will be added and processed automatically.
ZoneApi comes with a set of built-in actions. There is no need to determine what action to add to a system. Instead, they are added automatically based on the zones structure and based on the type of devices available in each zone.
Here is a sample info log that illustrate the structure of the managed objects.
Zone: Kitchen, floor: FIRST_FLOOR, internal, displayIcon: kitchen, displayOrder: 3, 7 devices
AstroSensor: VT_Time_Of_Day
HumiditySensor: FF_Kitchen_Humidity
IlluminanceSensor: FF_Kitchen_LightSwitch_Illuminance
Light: FF_Kitchen_LightSwitch, duration: 5 mins, illuminance: 8
MotionSensor: FF_Kitchen_SecurityMotionSensor, battery powered
MotionSensor: FF_Kitchen_LightSwitch_PantryMotionSensor, battery powered
TemperatureSensor: FF_Kitchen_Temperature
Action: HUMIDITY_CHANGED -> AlertOnHumidityOutOfRange
Action: MOTION -> TurnOnSwitch
Action: MOTION -> AnnounceMorningWeatherAndPlayMusic
Action: MOTION -> PlayMusicAtDinnerTime
Action: SWITCH_TURNED_ON -> TurnOffAdjacentZones
Action: TEMPERATURE_CHANGED -> AlertOnTemperatureOutOfRange
Action: TIMER -> TellKidsToGoToBed
Neighbor: FF_Foyer, OPEN_SPACE
Neighbor: FF_GreatRoom, OPEN_SPACE_MASTER
Zone: Foyer, floor: FIRST_FLOOR, internal, displayIcon: groundfloor, displayOrder: 4, 6 devices
AlarmPartition: FF_Foyer_AlarmPartition, armMode: ARM_STAY
AstroSensor: VT_Time_Of_Day
Door: FF_Foyer_Door
Light: FF_Foyer_LightSwitch, duration: 5 mins, illuminance: 8, no premature turn-off time range: 0-23:59
MotionSensor: FF_Foyer_LightSwitch_ClosetMotionSensor, battery powered
MotionSensor: FF_Foyer_LightSwitch_MotionSensor, battery powered
Action: MOTION -> TurnOnSwitch
Action: MOTION -> DisarmOnInternalMotion
Action: MOTION -> ManagePlugs
Action: PARTITION_ARMED_AWAY -> ChangeThermostatBasedOnSecurityArmMode
Action: PARTITION_ARMED_AWAY -> ManagePlugs
Action: PARTITION_ARMED_AWAY -> TurnOffDevicesOnAlarmModeChange
Action: PARTITION_DISARMED_FROM_AWAY -> ChangeThermostatBasedOnSecurityArmMode
Action: PARTITION_DISARMED_FROM_AWAY -> ManagePlugs
Action: PARTITION_DISARMED_FROM_AWAY -> TurnOffDevicesOnAlarmModeChange
Action: SWITCH_TURNED_ON -> TurnOffAdjacentZones
Action: TIMER -> ArmStayIfNoMovement
Action: TIMER -> ArmStayInTheNight
Action: TIMER -> ManagePlugs
Neighbor: SF_Lobby, OPEN_SPACE
Neighbor: FF_Office, OPEN_SPACE_MASTER
Running on top of HABApp but with minimal dependency:
The original Zone API modules were written in Jython. It was recently migrated over to the HABApp framework with minimal changes needed to the core code. See here for the comparison between HABApp and JSR223 Jython.
There are 3 peripheral modules that are tightly coupled to the HABApp API. The rest of the modules is framework neutral. It is possible to migrate Zone API to another framework running on top of GravVM when it is available. Zone API is now written in Python 3 and thus is not compatible with Jython (equivalent to Python 2.8).
Set up your home automation system with zone_api
1. Name the OpenHab items using the default naming convention
Zone_api comes with a default parser that builds the zone manager using a pre-defined naming convention. See the
ZoneParser section at the end of this page for details.
Here are a few sample .items files. Note that the file organization doesn't matter; all items can be defined in a single file if desired.
zones.items: defines two zones and their relationship.
String Zone_Office
{ level="FF", displayIcon="office", displayOrder="2",
openSpaceSlaveNeighbors="FF_Foyer" }
String Zone_Foyer
{ level="FF", displayIcon="groundfloor", displayOrder="4",
openSpaceMasterNeighbors="FF_Office",
openSpaceNeighbors="SF_Lobby" }
foyer.items: defines the items in the Foyer zone.
Switch FF_Foyer_LightSwitch "Foyer Light" (gWallSwitch, gLightSwitch, gFirstFloorLightSwitch)
{ channel="zwave:device:9e4ce05e:node2:switch_binary",
disableMotionTriggeringIfOtherLightIsOn="FF_Office_LightSwitch",
durationInMinutes="5"}
Switch FF_Foyer_LightSwitch_ClosetMotionSensor "Foyer Closet Motion Sensor"
(gWallSwitchMotionSensor)
{ channel="mqtt:topic:myBroker:xiaomiMotionSensors:FoyerMotionSensor"}
office.items: defines the items in the Office zone.
Switch FF_Office_LightSwitch "Office Light" (gWallSwitch, gLightSwitch, gFirstFloorLightSwitch)
[shared-motion-sensor]
{ channel="zwave:device:9e4ce05e:node8:switch_binary",
durationInMinutes="15" }
Switch FF_Office_LightSwitch_MotionSensor "Office Motion Sensor"
(gWallSwitchMotionSensor, gFirstFloorMotionSensors)
{ channel="mqtt:topic:myBroker:xiaomiMotionSensors:OfficeMotionSensor"}
That's it. Once the system is fully set up, ZoneApi's default actions will be registered automatically depending on the available devices.
In the example above, the two zones have light switches and motion sensor. Thus the light rule is applicable and will automatically turn on the light when a motion sensor is triggered, and turn it off if there is no activity for the pre-defined duration. It will also turn off lights in the dependent zones.
2. Clone this repository
git clone git@github.com:yfaway/zone-apis.git
3. Install, configure, and run HABapp
Refer to the instructions on the official HABApp website. The instruction below is specifically for the zone_api.
sudo apt-get install python3-venv # to install python3-venv library
cd zone_api # the cloned project in the section above
python3 -m venv .
source bin/activate # to get into our virtual environment
python3 -m pip install --upgrade pip # to upgrade the pip library.
python3 -m pip install habapp request schedule # request and schedule are required by zone_api
To manually run HABApp, execute this command within the zone_api folder:
./bin/habapp --config ./habapp/config.yml
The ./habapp/rules folder contains the bootstrap rule to initialize the zone_api framework.
The rule is pretty simple with its entire content below.
import HABApp
from zone_api import zone_parser as zp
from zone_api.core.devices.activity_times import ActivityType, ActivityTimes
class ConfigureZoneManagerRule(HABApp.Rule):
def __init__(self):
super().__init__()
self.run_soon(self.configure_zone_manager)
# noinspection PyMethodMayBeStatic
def configure_zone_manager(self):
time_map = {
ActivityType.WAKE_UP: '6 - 9',
ActivityType.LUNCH: '12:00 - 13:30',
ActivityType.QUIET: '14:00 - 16:00, 20:00 - 22:59',
ActivityType.DINNER: '17:50 - 20:00',
ActivityType.SLEEP: '23:00 - 7:00',
ActivityType.AUTO_ARM_STAY: '20:00 - 2:00',
ActivityType.TURN_OFF_PLUGS: '23:00 - 2:00',
}
zone_manager = zp.parse(ActivityTimes(time_map))
ConfigureZoneManagerRule()
The code above defines an ActivityTimes object with various activity time periods and pass it over to the zone_parser module. The zone_parser parses the OpenHab items following a specific naming pattern, and construct the zones and the devices / sensors. It then registers the handlers for the events associated with the devices / sensors. Finally, it loads all the actions and add them to the zones based on the pre-declared execution rules associated with each action (more on this later). That's it; from this point forward, events generated by the devices / sensors will trigger the associated actions.
It is important to note that the zone_parser is just a default mechanism to build the ZoneManager. A custom module can be used to parse from a different naming pattern for the OpenHab items, or the ZoneManager can be constructed manually. The role of the parser is no longer needed once the ZoneManager has been built.
ZoneManager
Contains a set of zones and is responsible for dispatching the events to the zones.
Zone
Contains a set of devices, actions, and is responsible for dispatching the events to the actions.
A zone is aware of its neighbors. Certain rules such as the turning on/off of the lights is highly dependent on the layout of the zones. The following neighbor types are available.
CLOSED_SPACEOPEN_SPACEOPEN_SPACE_MASTEROPEN_SPACE_SLAVE
Devices
The devices contains one or more underlying OpenHab items. Rather than operating on a SwitchItem or on a NumberItem, the device represents meaningful concrete things such as a MotionSensor, or a Light. Devices contain both attributes (e.g. 'is the door open') and behaviors (e.g. 'arm the security system').
Events
Similar to the abstraction for the devices, the events are also more concrete. Zone API maps the
OpenHab items events to the event enums in ZoneEvent
such as ZoneEvent.HUMIDITY_CHANGED or ZoneEvent.PARTITION_ARMED_AWAY.
There is also the special event ZoneEvent.TIMER that represents triggering from a scheduler.
The event is dispatched to the appropriate zones which then invokes the actions registered for that event. See EventInfo for more info.
Actions
All the actions implement the Action interface. The action's life cycle is represented by the three functions:
on_startup()- invoked after the ZoneManager has been fully populated, via the eventZoneEvent.STARTUP.on_action()- invoked where the device generates an event or when a timer event is triggered (viaZoneEvent.TIMER).on_destroy()- currently not invoked.
The @action decorator provides execution rules for the action as well as basic validation.
If the condition (based on the execution rules) does not match, the action won't be executed.
Below are the currently supported decorator parameters:
- devices - the list of devices the zone must have in order to invoke the action.
- events - the list of events for which the action will response to.
- internal - if set, this action is only applicable for internal zone
- external - if set, this action is only applicable for external zone
- levels - the zone levels that this action is applicable to. the empty list default value indicates that the action is applicable to all zone levels.
- unique_instance - if set, do not share the same action instance across zones. This is the case when the action is stateful.
- zone_name_pattern - if set, the zone name regular expression that is applicable to this action.
- external_events - the list of events from other zones that this action processes. These events won't be filtered using the same mechanism as the internal events as they come from other zones.
- priority - the action priority with respect to other actions within the same zone. Actions with lower priority values are executed first.
These parameters are also available to the action and can be used as a filtering mechanism to make sure that the action is only added to the applicable zones.
Here is a simple action to disarm the security system when a motion sensor is triggered:
from zone_api import security_manager as sm
from zone_api.core.devices.activity_times import ActivityTimes
from zone_api.core.devices.motion_sensor import MotionSensor
from zone_api.core.zone_event import ZoneEvent
from zone_api.core.action import action
from zone_api.core.devices.alarm_partition import AlarmPartition
@action(events=[ZoneEvent.MOTION], devices=[AlarmPartition, MotionSensor])
class DisarmOnInternalMotion:
"""
Automatically disarm the security system when the motion sensor in the zone containing the
security panel is triggered and the current time is not in the auto-arm-stay or sleep
time periods.
"""
def on_action(self, event_info):
events = event_info.get_event_dispatcher()
zone_manager = event_info.get_zone_manager()
if not sm.is_armed_stay(zone_manager):
return False
activity = zone_manager.get_first_device_by_type(ActivityTimes)
if activity is None:
self.log_warning("Missing activities time; can't determine wake-up time.")
return False
if activity.is_auto_arm_stay_time() or (activity.is_sleep_time() and not activity.is_wakeup_time()):
return False
sm.disarm(zone_manager, events)
return True
The decorator for the action above indicates that it is triggered by the motion event, and should only be added to a zone that contains both the AlarmPartition and the Motion devices.
ZoneParser
The default parser uses this naming pattern for the OpenHab items.
-
The zones are defined as a String item with this pattern Zone_{name}:
String Zone_GreatRoom { level="FF", displayIcon="player", displayOrder="1", openSpaceSlaveNeighbors="FF_Kitchen" }- The levels are the reversed mapping of the enums in Zone::Level.
- Here are the list of supported attributes: level, external, openSpaceNeighbors, openSpaceMasterNeighbors, openSpaceSlaveNeighbors, displayIcon, displayOrder.
-
The individual OpenHab items are named after this convention:
{zone_id}_{device_type}_{device_name}.Here's an example:
Switch FF_Office_LightSwitch "Office Light" (gWallSwitch, gLightSwitch, gFirstFloorLightSwitch) [shared-motion-sensor] { channel="zwave:device:9e4ce05e:node8:switch_binary", durationInMinutes="15" }
See here for a sample .items file that is parsable by ZoneParser.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file zone_api-0.2.1.tar.gz.
File metadata
- Download URL: zone_api-0.2.1.tar.gz
- Upload date:
- Size: 74.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.4.1 importlib_metadata/4.5.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.1 CPython/3.9.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c0cb304933f67c58fc5056185928157ff402e20566a5d2d999bcd542e551700
|
|
| MD5 |
fe6898a151718cd60ff3c68ae68ac3d9
|
|
| BLAKE2b-256 |
338ea660e0193c06714c6f0e84bbf47dc3c882b2771bdf736d52dc42e720ccc5
|