Skip to content

Commit

Permalink
[homekit] implement StatelessProgrammableSwitch
Browse files Browse the repository at this point in the history
also supports adding multiple of them in a group, by supporting ServiceIndex
as an optional characteristic

refs openhab#9969

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer committed Jul 16, 2024
1 parent 63862c9 commit 2609b4e
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 239 deletions.
460 changes: 238 additions & 222 deletions bundles/org.openhab.io.homekit/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public enum HomekitAccessoryType {
SMART_SPEAKER("SmartSpeaker"),
SMOKE_SENSOR("SmokeSensor"),
SPEAKER("Speaker"),
STATELESS_PROGRAMMABLE_SWITCH("StatelessProgrammableSwitch"),
SWITCH("Switchable"),
TELEVISION("Television"),
TELEVISION_SPEAKER("TelevisionSpeaker"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.items.GenericItem;
Expand All @@ -35,7 +36,7 @@
*/
public class HomekitAccessoryUpdater {
private final Logger logger = LoggerFactory.getLogger(HomekitAccessoryUpdater.class);
private final ConcurrentMap<ItemKey, Subscription> subscriptionsByName = new ConcurrentHashMap<>();
private final ConcurrentMap<ItemKey, StateChangeListener> subscriptionsByName = new ConcurrentHashMap<>();

public void subscribe(GenericItem item, HomekitCharacteristicChangeCallback callback) {
subscribe(item, null, callback);
Expand Down Expand Up @@ -63,6 +64,28 @@ public void subscribe(GenericItem item, String key, HomekitCharacteristicChangeC
});
}

public void subscribeToUpdates(GenericItem item, String key, Consumer<State> callback) {
logger.trace("Received subscription request for {} / {}", item, key);
if (item == null) {
return;
}
if (callback == null) {
logger.trace("The received subscription contains a null callback, skipping");
return;
}
ItemKey itemKey = new ItemKey(item, key);
subscriptionsByName.compute(itemKey, (k, v) -> {
if (v != null) {
logger.debug("Received duplicate subscription for {} / {}", item, key);
unsubscribe(item, key);
}
logger.trace("Adding subscription for {} / {}", item, key);
UpdateSubscription subscription = (changedItem, newState) -> callback.accept(newState);
item.addStateChangeListener(subscription);
return subscription;
});
}

public void unsubscribe(GenericItem item) {
unsubscribe(item, null);
}
Expand Down Expand Up @@ -91,6 +114,19 @@ default void stateUpdated(Item item, State state) {
}
}

@FunctionalInterface
@NonNullByDefault
private interface UpdateSubscription extends StateChangeListener {

@Override
default void stateChanged(Item item, State oldState, State newState) {
// Do nothing on change update
}

@Override
void stateUpdated(Item item, State state);
}

private static class ItemKey {
public final GenericItem item;
public final String key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public enum HomekitCharacteristicType {
POSITION_STATE("PositionState"),
POWER_MODE("PowerMode"),
PROGRAM_MODE("ProgramMode"),
PROGRAMMABLE_SWITCH_EVENT("ProgrammableSwitchEvent"),
RELATIVE_HUMIDITY("RelativeHumidity"),
REMAINING_DURATION("RemainingDuration"),
REMOTE_KEY("RemoteKey"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@ public <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map
T defaultValue) {
final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
if (c.isPresent()) {
return HomekitCharacteristicFactory.getKeyFromMapping(c.get(), mapping, defaultValue);
return HomekitCharacteristicFactory.getKeyFromMapping(c.get(), c.get().getItem().getState(), mapping,
defaultValue);
}
return defaultValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class HomekitAccessoryFactory {
put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
put(STATELESS_PROGRAMMABLE_SWITCH, new HomekitCharacteristicType[] { PROGRAMMABLE_SWITCH_EVENT });
put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE });
put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE });
Expand Down Expand Up @@ -145,6 +146,7 @@ public class HomekitAccessoryFactory {
put(SMART_SPEAKER, HomekitSmartSpeakerImpl.class);
put(SMOKE_SENSOR, HomekitSmokeSensorImpl.class);
put(SPEAKER, HomekitSpeakerImpl.class);
put(STATELESS_PROGRAMMABLE_SWITCH, HomekitStatelessProgrammableSwitchImpl.class);
put(SWITCH, HomekitSwitchImpl.class);
put(TELEVISION, HomekitTelevisionImpl.class);
put(TELEVISION_SPEAKER, HomekitTelevisionSpeakerImpl.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
import org.openhab.io.homekit.internal.HomekitException;
import org.openhab.io.homekit.internal.HomekitSettings;
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.impl.common.ServiceLabelNamespaceCharacteristic;
import io.github.hapjava.services.impl.ServiceLabelService;

/**
* Bare accessory (for being the root of a multi-service accessory).
Expand All @@ -33,4 +36,12 @@ public HomekitAccessoryGroupImpl(HomekitTaggedItem taggedItem, List<HomekitTagge
throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
}

@Override
public void init() throws HomekitException {
super.init();

getCharacteristic(ServiceLabelNamespaceCharacteristic.class)
.ifPresent(c -> getServices().add(new ServiceLabelService(c)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.common.ObstructionDetectedCharacteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEnum;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
import io.github.hapjava.characteristics.impl.common.StatusActiveCharacteristic;
import io.github.hapjava.characteristics.impl.common.StatusFaultCharacteristic;
import io.github.hapjava.characteristics.impl.common.StatusFaultEnum;
Expand Down Expand Up @@ -231,6 +233,7 @@ public class HomekitCharacteristicFactory {
put(PM10_DENSITY, HomekitCharacteristicFactory::createPM10DensityCharacteristic);
put(PM25_DENSITY, HomekitCharacteristicFactory::createPM25DensityCharacteristic);
put(POWER_MODE, HomekitCharacteristicFactory::createPowerModeCharacteristic);
put(PROGRAMMABLE_SWITCH_EVENT, HomekitCharacteristicFactory::createProgrammableSwitchEventCharacteristic);
put(REMAINING_DURATION, HomekitCharacteristicFactory::createRemainingDurationCharacteristic);
put(REMOTE_KEY, HomekitCharacteristicFactory::createRemoteKeyCharacteristic);
put(RELATIVE_HUMIDITY, HomekitCharacteristicFactory::createRelativeHumidityCharacteristic);
Expand Down Expand Up @@ -391,8 +394,7 @@ public static <T extends Enum<T> & CharacteristicEnum> Map<T, String> createMapp
* @param <T> type of the result derived from
* @return key for the value
*/
public static <T> T getKeyFromMapping(HomekitTaggedItem item, Map<T, String> mapping, T defaultValue) {
final State state = item.getItem().getState();
public static <T> T getKeyFromMapping(HomekitTaggedItem item, State state, Map<T, String> mapping, T defaultValue) {
LOGGER.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", item.getAccessoryType().getTag(),
state, mapping);

Expand Down Expand Up @@ -448,7 +450,8 @@ public static Unit<Temperature> getSystemTemperatureUnit() {

private static <T extends CharacteristicEnum> CompletableFuture<T> getEnumFromItem(HomekitTaggedItem item,
Map<T, String> mapping, T defaultValue) {
return CompletableFuture.completedFuture(getKeyFromMapping(item, mapping, defaultValue));
return CompletableFuture
.completedFuture(getKeyFromMapping(item, item.getItem().getState(), mapping, defaultValue));
}

public static <T extends Enum<T>> void setValueFromEnum(HomekitTaggedItem taggedItem, T value, Map<T, String> map) {
Expand Down Expand Up @@ -1160,6 +1163,80 @@ private static PowerModeCharacteristic createPowerModeCharacteristic(HomekitTagg
return new PowerModeCharacteristic((value) -> setValueFromEnum(taggedItem, value, map));
}

// this characteristic is unique in a few ways, so we can't use the "normal" helpers:
// * you don't return a "current" value, just the value of the most recent event
// * NULL/invalid values are very much expected, and should silently _not_ trigger an event
// * every update to the item should trigger an event, not just changes

private static ProgrammableSwitchEventCharacteristic createProgrammableSwitchEventCharacteristic(
HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) {
// have to build the map custom, since SINGLE_PRESS starts at 0
Map<ProgrammableSwitchEnum, String> map = new EnumMap(ProgrammableSwitchEnum.class);
List<ProgrammableSwitchEnum> validValues = new ArrayList<>();

if (taggedItem.getBaseItem().getAcceptedDataTypes().contains(OnOffType.class)) {
map.put(ProgrammableSwitchEnum.SINGLE_PRESS, OnOffType.ON.toString());
validValues.add(ProgrammableSwitchEnum.SINGLE_PRESS);
} else if (taggedItem.getBaseItem().getAcceptedDataTypes().contains(OpenClosedType.class)) {
map.put(ProgrammableSwitchEnum.SINGLE_PRESS, OpenClosedType.OPEN.toString());
validValues.add(ProgrammableSwitchEnum.SINGLE_PRESS);
} else {
map = createMapping(taggedItem, ProgrammableSwitchEnum.class, validValues, false);
}

var helper = new ProgrammableSwitchEventCharacteristicHelper(taggedItem, updater, map);

return new ProgrammableSwitchEventCharacteristic(validValues.toArray(new ProgrammableSwitchEnum[0]),
helper::getValue, helper::subscribe, getUnsubscriber(taggedItem, PROGRAMMABLE_SWITCH_EVENT, updater));
}

private static class ProgrammableSwitchEventCharacteristicHelper {
private @Nullable ProgrammableSwitchEnum lastValue = null;
private final HomekitTaggedItem taggedItem;
private final Map<ProgrammableSwitchEnum, String> map;
private final HomekitAccessoryUpdater updater;

ProgrammableSwitchEventCharacteristicHelper(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater,
Map<ProgrammableSwitchEnum, String> map) {
this.taggedItem = taggedItem;
this.map = map;
this.updater = updater;
}

public CompletableFuture<ProgrammableSwitchEnum> getValue() {
return CompletableFuture.completedFuture(lastValue);
}

public void subscribe(HomekitCharacteristicChangeCallback cb) {
updater.subscribeToUpdates((GenericItem) taggedItem.getItem(), PROGRAMMABLE_SWITCH_EVENT.getTag(),
state -> {
// perform inversion here, so logic below only needs to deal with the
// canonical style
if (state instanceof OnOffType && taggedItem.isInverted()) {
if (state.equals(OnOffType.ON)) {
state = OnOffType.OFF;
} else {
state = OnOffType.ON;
}
} else if (state instanceof OpenClosedType && taggedItem.isInverted()) {
if (state.equals(OpenClosedType.OPEN)) {
state = OpenClosedType.CLOSED;
} else {
state = OpenClosedType.OPEN;
}
}
// if "not pressed", don't send an event
if (state instanceof UnDefType || (state instanceof OnOffType && state.equals(OnOffType.OFF))
|| (state instanceof OpenClosedType && state.equals(OpenClosedType.CLOSED))) {
lastValue = null;
return;
}
lastValue = getKeyFromMapping(taggedItem, state, map, ProgrammableSwitchEnum.SINGLE_PRESS);
cb.changed();
});
}
}

private static RemainingDurationCharacteristic createRemainingDurationCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new RemainingDurationCharacteristic(getIntSupplier(taggedItem, 0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
public class HomekitIrrigationSystemImpl extends AbstractHomekitAccessoryImpl implements IrrigationSystemAccessory {
private Map<InUseEnum, String> inUseMapping;
private Map<ProgramModeEnum, String> programModeMap;
private static final String SERVICE_LABEL = "ServiceLabel";

public HomekitIrrigationSystemImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings)
Expand All @@ -64,17 +63,9 @@ public HomekitIrrigationSystemImpl(HomekitTaggedItem taggedItem, List<HomekitTag
public void init() throws HomekitException {
super.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)));
var serviceLabelNamespace = getCharacteristic(ServiceLabelNamespaceCharacteristic.class)
.orElseGet(() -> new ServiceLabelNamespaceCharacteristic(
() -> CompletableFuture.completedFuture(ServiceLabelNamespaceEnum.ARABIC_NUMERALS)));
addService(new ServiceLabelService(serviceLabelNamespace));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
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.common.ServiceLabelNamespaceCharacteristic;
import io.github.hapjava.characteristics.impl.common.ServiceLabelNamespaceEnum;
import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateCharacteristic;
import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateEnum;
import io.github.hapjava.characteristics.impl.heatercooler.TargetHeaterCoolerStateCharacteristic;
Expand Down Expand Up @@ -101,6 +103,7 @@ public class HomekitMetadataCharacteristicFactory {
put(PICTURE_MODE, HomekitMetadataCharacteristicFactory::createPictureModeCharacteristic);
put(SERIAL_NUMBER, HomekitMetadataCharacteristicFactory::createSerialNumberCharacteristic);
put(SERVICE_INDEX, HomekitMetadataCharacteristicFactory::createServiceIndexCharacteristic);
put(SERVICE_LABEL, HomekitMetadataCharacteristicFactory::createServiceLabelNamespaceCharacteristic);
put(SLEEP_DISCOVERY_MODE, HomekitMetadataCharacteristicFactory::createSleepDiscoveryModeCharacteristic);
put(TARGET_HEATER_COOLER_STATE,
HomekitMetadataCharacteristicFactory::createTargetHeaterCoolerStateCharacteristic);
Expand Down Expand Up @@ -292,6 +295,10 @@ private static Characteristic createServiceIndexCharacteristic(Object value) {
return new ServiceLabelIndexCharacteristic(getInteger(value));
}

private static Characteristic createServiceLabelNamespaceCharacteristic(Object value) {
return new ServiceLabelNamespaceCharacteristic(getEnum(value, ServiceLabelNamespaceEnum.class));
}

private static Characteristic createSleepDiscoveryModeCharacteristic(Object value) {
return new SleepDiscoveryModeCharacteristic(getEnum(value, SleepDiscoveryModeEnum.class,
SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE, SleepDiscoveryModeEnum.NOT_DISCOVERABLE), v -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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 org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
import org.openhab.io.homekit.internal.HomekitException;
import org.openhab.io.homekit.internal.HomekitSettings;
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
import io.github.hapjava.services.impl.StatelessProgrammableSwitchService;

/**
* Implements a HomeKit Stateless Programmable Switch
*
* @author Cody Cutrer - Initial contribution
*/
class HomekitStatelessProgrammableSwitchImpl extends AbstractHomekitAccessoryImpl {

public HomekitStatelessProgrammableSwitchImpl(HomekitTaggedItem taggedItem,
List<HomekitTaggedItem> mandatoryCharacteristics, List<Characteristic> mandatoryRawCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
}

@Override
public void init() throws HomekitException {
super.init();

addService(new StatelessProgrammableSwitchService(
getCharacteristic(ProgrammableSwitchEventCharacteristic.class).get()));
}
}

0 comments on commit 2609b4e

Please sign in to comment.