From 64d97374ad613ddf7922577043f66b4547ea7f6e Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 3 Oct 2022 04:13:39 -0600 Subject: [PATCH] [homekit] allow NumberItems for BatteryLowStatus (#13449) * [homekit] allow NumberItems for BatteryLowStatus use a lowThreshold metadata config to infer if it's low Signed-off-by: Cody Cutrer --- bundles/org.openhab.io.homekit/README.md | 26 +++++----- .../homekit/internal/HomekitTaggedItem.java | 13 ++--- .../AbstractHomekitAccessoryImpl.java | 20 ++++++++ .../accessories/BooleanItemReader.java | 51 ++++++++++++++++--- .../accessories/HomekitBatteryImpl.java | 7 ++- .../HomekitCharacteristicFactory.java | 9 +++- 6 files changed, 97 insertions(+), 29 deletions(-) diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index 8ef2b8578..198e22d1a 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -424,7 +424,7 @@ Following table summarizes the optional characteristics supported by sensors. | ActiveStatus | Switch, Contact | Accessory current working status. "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. | | 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. | | TamperedStatus | Switch, Contact | Accessory tampered status. "ON"/"OPEN" indicates that the accessory has been tampered. Value should return to "OFF"/"CLOSED" when the accessory has been reset to a non-tampered state. | -| BatteryLowStatus | Switch, Contact | Accessory battery status. "ON"/"OPEN" indicates that the battery level of the accessory is low. Value should return to "OFF"/"CLOSED" when the battery charges to a level thats above the low threshold. | +| BatteryLowStatus | Switch, Contact, Number | Accessory battery status. "ON"/"OPEN" indicates that the battery level of the accessory is low. Value should return to "OFF"/"CLOSED" when the battery charges to a level that's above the low threshold. Alternatively, you can give a Number item that's the battery level, and if it's lower than the lowThreshold configuration, it will report low. | Switch and Contact items support inversion of the state mapping, e.g. by default the openHAB switch state "ON" is mapped to HomeKit contact sensor state "Open", and "OFF" to "Closed". The configuration "inverted=true" inverts this mapping, so that "ON" will be mapped to "Closed" and "OFF" to "Open". @@ -555,63 +555,63 @@ Support for this is planned for the future release of openHAB HomeKit binding. | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | LeakSensor | | | | Leak Sensor | | | LeakDetectedState | | Switch, Contact | Leak sensor state (ON=Leak Detected, OFF=no leak) | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | MotionSensor | | | | Motion Sensor | | | MotionDetectedState | | Switch, Contact | Motion sensor state (ON=motion detected, OFF=no motion) | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | OccupancySensor | | | | Occupancy Sensor | | | OccupancyDetectedState | | Switch, Contact | Occupancy sensor state (ON=occupied, OFF=not occupied) | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | ContactSensor | | | | Contact Sensor, An accessory with on/off state that can be viewed in HomeKit but not changed such as a contact sensor for a door or window | | | ContactSensorState | | Switch, Contact | Contact sensor state (ON=open, OFF=closed) | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | SmokeSensor | | | | Smoke Sensor | | | SmokeDetectedState | | Switch, Contact | Smoke sensor state (ON=smoke detected, OFF=no smoke) | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | LightSensor | | | | Light sensor | | | LightLevel | | Number | Light level in lux | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | HumiditySensor | | | | Relative Humidity Sensor providing read-only values | | | RelativeHumidity | | Number | Relative humidity in % between 0 and 100. additional configuration homekitMultiplicator = . | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | TemperatureSensor | | | | Temperature sensor | | | CurrentTemperature | | Number | current temperature. supported configuration: minValue, maxValue, step. | | | | Name | String | Name of the sensor | | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | CarbonDioxideSensor | | | | Carbon Dioxide Sensor | | | CarbonDioxideDetectedState | | Switch, Contact | carbon dioxide sensor state (ON- abnormal level of carbon dioxide detected, OFF - level is normal) | | | | CarbonDioxideLevel | Number | Carbon dioxide level in ppm, max 100000 | @@ -620,7 +620,7 @@ Support for this is planned for the future release of openHAB HomeKit binding. | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | CarbonMonoxideSensor | | | | Carbon monoxide Sensor | | | CarbonMonoxideDetectedState | | Switch, Contact | Carbon monoxide sensor state (ON- abnormal level of carbon monoxide detected, OFF - level is normal) | | | | CarbonMonoxideLevel | Number | Carbon monoxide level in ppm, max 100 | @@ -629,7 +629,7 @@ Support for this is planned for the future release of openHAB HomeKit binding. | | | ActiveStatus | Switch, Contact | Working status | | | | FaultStatus | Switch, Contact | Fault status | | | | TamperedStatus | Switch, Contact | Tampered status | -| | | BatteryLowStatus | Switch, Contact | Battery status | +| | | BatteryLowStatus | Switch, Contact, Number | Battery status | | Door | | | | Motorized door. One Rollershutter item covers all mandatory characteristics. see examples below. | | | CurrentPosition | | Rollershutter, Dimmer, Number | Current position of motorized door | | | TargetPosition | | Rollershutter, Dimmer, Number | Target position of motorized door | @@ -732,7 +732,7 @@ Support for this is planned for the future release of openHAB HomeKit binding. | | | LockTargetState | Switch | target states of lock mechanism (OFF=SECURED, ON=UNSECURED) | | Battery | | | | Accessory with battery. Battery can be chargeable (configuration chargeable:true) and non-chargeable (configuration chargeable:false) | | | BatteryLevel | | Number | Battery level 0% to 100% | -| | BatteryLowStatus | | Switch, Contact | Battery low indicator. ON/OPEN = battery level is low. | +| | BatteryLowStatus | | Switch, Contact, Number | Battery low indicator. ON/OPEN = battery level is low; for number if the value is below the lowThreshold, then it is low. Default is 20. | | | BatteryChargingState | | Switch, Contact | Mandatory only for chargeable battery. ON/OPEN = battery is charging | | | | Name | String | Name of the battery accessory | | Filter | | | | Accessory with filter maintenance indicator | diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java index 79ef8af86..522b063d1 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitTaggedItem.java @@ -45,14 +45,15 @@ public class HomekitTaggedItem { private final Logger logger = LoggerFactory.getLogger(HomekitTaggedItem.class); /** configuration keywords at items level **/ - public final static String MIN_VALUE = "minValue"; - public final static String MAX_VALUE = "maxValue"; - public final static String STEP = "step"; - public final static String DIMMER_MODE = "dimmerMode"; public final static String DELAY = "commandDelay"; - public final static String INVERTED = "inverted"; - public final static String PRIMARY_SERVICE = "primary"; + public final static String DIMMER_MODE = "dimmerMode"; + public static final String BATTERY_LOW_THRESHOLD = "lowThreshold"; public final static String INSTANCE = "instance"; + public final static String INVERTED = "inverted"; + public final static String MAX_VALUE = "maxValue"; + public final static String MIN_VALUE = "minValue"; + public final static String PRIMARY_SERVICE = "primary"; + public final static String STEP = "step"; private static final Map CREATED_ACCESSORY_IDS = new ConcurrentHashMap<>(); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java index 620452440..1c0d3fb3e 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java @@ -12,6 +12,7 @@ */ package org.openhab.io.homekit.internal.accessories; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -314,6 +315,25 @@ abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory { trueOnOffValue, trueOpenClosedValue); } + /** + * create boolean reader for a number item with ON state mapped to the value of the + * item being above a given threshold + * + * @param characteristicType characteristic id + * @param trueThreshold threshold for true of number item + * @param invertThreshold result is true if item is less than threshold, instead of more + * @return boolean read + * @throws IncompleteAccessoryException + */ + @NonNullByDefault + protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType, + BigDecimal trueThreshold, boolean invertThreshold) throws IncompleteAccessoryException { + final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType) + .orElseThrow(() -> new IncompleteAccessoryException(characteristicType)); + return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON, + taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN, trueThreshold, invertThreshold); + } + /** * create boolean reader with default ON/OFF mapping considering inverted flag * diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java index a4ae7a124..b6862b9ec 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/BooleanItemReader.java @@ -12,14 +12,20 @@ */ package org.openhab.io.homekit.internal.accessories; +import java.math.BigDecimal; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; import org.openhab.core.library.items.ContactItem; +import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.State; import org.openhab.io.homekit.internal.HomekitOHItemProxy; @@ -38,6 +44,8 @@ public class BooleanItemReader { private final OnOffType trueOnOffValue; private final OpenClosedType trueOpenClosedValue; private final Logger logger = LoggerFactory.getLogger(BooleanItemReader.class); + private final @Nullable BigDecimal trueThreshold; + private final boolean invertThreshold; /** * @@ -46,29 +54,58 @@ public class BooleanItemReader { * @param trueOpenClosedValue if OpenClosedType, then consider true if this value */ BooleanItemReader(Item item, OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue) { + this(item, trueOnOffValue, trueOpenClosedValue, null, false); + } + + /** + * + * @param item The item to read + * @param trueOnOffValue If OnOffType, then consider true if this value + * @param trueOpenClosedValue if OpenClosedType, then consider true if this value + * @param trueThreshold If the state is numeric, and this param is given, return true if the value is above this + * threshold + * @param invertThreshold Invert threshold to be true if below, not above + */ + BooleanItemReader(Item item, OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue, + @Nullable BigDecimal trueThreshold, boolean invertThreshold) { this.item = item; this.trueOnOffValue = trueOnOffValue; this.trueOpenClosedValue = trueOpenClosedValue; final Item baseItem = HomekitOHItemProxy.getBaseItem(item); - if (!(baseItem instanceof SwitchItem) && !(baseItem instanceof ContactItem) - && !(baseItem instanceof StringItem)) { - logger.warn("Item {} is a {} instead of the expected SwitchItem, ContactItem or StringItem", item.getName(), - item.getClass().getName()); + this.trueThreshold = trueThreshold; + this.invertThreshold = invertThreshold; + if (!(baseItem instanceof SwitchItem || baseItem instanceof ContactItem || baseItem instanceof StringItem + || (trueThreshold != null && baseItem instanceof NumberItem))) { + if (trueThreshold != null) { + logger.warn("Item {} is a {} instead of the expected SwitchItem, ContactItem, NumberItem or StringItem", + item.getName(), item.getClass().getName()); + } else { + logger.warn("Item {} is a {} instead of the expected SwitchItem, ContactItem or StringItem", + item.getName(), item.getClass().getName()); + } } } boolean getValue() { final State state = item.getState(); + final BigDecimal localTrueThresheold = trueThreshold; if (state instanceof OnOffType) { return state.equals(trueOnOffValue); } else if (state instanceof OpenClosedType) { return state.equals(trueOpenClosedValue); } else if (state instanceof StringType) { return state.toString().equalsIgnoreCase("Open") || state.toString().equalsIgnoreCase("Opened"); - } else { - logger.debug("Unexpected item state, returning false. Item {}, State {}", item.getName(), state); - return false; + } else if (localTrueThresheold != null) { + if (state instanceof DecimalType) { + final boolean result = ((DecimalType) state).toBigDecimal().compareTo(localTrueThresheold) > 0; + return result ^ invertThreshold; + } else if (state instanceof QuantityType) { + final boolean result = ((QuantityType) state).toBigDecimal().compareTo(localTrueThresheold) > 0; + return result ^ invertThreshold; + } } + logger.debug("Unexpected item state, returning false. Item {}, State {}", item.getName(), state); + return false; } private OnOffType getOffValue(OnOffType onValue) { diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitBatteryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitBatteryImpl.java index a67fb8266..50ead3b31 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitBatteryImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitBatteryImpl.java @@ -16,12 +16,14 @@ import static org.openhab.io.homekit.internal.HomekitCharacteristicType.BATTERY_ import static org.openhab.io.homekit.internal.HomekitCharacteristicType.BATTERY_LEVEL; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.BATTERY_LOW_STATUS; +import java.math.BigDecimal; import java.util.List; import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.DecimalType; 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; @@ -42,11 +44,14 @@ public class HomekitBatteryImpl extends AbstractHomekitAccessoryImpl implements private final BooleanItemReader lowBatteryReader; private BooleanItemReader chargingBatteryReader; private final boolean isChargeable; + private final BigDecimal lowThreshold; public HomekitBatteryImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); - lowBatteryReader = createBooleanReader(BATTERY_LOW_STATUS); + lowThreshold = getAccessoryConfiguration(HomekitCharacteristicType.BATTERY_LOW_STATUS, + HomekitTaggedItem.BATTERY_LOW_THRESHOLD, BigDecimal.valueOf(20)); + lowBatteryReader = createBooleanReader(BATTERY_LOW_STATUS, lowThreshold, true); isChargeable = getAccessoryConfigurationAsBoolean(BATTERY_TYPE, false); if (isChargeable) { chargingBatteryReader = createBooleanReader(BATTERY_CHARGING_STATE); diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java index 22c86bae8..662addf6d 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitCharacteristicFactory.java @@ -405,9 +405,14 @@ public class HomekitCharacteristicFactory { // create method for characteristic private static StatusLowBatteryCharacteristic createStatusLowBatteryCharacteristic(HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) { + BigDecimal lowThreshold = taggedItem.getConfiguration(HomekitTaggedItem.BATTERY_LOW_THRESHOLD, + BigDecimal.valueOf(20)); + BooleanItemReader lowBatteryReader = new BooleanItemReader(taggedItem.getItem(), + taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON, + taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN, lowThreshold, true); return new StatusLowBatteryCharacteristic( - () -> getEnumFromItem(taggedItem, StatusLowBatteryEnum.NORMAL, StatusLowBatteryEnum.LOW, - StatusLowBatteryEnum.NORMAL), + () -> CompletableFuture.completedFuture( + lowBatteryReader.getValue() ? StatusLowBatteryEnum.LOW : StatusLowBatteryEnum.NORMAL), getSubscriber(taggedItem, BATTERY_LOW_STATUS, updater), getUnsubscriber(taggedItem, BATTERY_LOW_STATUS, updater)); }