Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[homekit] Implement StatelessProgrammableSwitch #17129

Merged
merged 1 commit into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
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(),
lsiepel marked this conversation as resolved.
Show resolved Hide resolved
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-2024 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()));
}
}