Skip to content

Commit

Permalink
[homekit] implement StatelessProgrammableSwitch
Browse files Browse the repository at this point in the history
refs openhab#9969

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer committed Jan 10, 2023
1 parent 0d69e61 commit 91a976a
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 227 deletions.
447 changes: 225 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 @@ -386,7 +386,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 @@ -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 @@ -232,6 +234,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 @@ -385,8 +388,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 @@ -432,7 +434,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 @@ -1146,6 +1149,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
@@ -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();

getServices().add(new StatelessProgrammableSwitchService(
getCharacteristic(ProgrammableSwitchEventCharacteristic.class).get()));
}
}

0 comments on commit 91a976a

Please sign in to comment.