Skip to content

Commit

Permalink
[homekit] Allow configuring thermostat modes via metadata (#17056)
Browse files Browse the repository at this point in the history
* [homekit] allow configuring thermostat modes via metadata

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer committed Jul 14, 2024
1 parent 674e545 commit 3af703c
Show file tree
Hide file tree
Showing 44 changed files with 352 additions and 326 deletions.
2 changes: 2 additions & 0 deletions bundles/org.openhab.io.homekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,8 @@ All accessories also support the following optional characteristic that can be l
| | | CoolingThresholdTemperature | Number | Maximum temperature that must be reached before cooling is turned on | minValue (10), maxValue (35), step (0.1) | |
| | | HeatingThresholdTemperature | Number | Minimum temperature that must be reached before heating is turned on | minValue (0), maxValue (25), step (0.1) | |
| | | RelativeHumidity | Number | Relative humidity in % between 0 and 100. | | |
| | | TargetRelativeHumidity | Number | Target relative humidity in % between 0 and 100. | | |
| | | TemperatureUnit | Number, String, Switch | The units the accessory itself uses to display the temperature. Can also be configured via metadata, e.g. [TemperatureUnit="CELSIUS"] | | CELSIUS (0, OFF), FAHRENHEIT (1, ON) |
| Valve | | | | Valve | ValveType = ["Generic", "Irrigation", "Shower", "Faucet"] ("Generic") | |
| | ActiveStatus | | Dimmer, Switch | Accessory current working status. A value of "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. | | |
| | InUseStatus | | Contact, Dimmer, Switch | Indicates whether fluid flowing through the valve. A value of "ON"/"OPEN" indicates that fluid is flowing. | inverted (false) | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public enum HomekitCharacteristicType {
CURRENT_HEATING_COOLING_STATE("CurrentHeatingCoolingMode"),
TARGET_TEMPERATURE("TargetTemperature"),
TEMPERATURE_UNIT("TemperatureUnit"),
TARGET_RELATIVE_HUMIDITY("TargetRelativeHumidity"),

LOCK_CURRENT_STATE("LockCurrentState"),
LOCK_TARGET_STATE("LockTargetState"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
private final Map<Class<? extends Characteristic>, Characteristic> rawCharacteristics;
private boolean isLinkedService = false;

public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTaggedItem> characteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) {
this.characteristics = characteristics;
public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTaggedItem> mandatoryCharacteristics,
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater,
HomekitSettings settings) {
this.characteristics = mandatoryCharacteristics;
this.accessory = accessory;
this.updater = updater;
this.services = new ArrayList<>();
Expand All @@ -88,6 +89,15 @@ public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTag
rawCharacteristics.put(rawCharacteristic.getClass(), rawCharacteristic);
}
});
mandatoryRawCharacteristics.forEach(c -> {
if (rawCharacteristics.get(c.getClass()) != null) {
logger.warn(
"Accessory {} already has a characteristic of type {}; ignoring additional definition from metadata.",
accessory.getName(), c.getClass().getSimpleName());
} else {
rawCharacteristics.put(c.getClass(), c);
}
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.windowcovering.PositionStateEnum;

Expand All @@ -58,9 +59,9 @@ abstract class AbstractHomekitPositionAccessoryImpl extends AbstractHomekitAcces
protected PositionStateEnum emulatedState = PositionStateEnum.STOPPED;

public AbstractHomekitPositionAccessoryImpl(HomekitTaggedItem taggedItem,
List<HomekitTaggedItem> mandatoryCharacteristics, HomekitAccessoryUpdater updater,
HomekitSettings settings) {
super(taggedItem, mandatoryCharacteristics, updater, settings);
List<HomekitTaggedItem> mandatoryCharacteristics, List<Characteristic> mandatoryRawCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
final boolean inverted = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.INVERTED, true);
emulateState = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_STATE, false);
emulateStopSameDirection = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_SAME_DIRECTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@ private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem,
throws HomekitException {
final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
LOGGER.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
metadataRegistry);
final List<HomekitTaggedItem> characteristics = new ArrayList<>();
final List<Characteristic> rawCharacteristics = new ArrayList<>();

getMandatoryCharacteristicsFromItem(taggedItem, metadataRegistry, characteristics, rawCharacteristics);
final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
LOGGER.warn("Accessory of type {} must have following characteristics {}. Found only {}",
accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
if (characteristics.size() + rawCharacteristics.size() < mandatoryCharacteristics.size()) {
LOGGER.warn("Accessory of type {} must have following characteristics {}. Found only {}, {}",
accessoryType.getTag(), mandatoryCharacteristics, characteristics, rawCharacteristics);
throw new HomekitException("Missing mandatory characteristics");
}
AbstractHomekitAccessoryImpl accessoryImpl;
Expand All @@ -210,9 +212,10 @@ private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem,
taggedItem.getName());
throw new HomekitException("Circular accessory references");
}
accessoryImpl = accessoryImplClass.getConstructor(HomekitTaggedItem.class, List.class,
HomekitAccessoryUpdater.class, HomekitSettings.class)
.newInstance(taggedItem, foundCharacteristics, updater, settings);
accessoryImpl = accessoryImplClass
.getConstructor(HomekitTaggedItem.class, List.class, List.class, HomekitAccessoryUpdater.class,
HomekitSettings.class)
.newInstance(taggedItem, characteristics, rawCharacteristics, updater, settings);
addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
accessoryImpl.setIsLinkedService(!ancestorServices.isEmpty());
Expand Down Expand Up @@ -298,18 +301,18 @@ public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemReg
* @param metadataRegistry meta data registry
* @return list of mandatory
*/
private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
MetadataRegistry metadataRegistry) {
List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
private static void getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
MetadataRegistry metadataRegistry, List<HomekitTaggedItem> characteristics,
List<Characteristic> rawCharacteristics) {
if (taggedItem.isGroup()) {
for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, item, metadataRegistry);
}
} else {
addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, taggedItem.getItem(),
metadataRegistry);
}
LOGGER.trace("Mandatory characteristics: {}", collectedCharacteristics);
return collectedCharacteristics;
LOGGER.trace("Mandatory characteristics: {}, {}", characteristics, rawCharacteristics);
}

