[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 <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2023-01-13 12:25:06 -07:00 committed by GitHub
parent ee54882841
commit 0de87b15d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 201 additions and 8 deletions

View 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

View 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<>();

View 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<>();

View 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);
}
};

View File

@ -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);
}
}

View 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 -> {

View 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;
}
}