From 0de87b15d2d03e07074c51a41237c647a20c291d Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 13 Jan 2023 12:25:06 -0700 Subject: [PATCH] [homekit] Implement IrrigationSystem Accessory (#14209) * [homekit] Implement IrrigationSystem Fairly trivial now, except that a ServiceLabelService has to be added to the accessory. Signed-off-by: Cody Cutrer --- bundles/org.openhab.io.homekit/README.md | 41 +++++- .../internal/HomekitAccessoryType.java | 1 + .../internal/HomekitCharacteristicType.java | 6 +- .../accessories/HomekitAccessoryFactory.java | 2 + .../HomekitIrrigationSystemImpl.java | 130 ++++++++++++++++++ .../HomekitMetadataCharacteristicFactory.java | 6 + .../accessories/HomekitValveImpl.java | 23 +++- 7 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitIrrigationSystemImpl.java diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index a5ac5e3240..6808a16932 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -41,6 +41,7 @@ HomeKit integration supports following accessory types: - Battery - Filter Maintenance - Television +- Irrigation System ## Quick start @@ -591,7 +592,7 @@ configuration for these two cases looks as follow: - valve with timer: ```xtend -Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation"]} +Group gValve "Valve Group" {homekit="Valve" [ValveType="Irrigation"]} Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration"} Number valve_remaining_duration "Valve remaining duration" (gValve) {homekit = "Valve.RemainingDuration"} @@ -600,11 +601,38 @@ Number valve_remaining_duration "Valve remaining duration" (gValve) - valve without timer (no item for remaining duration required) ```xtend -Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation", homekitTimer="true"]} +Group gValve "Valve Group" {homekit="Valve" [ValveType="Irrigation", homekitTimer="true"]} Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration" [homekitDefaultDuration = 1800]} ``` +### Irrigation System + +An irrigation system is an accessory composed of multiple valves. +You just need to link multiple valves within an irrigation system's group. +When part of an irrigation system, valves are required to have Duration and RemainingDuration characteristics, as well as a ServiceIndex. +The valve's types will also automatically be set to IRRIGATION. + +```java +Group gIrrigationSystem "Irrigation System" { homekit="IrrigationSystem" } +String irrigationSystemProgramMode (gIrrigationSystem) { homekit="ProgramMode" } +Switch irrigationSystemEnabled (gIrrigationSystem) { homekit="Active" } +Switch irrigationSystemInUse (gIrrigationSystem) { homekit="InUseStatus" } +Group irrigationSystemTotalRemaining (gIrrigationSystem) { homekit="RemainingDuration" } + +Group gValve1 "Valve 1" (gIrrigationSystem) { homekit="Valve"[ServiceIndex=1] } +Switch valve1Active (gValve1) { homekit="ActiveStatus" } +Switch valve1InUse (gValve1) { homekit="InUseStatus" } +Number valve1SetDuration (gValve1) { homekit="Duration" } +Number valve1RemainingDuration (gValve1) { homekit="RemainingDuration" } + +Group gValve2 "Valve 2" (gIrrigationSystem) { homekit="Valve"[ServiceIndex=2] } +Switch valve2Active (gValve2) { homekit="ActiveStatus" } +Switch valve2InUse (gValve2) { homekit="InUseStatus" } +Number valve2SetDuration (gValve2) { homekit="Duration" } +Number valve2RemainingDuration (gValve2) { homekit="RemainingDuration" } +``` + ### Sensors Sensors have typically one mandatory characteristic, e.g. temperature or lead trigger, and several optional characteristics which are typically used for battery powered sensors and/or wireless sensors. @@ -830,7 +858,7 @@ or using UI | | LockCurrentState | | Switch, Number | Current state of lock mechanism (1/ON=SECURED, 0/OFF=UNSECURED, 2=JAMMED, 3=UNKNOWN) | | | LockTargetState | | Switch | Target state of lock mechanism (ON=SECURED, OFF=UNSECURED) | | | | Name | String | Name of the lock | -| Valve | | | | Valve. additional configuration: homekitValveType = ["Generic", "Irrigation", "Shower", "Faucet"] | +| Valve | | | | Valve. additional configuration: ValveType = ["Generic", "Irrigation", "Shower", "Faucet"] | | | ActiveStatus | | Switch, Dimmer | Accessory current working status. A value of "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. | | | InUseStatus | | Switch, Dimmer | Indicates whether fluid flowing through the valve. A value of "ON"/"OPEN" indicates that fluid is flowing. | | | | Duration | Number | Defines how long a valve should be set to ʼIn Useʼ in second. You can define the default duration via configuration homekitDefaultDuration = | @@ -908,6 +936,13 @@ or using UI | | | Volume | Dimmer, Number | Current volume. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] | | | | VolumeSelector | Dimmer, String | If linked do a dimmer item, will send INCREASE/DECREASE commands. If linked to a string item, will send INCREMENT and DECREMENT. | | | | VolumeControlType | String | The type of control available. This will default to infer based on what other items are linked. NONE = status only, no control; RELATIVE = INCREMENT/DECREMENT only, no status; RELATIVE_WITH_CURRENT = INCREMENT/DECREMENT only with status; ABSOLUTE = direct status and control. Can also be configured via metadata, e.g. [VolumeControlType="ABSOLUTE"]. | +| IrrigationSystem | | | | An accessory that represents multiple water valves and accommodates a programmed scheduled. | +| | Active | | Switch | If the irrigation system as a whole is enabled. This must be ON if any of the valves are also enabled. | +| | InUseStatus | | Switch | If the irrigation system as a whole is running. This must be ON if any of the valves are ON. | +| | ProgramMode | | String | The current program mode of the irrigation system. Possible values (NO_SCHEDULED - no programs scheduled, SCHEDULED - program scheduled, SCHEDULED_MANUAL - program scheduled, currently overriden to manual mode). | +| | | RemainingDuration | Number | The remaining duration for all scheduled valves in the current program in seconds. | +| | | FaultStatus | Switch, Contact | Accessory fault status. "ON"/"OPEN" value indicates that the accessory has experienced a fault that may be interfering with its intended functionality. A value of "OFF"/"CLOSED" indicates that there is no fault. | + ### Examples diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java index fb1bac6f86..8c6ff3c3c5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java @@ -59,6 +59,7 @@ public enum HomekitAccessoryType { INPUT_SOURCE("InputSource"), TELEVISION_SPEAKER("TelevisionSpeaker"), ACCESSORY_GROUP("AccessoryGroup"), + IRRIGATION_SYSTEM("IrrigationSystem"), DUMMY("Dummy"); private static final Map TAG_MAP = new HashMap<>(); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java index b95859c510..1915a0b048 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java @@ -141,7 +141,11 @@ public enum HomekitCharacteristicType { TARGET_VISIBILITY_STATE("TargetVisibilityState"), VOLUME_SELECTOR("VolumeSelector"), - VOLUME_CONTROL_TYPE("VolumeControlType"); + VOLUME_CONTROL_TYPE("VolumeControlType"), + + PROGRAM_MODE("ProgramMode"), + SERVICE_LABEL("ServiceLabel"), + SERVICE_INDEX("ServiceIndex"); private static final Map TAG_MAP = new HashMap<>(); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java index a3c15b4817..abd8685b31 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java @@ -108,6 +108,7 @@ public class HomekitAccessoryFactory { put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE }); put(INPUT_SOURCE, new HomekitCharacteristicType[] {}); put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE }); + put(IRRIGATION_SYSTEM, new HomekitCharacteristicType[] { ACTIVE, INUSE_STATUS, PROGRAM_MODE }); } }; @@ -150,6 +151,7 @@ public class HomekitAccessoryFactory { put(TELEVISION, HomekitTelevisionImpl.class); put(INPUT_SOURCE, HomekitInputSourceImpl.class); put(TELEVISION_SPEAKER, HomekitTelevisionSpeakerImpl.class); + put(IRRIGATION_SYSTEM, HomekitIrrigationSystemImpl.class); } }; diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitIrrigationSystemImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitIrrigationSystemImpl.java new file mode 100644 index 0000000000..aab76fd707 --- /dev/null +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitIrrigationSystemImpl.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.homekit.internal.accessories; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +import org.openhab.io.homekit.internal.HomekitAccessoryUpdater; +import org.openhab.io.homekit.internal.HomekitCharacteristicType; +import org.openhab.io.homekit.internal.HomekitSettings; +import org.openhab.io.homekit.internal.HomekitTaggedItem; + +import io.github.hapjava.accessories.IrrigationSystemAccessory; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.common.ActiveEnum; +import io.github.hapjava.characteristics.impl.common.InUseEnum; +import io.github.hapjava.characteristics.impl.common.ProgramModeEnum; +import io.github.hapjava.characteristics.impl.common.ServiceLabelNamespaceCharacteristic; +import io.github.hapjava.characteristics.impl.common.ServiceLabelNamespaceEnum; +import io.github.hapjava.services.impl.IrrigationSystemService; +import io.github.hapjava.services.impl.ServiceLabelService; + +/** + * Implements an Irrigation System accessory. + * + * To be a complete accessory, the user must configure individual valves linked + * to this primary service. This class also adds the ServiceLabelService + * automatically. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault({}) +public class HomekitIrrigationSystemImpl extends AbstractHomekitAccessoryImpl implements IrrigationSystemAccessory { + private BooleanItemReader inUseReader; + private Map programModeMap; + private static final String SERVICE_LABEL = "ServiceLabel"; + + public HomekitIrrigationSystemImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, + HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { + super(taggedItem, mandatoryCharacteristics, updater, settings); + inUseReader = createBooleanReader(HomekitCharacteristicType.INUSE_STATUS); + programModeMap = HomekitCharacteristicFactory + .createMapping(getCharacteristic(HomekitCharacteristicType.PROGRAM_MODE).get(), ProgramModeEnum.class); + getServices().add(new IrrigationSystemService(this)); + } + + @Override + public void init() { + String serviceLabelNamespaceConfig = getAccessoryConfiguration(SERVICE_LABEL, "ARABIC_NUMERALS"); + ServiceLabelNamespaceEnum serviceLabelEnum; + + try { + serviceLabelEnum = ServiceLabelNamespaceEnum.valueOf(serviceLabelNamespaceConfig.toUpperCase()); + } catch (IllegalArgumentException e) { + serviceLabelEnum = ServiceLabelNamespaceEnum.ARABIC_NUMERALS; + } + final var finalEnum = serviceLabelEnum; + var serviceLabelNamespace = getCharacteristic(ServiceLabelNamespaceCharacteristic.class).orElseGet( + () -> new ServiceLabelNamespaceCharacteristic(() -> CompletableFuture.completedFuture(finalEnum))); + getServices().add(new ServiceLabelService(serviceLabelNamespace)); + } + + @Override + public CompletableFuture getActive() { + OnOffType state = getStateAs(HomekitCharacteristicType.ACTIVE, OnOffType.class); + return CompletableFuture.completedFuture(state == OnOffType.ON ? ActiveEnum.ACTIVE : ActiveEnum.INACTIVE); + } + + @Override + public CompletableFuture setActive(ActiveEnum value) { + getCharacteristic(HomekitCharacteristicType.ACTIVE).ifPresent(tItem -> { + tItem.send(value == ActiveEnum.ACTIVE ? OnOffType.ON : OnOffType.OFF); + }); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture getInUse() { + return CompletableFuture.completedFuture(inUseReader.getValue() ? InUseEnum.IN_USE : InUseEnum.NOT_IN_USE); + } + + @Override + public CompletableFuture getProgramMode() { + return CompletableFuture.completedFuture(getKeyFromMapping(HomekitCharacteristicType.PROGRAM_MODE, + programModeMap, ProgramModeEnum.NO_SCHEDULED)); + } + + @Override + public void subscribeActive(HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.ACTIVE, callback); + } + + @Override + public void unsubscribeActive() { + unsubscribe(HomekitCharacteristicType.ACTIVE); + } + + @Override + public void subscribeInUse(HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.INUSE_STATUS, callback); + } + + @Override + public void unsubscribeInUse() { + unsubscribe(HomekitCharacteristicType.INUSE_STATUS); + } + + @Override + public void subscribeProgramMode(HomekitCharacteristicChangeCallback callback) { + subscribe(HomekitCharacteristicType.PROGRAM_MODE, callback); + } + + @Override + public void unsubscribeProgramMode() { + unsubscribe(HomekitCharacteristicType.PROGRAM_MODE); + } +} diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMetadataCharacteristicFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMetadataCharacteristicFactory.java index 0458a9bacb..808d833a1f 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMetadataCharacteristicFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMetadataCharacteristicFactory.java @@ -39,6 +39,7 @@ import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic; import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic; import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum; import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.common.ServiceLabelIndexCharacteristic; import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateCharacteristic; import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateEnum; import io.github.hapjava.characteristics.impl.heatercooler.TargetHeaterCoolerStateCharacteristic; @@ -90,6 +91,7 @@ public class HomekitMetadataCharacteristicFactory { put(INPUT_SOURCE_TYPE, HomekitMetadataCharacteristicFactory::createInputSourceTypeCharacteristic); put(NAME, HomekitMetadataCharacteristicFactory::createNameCharacteristic); put(PICTURE_MODE, HomekitMetadataCharacteristicFactory::createPictureModeCharacteristic); + put(SERVICE_INDEX, HomekitMetadataCharacteristicFactory::createServiceIndexCharacteristic); put(SLEEP_DISCOVERY_MODE, HomekitMetadataCharacteristicFactory::createSleepDiscoveryModeCharacteristic); put(TARGET_HEATER_COOLER_STATE, HomekitMetadataCharacteristicFactory::createTargetHeaterCoolerStateCharacteristic); @@ -249,6 +251,10 @@ public class HomekitMetadataCharacteristicFactory { }); } + private static Characteristic createServiceIndexCharacteristic(Object value) { + return new ServiceLabelIndexCharacteristic(getInteger(value)); + } + private static Characteristic createSleepDiscoveryModeCharacteristic(Object value) { return new SleepDiscoveryModeCharacteristic(getEnum(value, SleepDiscoveryModeEnum.class, SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE, SleepDiscoveryModeEnum.NOT_DISCOVERABLE), v -> { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java index 8f96d0a10d..f159b6fdac 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java @@ -38,6 +38,7 @@ import org.openhab.io.homekit.internal.HomekitTaggedItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.github.hapjava.accessories.HomekitAccessory; import io.github.hapjava.accessories.ValveAccessory; import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; import io.github.hapjava.characteristics.impl.common.ActiveEnum; @@ -53,7 +54,8 @@ import io.github.hapjava.services.impl.ValveService; */ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements ValveAccessory { private final Logger logger = LoggerFactory.getLogger(HomekitValveImpl.class); - private static final String CONFIG_VALVE_TYPE = "homekitValveType"; + private static final String CONFIG_VALVE_TYPE = "ValveType"; + private static final String CONFIG_VALVE_TYPE_DEPRECATED = "homekitValveType"; public static final String CONFIG_DEFAULT_DURATION = "homekitDefaultDuration"; private static final String CONFIG_TIMER = "homekitTimer"; @@ -70,6 +72,7 @@ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements Va private final ScheduledExecutorService timerService = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture valveTimer; private final boolean homekitTimer; + private ValveTypeEnum valveType; public HomekitValveImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { @@ -82,6 +85,10 @@ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements Va if (homekitTimer) { addRemainingDurationCharacteristic(taggedItem, updater, service); } + String valveTypeConfig = getAccessoryConfiguration(CONFIG_VALVE_TYPE, "GENERIC"); + valveTypeConfig = getAccessoryConfiguration(CONFIG_VALVE_TYPE_DEPRECATED, valveTypeConfig); + var valveType = CONFIG_VALVE_TYPE_MAPPING.get(valveTypeConfig.toUpperCase()); + this.valveType = valveType != null ? valveType : ValveTypeEnum.GENERIC; } private void addRemainingDurationCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater, @@ -191,9 +198,7 @@ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements Va @Override public CompletableFuture getValveType() { - final String valveType = getAccessoryConfiguration(CONFIG_VALVE_TYPE, "GENERIC"); - ValveTypeEnum type = CONFIG_VALVE_TYPE_MAPPING.get(valveType.toUpperCase()); - return CompletableFuture.completedFuture(type != null ? type : ValveTypeEnum.GENERIC); + return CompletableFuture.completedFuture(valveType); } @Override @@ -205,4 +210,14 @@ public class HomekitValveImpl extends AbstractHomekitAccessoryImpl implements Va public void unsubscribeValveType() { // nothing changes here } + + @Override + public boolean isLinkable(HomekitAccessory parentAccessory) { + // When part of an irrigation system, the valve type _must_ be irrigation. + if (parentAccessory instanceof HomekitIrrigationSystemImpl) { + valveType = ValveTypeEnum.IRRIGATION; + return true; + } + return false; + } } -- 2.47.3