/**
Expand All @@ -325,7 +328,7 @@ private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(Homek
* @param metadataRegistry meta date registry
*/
private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
Item item, MetadataRegistry metadataRegistry) {
List<Characteristic> rawCharacteristics, Item item, MetadataRegistry metadataRegistry) {
// get list of mandatory characteristics
List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
if (mandatoryCharacteristics.isEmpty()) {
Expand Down Expand Up @@ -362,6 +365,23 @@ && isMandatoryCharacteristic(mainItem, characteristic)) {
}
}
}
mandatoryCharacteristics.forEach(c -> {
// Check every metadata key looking for a characteristics we can create
var config = mainItem.getConfiguration();
if (config == null) {
return;
}
for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
.collect(Collectors.toList())) {
var type = HomekitCharacteristicType.valueOfTag(entry.getKey());
if (type.isPresent() && isMandatoryCharacteristic(mainItem, type.get())) {
var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(type.get(),
entry.getValue());

characteristic.ifPresent(rc -> rawCharacteristics.add(rc));
}
}
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.openhab.io.homekit.internal.HomekitSettings;
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.characteristics.Characteristic;

/**
* Bare accessory (for being the root of a multi-service accessory).
*
Expand All @@ -27,7 +29,8 @@
@NonNullByDefault
public class HomekitAccessoryGroupImpl extends AbstractHomekitAccessoryImpl {
public HomekitAccessoryGroupImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings)
throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.accessories.AirQualityAccessory;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.airquality.AirQualityEnum;
import io.github.hapjava.services.impl.AirQualityService;
Expand All @@ -37,8 +38,9 @@ public class HomekitAirQualitySensorImpl extends AbstractHomekitAccessoryImpl im
private final Map<AirQualityEnum, String> qualityStateMapping;

public HomekitAirQualitySensorImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings)
throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
qualityStateMapping = createMapping(AIR_QUALITY, AirQualityEnum.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.accessories.BasicFanAccessory;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.services.impl.BasicFanService;

Expand All @@ -37,8 +38,9 @@ class HomekitBasicFanImpl extends AbstractHomekitAccessoryImpl implements BasicF
private final BooleanItemReader onReader;

public HomekitBasicFanImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings)
throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
onReader = createBooleanReader(ON_STATE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.openhab.io.homekit.internal.HomekitTaggedItem;

import io.github.hapjava.accessories.BatteryAccessory;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.battery.ChargingStateEnum;
import io.github.hapjava.characteristics.impl.battery.StatusLowBatteryEnum;
Expand All @@ -48,8 +49,9 @@ public class HomekitBatteryImpl extends AbstractHomekitAccessoryImpl implements
private final BigDecimal lowThreshold;

public HomekitBatteryImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings)
throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
lowThreshold = getAccessoryConfiguration(HomekitCharacteristicType.BATTERY_LOW_STATUS,
HomekitTaggedItem.BATTERY_LOW_THRESHOLD, BigDecimal.valueOf(20));
lowBatteryReader = createBooleanReader(BATTERY_LOW_STATUS, lowThreshold, true);
Expand Down
Loading

0 comments on commit 3af703c

Please sign in to comment.