]> git.basschouten.com Git - openhab-addons.git/commitdiff
[homekit] Implement IrrigationSystem Accessory (#14209)
authorCody Cutrer <cody@cutrer.us>
Fri, 13 Jan 2023 19:25:06 +0000 (12:25 -0700)
committerGitHub <noreply@github.com>
Fri, 13 Jan 2023 19:25:06 +0000 (20:25 +0100)
* [homekit] Implement IrrigationSystem

Fairly trivial now, except that a ServiceLabelService has to be added
to the accessory.

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.io.homekit/README.md
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAccessoryType.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitCharacteristicType.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitIrrigationSystemImpl.java [new file with mode: 0644]
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitMetadataCharacteristicFactory.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitValveImpl.java

index a5ac5e3240f2a66bf669c053c1a90d3866cacf06..6808a16932d6ca813160566dc28ad4cfbca9d9ce 100644 (file)
@@ -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 = <default duration in seconds>                                                                                                                                                                          |
@@ -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
 
index fb1bac6f8611f7e093f43cb7ea4830b42bde9f3b..8c6ff3c3c54fad00459de65b0663942fd755deb1 100644 (file)
@@ -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<String, HomekitAccessoryType> TAG_MAP = new HashMap<>();
index b95859c510c7313fbb811afdde352ba49a7b1243..1915a0b04892be9601442b7708f2b72789ebd9b2 100644 (file)
@@ -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<String, HomekitCharacteristicType> TAG_MAP = new HashMap<>();
 
index a3c15b48173f223a59a939b5a52444452bafd25d..abd8685b31610241f4008ead8b41e6cb60d2b76b 100644 (file)
@@ -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 (file)
index 0000000..aab76fd
--- /dev/null
@@ -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<ProgramModeEnum, String> programModeMap;
+    private static final String SERVICE_LABEL = "ServiceLabel";
+
+    public HomekitIrrigationSystemImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> 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<ActiveEnum> getActive() {
+        OnOffType state = getStateAs(HomekitCharacteristicType.ACTIVE, OnOffType.class);
+        return CompletableFuture.completedFuture(state == OnOffType.ON ? ActiveEnum.ACTIVE : ActiveEnum.INACTIVE);
+    }
+
+    @Override
+    public CompletableFuture<Void> setActive(ActiveEnum value) {
+        getCharacteristic(HomekitCharacteristicType.ACTIVE).ifPresent(tItem -> {
+            tItem.send(value == ActiveEnum.ACTIVE ? OnOffType.ON : OnOffType.OFF);
+        });
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public CompletableFuture<InUseEnum> getInUse() {
+        return CompletableFuture.completedFuture(inUseReader.getValue() ? InUseEnum.IN_USE : InUseEnum.NOT_IN_USE);
+    }
+
+    @Override
+    public CompletableFuture<ProgramModeEnum> 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);
+    }
+}
index 0458a9bacb142cbf7f73f9d351fa91f265c83698..808d833a1f6e53e254b562b47f3bff1c6da6a0a1 100644 (file)
@@ -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 -> {
index 8f96d0a10da752ae49ab917e2a64703e210c2b76..f159b6fdac8a975e31e1dceab3cff36ef761e4d6 100644 (file)
@@ -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<HomekitTaggedItem> 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<ValveTypeEnum> 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;
+    }
 }