[homekit] Add support for TV accessory (#14055)

* [homekit] TV accessory

Now possible since we support multiple secondary services. Just need to explicitly
declare that InputSource is a linked service to a Television, not just a secondary
service.

Note also that since TV and related services have so many mandatary characteristics
that are often static, I introduced a new way to declare characteristics -
via metadata on the service's item. Honestly, I feel like it's a lot cleaner to
have a factory create the mandatory characteristics the same way as the optional
characteristics, and then construct the service ourselves instead of basing the
service on the specific accessory interface. But this commit is already big enough,
I didn't want to go refactoring _all_ of the accessories to do it that way just
yet. This is why I have "unused" metadata characteristic factory methods for
AirQuality, HeaterCooler, and Thermostat - I started to make those configurable
via metadata, then realized they were mandatory characteristics that couldn't
be found from metadata via the current infrastructure.

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2023-01-12 07:29:50 -07:00 committed by GitHub
parent 03a67d25b1
commit 32909fa237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1102 additions and 98 deletions

View File

@ -40,6 +40,7 @@ HomeKit integration supports following accessory types:
- Carbon Monoxide Sensor
- Battery
- Filter Maintenance
- Television
## Quick start
@ -313,11 +314,11 @@ For complete list of supported accessory types and characteristics please see se
### Dimmers
The way HomeKit handles dimmer devices can be different to the actual dimmers' way of working.
HomeKit home app sends following commands/update:
HomeKit Home app sends following commands/update:
- On brightness change home app sends "ON" event along with target brightness, e.g. "Brightness = 50%" + "State = ON".
- On "ON" event home app sends "ON" along with brightness 100%, i.e. "Brightness = 100%" + "State = ON"
- On "OFF" event home app sends "OFF" without brightness information.
- On brightness change Home app sends "ON" event along with target brightness, e.g. "Brightness = 50%" + "State = ON".
- On "ON" event Home app sends "ON" along with brightness 100%, i.e. "Brightness = 100%" + "State = ON"
- On "OFF" event Home app sends "OFF" without brightness information.
However, some dimmer devices for example do not expect brightness on "ON" event, some others do not expect "ON" upon brightness change.
In order to support different devices HomeKit integration can filter some events. Which events should be filtered is defined via dimmerMode configuration.
@ -373,6 +374,55 @@ Dimmer light_temp { homekit="Lighting.ColorTemperature"[ minValue="2700 K", maxV
Dimmer light_temp { homekit="Lighting.ColorTemperature"[ minValue="2700 K", maxValue="5000 K", inverted=true ]}
```
### Television
HomeKit Televisions are represented as a complex accessory with multiple associated services.
The base service is a Television.
Then you need to add one or more InputSource services to describe the possible inputs.
Finally you can add a TelevisionSpeaker to have control of the audio.
A minimal example relying on multiple defaults with a single input, and no speaker:
```java
Group gTelevision "Television" { homekit="Television" }
Switch Television_Switch "Power" (gTelevision) { homekit="Television.Active" }
Group gInput1 "Input 1" (gTelevision) { homekit="InputSource" }
```
Or, you can go nuts, and fill out many of the optional characteristics, to fully customize your TV:
```java
Group gTelevision "Television" { homekit="Television" }
Switch Television_Switch "Power" (gTelevision) { homekit="Television.Active" }
String Television_Name "Name" (gTelevision) { homekit="Television.ConfiguredName" }
Number Television_CurrentInput "Current Input" (gTelevision) { homekit="Television.ActiveIdentifier" }
String Television_RemoteKey "Remote Key" (gTelevision) { homekit="Television.RemoteKey" }
Switch Television_SleepDiscoveryMode "Sleep Discovery Mode" (gTelevision) { homekit="Television.SleepDiscoveryMode" }
Dimmer Television_Brightness "Brightness" (gTelevision) { homekit="Television.Brightness" }
Switch Television_PowerMode "Power Mode" (gTelevision) { homekit="Television.PowerMode" }
Switch Television_ClosedCaptions "Closed Captions" (gTelevision) { homekit="Television.ClosedCaptions" }
String Television_CurrentMediaState "Current Media State" (gTelevision) { homekit="Television.CurrentMediaState" }
String Television_TargetMediaState "Target Media State" (gTelevision) { homekit="Television.TargetMediaState" }
String Television_PictureMode "Picture Mode" (gTelevision) { homekit="Television.PictureMode" }
Group gInput1 "Input 1" (gTelevision) { homekit="InputSource" }
Switch Input1_Visible "Visibility" (gInput1) { homekit="InputSource.CurrentVisibility" }
Switch Input1_TargetVisibility "Target Visibility" (gInput1) { homekit="InputSource.TargetVisibilityState" }
Group gInput2 "Input 2" (gTelevision) { homekit="InputSource"[Identifier=2, InputDeviceType="AUDIO_SYSTEM", InputSourceType="HDMI"] }
String Input2_Name "Name" (gInput2) { homekit="InputSource.ConfiguredName" }
Switch Input2_Configured "Configured" (gInput2) { homekit="InputSource.Configured" }
Switch Input2_Visible "Visibility" (gInput2) { homekit="InputSource.CurrentVisibility" }
Switch Input2_TargetVisibility "Target Visibility" (gInput2) { homekit="InputSource.TargetVisibilityState" }
Group gTelevisionSpeaker "Speaker" (gTelevision) { homekit="TelevisionSpeaker" }
Switch Television_Mute "Mute" (gTelevisionSpeaker) { homekit="TelevisionSpeaker.Mute" }
Switch Television_SpeakerActive "Speaker Active" (gTelevisionSpeaker) { homekit="TelevisionSpeaker.Active" }
Dimmer Television_Volume "Volume" (gTelevisionSpeaker) { homekit="TelevisionSpeaker.Volume,TelevisionSpeaker.VolumeSelector" }
```
Note that seemingly most of these characteristics are not accessible from the Home app.
At the least, you should be able to edit names, control main power, switch inputs, alter input visibility, and be notified when the user wants to open the TV's menu.
### Windows Covering (Blinds) / Window / Door
HomeKit Windows Covering, Window and Door accessory types have following mandatory characteristics:
@ -510,7 +560,7 @@ String thermostat_current_mode "Thermostat Current Mode" (gThermost
You can provide mapping for target mode in similar way.
The custom mapping at item level can be also used to reduce number of modes shown in home app. The modes can be only reduced, but not added, i.e. it is not possible to add new custom mode to HomeKit thermostat.
The custom mapping at item level can be also used to reduce number of modes shown in Home app. The modes can be only reduced, but not added, i.e. it is not possible to add new custom mode to HomeKit thermostat.
Example: if your thermostat does not support cooling, then you need to limit mapping to OFF and HEAT values only:
@ -531,7 +581,7 @@ The HomeKit valve accessory supports following 2 optional characteristics:
- remaining duration: this describes the remaining duration on the valve. Notifications on this characteristic must only be used if the remaining duration increases/decreases from the accessoryʼs usual countdown of remaining duration.
Upon valve activation in home app, home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any action if it remaining duration get 0.
Upon valve activation in Home app, Home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any action if it remaining duration get 0.
It is up to valve to have an own timer and stop valve once the timer is over.
Some valves have such timer, e.g. pretty common for sprinklers.
In case the valve has no timer capability, openHAB can take care on this - start an internal timer and send "Off" command to the valve once the timer is over.
@ -613,7 +663,7 @@ or using UI
| Accessory Tag | Mandatory Characteristics | Optional Characteristics | Supported OH items | Description |
|:---------------------|:----------------------------|:-----------------------------|:------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AirQualitySensor | | | | Air Quality Sensor which can measure different parameters |
| | AirQuality | | String | Air quality state, possible values (UNKNOWN,EXCELLENT,GOOD,FAIR,INFERIOR,POOR). Custom mapping can be defined at item level, e.g. [EXCELLENT="BEST", POOR="BAD"] |
| | AirQuality | | String | Air quality state, possible values (UNKNOWN,EXCELLENT,GOOD,FAIR,INFERIOR,POOR). Custom mapping can be defined at item level, e.g. [EXCELLENT="BEST", POOR="BAD"]. |
| | | OzoneDensity | Number | Ozone density in micrograms/m3, max 1000 |
| | | NitrogenDioxideDensity | Number | NO2 density in micrograms/m3, max 1000 |
| | | SulphurDioxideDensity | Number | SO2 density in micrograms/m3, max 1000 |
@ -759,7 +809,7 @@ or using UI
| Thermostat | | | | A thermostat requires all mandatory characteristics defined below |
| | CurrentTemperature | | Number | Current temperature. supported configuration: minValue, maxValue, step |
| | TargetTemperature | | Number | Target temperature. supported configuration: minValue, maxValue, step |
| | CurrentHeatingCoolingMode | | String | Current heating cooling mode (OFF, AUTO, HEAT, COOL). for mapping see homekit settings above. |
| | CurrentHeatingCoolingMode | | String | Current heating cooling mode (OFF, HEAT, COOL). for mapping see homekit settings above. |
| | TargetHeatingCoolingMode | | String | Target heating cooling mode (OFF, AUTO, HEAT, COOL). for mapping see homekit settings above. |
| | | Name | String | Name of the thermostat |
| | | CoolingThresholdTemperature | Number | Maximum temperature that must be reached before cooling is turned on. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] |
@ -768,8 +818,8 @@ or using UI
| HeaterCooler | | | | Heater or/and cooler device |
| | ActiveStatus | | Switch, Dimmer | Accessory current working status. A value of "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. |
| | CurrentTemperature | | Number | Current temperature. supported configuration: minValue, maxValue, step |
| | CurrentHeaterCoolerState | | String | Current heater/cooler mode (INACTIVE, IDLE, HEATING, COOLING). Mapping can be redefined at item level, e.g. [HEATING="HEAT", COOLING="COOL"] |
| | TargetHeaterCoolerState | | String | Target heater/cooler mode (AUTO, HEAT, COOL). Mapping can be redefined at item level, e.g. [AUTO="AUTOMATIC"] |
| | CurrentHeaterCoolerState | | String | Current heater/cooler mode (INACTIVE, IDLE, HEATING, COOLING). Mapping can be redefined at item level, e.g. [HEATING="HEAT", COOLING="COOL"]. |
| | TargetHeaterCoolerState | | String | Target heater/cooler mode (AUTO, HEAT, COOL). Mapping can be redefined at item level, e.g. [AUTO="AUTOMATIC"]. |
| | | Name | String | Name of the heater/cooler |
| | | RotationSpeed | Number | Fan rotation speed in % (1-100) |
| | | SwingMode | Number, Switch | Swing mode. values: 0/OFF=SWING DISABLED, 1/ON=SWING ENABLED |
@ -812,7 +862,7 @@ or using UI
| Filter | | | | Accessory with filter maintenance indicator |
| | FilterChangeIndication | | Switch, Contact, Dimmer | Filter change indicator. ON/OPEN = filter change is required. |
| | | FilterLifeLevel | Number | Current filter life level. 0% to 100% |
| | | FilterResetIndication | Switch | Send "filter reset" action triggered by user in iOS home app to openHAB ("ON" = reset requested by user). |
| | | FilterResetIndication | Switch | Send "filter reset" action triggered by user in iOS Home app to openHAB ("ON" = reset requested by user). |
| | | Name | String | Name of the filter accessory |
| Microphone | | | | Microphone accessory |
| | Mute | | Switch, Contact, Dimmer | Mute indication. ON/OPEN = microphone is muted |
@ -828,8 +878,36 @@ or using UI
| | TargetMediaState | | String | Target smart speaker state. possible values (STOP,PLAY,PAUSE). Custom mapping can be defined at item level, e.g. [STOP="STOPPED", PLAY="PLAYING"] |
| | | Mute | Switch, Contact | Mute indication. ON/OPEN = speaker is muted |
| | | Name | String | Name of the speaker accessory |
| | | ConfiguredName | String | Name of the speaker accessory configured in iOS home app. User can rename speaker in iOS home app and this characteristic can be used to reflect change in openHAB and sync name changes from openHAB to home app. |
| | | ConfiguredName | String | Name of the speaker accessory configured in iOS Home app. User can rename the speaker in iOS Home app and this characteristic can be used to reflect change in openHAB and sync name changes from openHAB to Home app. |
| | | Volume | Number | Speaker volume from 0% to 100% |
| Television | | | | Television accessory with inputs |
| | Active | | Switch, Contact, Dimmer | State of the television - On/Off |
| | | ActiveIdentifier | Number | The input that is currently active (based on its identifier). Can also be configured via metadata, e.g. [ActiveIdentifier=1] |
| | | Name | String | Name of the television accessory |
| | | ConfiguredName | String | Name of the television accessory configured in the iOS Home app. User can rename the television in iOS Home app and this characteristic can be used to reflect change in openHAB and sync name changes from openHAB to Home app. |
| | | RemoteKey | String | Receives a keypress event. |
| | | SleepDiscoveryMode | Switch, Contact, Dimmer | Indicates if the television is discoverable while in standby mode. ON = always discoverable, OFF = not discoverable. Default is ON. Can also be configured via metadata, e.g. [SleepDiscoveryMode=true] |
| | | Brightness | Dimmer | Screen brightness in % (1-100). |
| | | PowerMode | Switch | This oddly named characteristic will receive an ON command when the user requests to open the TV's menu. |
| | | ClosedCaptions | Switch, Contact, Dimmer | Indicates closed captions are enabled. Can also be configured via metadata, e.g. [ClosedCaptions=true] |
| | | CurrentMediaState | String | Current television state. possible values (STOP,PLAY,PAUSE,UNKNOWN). Custom mapping can be defined at item level, e.g. [STOP="STOPPED", PLAY="PLAYING"] |
| | | TargetMediaState | String | Target television state. possible values (STOP,PLAY,PAUSE). Custom mapping can be defined at item level, e.g. [STOP="STOPPED", PLAY="PLAYING"] |
| | | PictureMode | String | Selected picture mode. possible values (OTHER,STANDARD,CALIBRATED,CALIBRATED_DARK,VIVID,GAME,COMPUTER,CUSTOM). Custom mapping can be defined at the item level, e.g. [OTHER="unknown"] |
| InputSource | | | | Input source linked service. Can only be used with Television.
| | | Name | String | Default name of the input source |
| | | ConfiguredName | String | Name of the input source configured in the iOS Home app. User can rename the source in iOS Home app and this characteristic can be used to reflect change in openHAB and sync name changes from openHAB to Home app. |
| | | Configured | Switch, Contact, Dimmer | If the source is configured on the device. Non-configured inputs will not show up in the Home app. - ON/OPEN = show, OFF/CLOSED = hide. Default is ON. Can also be configured via metadata, e.g. [Configured=true] |
| | | InputSourceType | String | Type of the input source. possible values (OTHER, HOME_SCREEN, TUNER, HDMI, COMPOSITE_VIDEO, S_VIDEO, COMPONENT_VIDEO, DVI, AIRPLAY, USB, APPLICATION). Custom mapping can be defined at item level. Can also be configured via metadata, e.g. [InputSourceType="OTHER"]. |
| | | CurrentVisibility | Switch, Contact, Dimmer | If the source has been hidden by the user - ON/OPEN = visible, OFF/CLOSED = hidden. Default is ON. Can also be configured via metadata, e.g. [CurrentVisibility=false] |
| | | Identifier | Number | The identifier of the source, to be used with the ActiveIdentifier characteristic. Can also be configured via metadata, e.g. [Identifier=1] |
| | | InputDeviceType | String | Type of the input device. possible values (OTHER, TV, RECORDING, TUNER, PLAYBACK, AUDIO_SYSTEM). Custom mapping can be defined at item level. Can also be configured via metadata, e.g. [InputDeviceType="OTHER"]. |
| | | TargetVisibilityState | Switch | The desired visibility state of the input source. ON = shown, OFF = hidden. |
| TelevisionSpeaker | | | | An accessory that can be added to a Television in order to control the speaker associated with it. |
| | Mute | | Switch | If the television is muted. ON = muted, OFF = not muted. |
| | | Active | Switch | Unknown. This characteristic is undocumented by Apple, but is still available. |
| | | Volume | Dimmer, Number | Current volume. min/max/step can configured at item level, e.g. minValue=10.5, maxValue=50, step=2] |
| | | VolumeSelector | Dimmer, String | If linked do a dimmer item, will send INCREASE/DECREASE commands. If linked to a string item, will send INCREMENT and DECREMENT. |
| | | VolumeControlType | String | The type of control available. This will default to infer based on what other items are linked. NONE = status only, no control; RELATIVE = INCREMENT/DECREMENT only, no status; RELATIVE_WITH_CURRENT = INCREMENT/DECREMENT only with status; ABSOLUTE = direct status and control. Can also be configured via metadata, e.g. [VolumeControlType="ABSOLUTE"]. |
### Examples
@ -976,9 +1054,9 @@ openhab> log:tail org.openhab.io.homekit.internal
## Troubleshooting
### openHAB is not listed in home app
### openHAB is not listed in Home app
if you don't see openHAB in the home app, probably multicast DNS (mDNS) traffic is not routed correctly from openHAB to home app device or openHAB is already in paired state.
if you don't see openHAB in the Home app, probably multicast DNS (mDNS) traffic is not routed correctly from openHAB to Home app device or openHAB is already in paired state.
You can verify this with [Discovery DNS iOS app](https://apps.apple.com/us/app/discovery-dns-sd-browser/id305441017) as follow:
- install discovery dns app from app store
@ -999,7 +1077,7 @@ You can verify this with [Discovery DNS iOS app](https://apps.apple.com/us/app/d
- verify the flag "sf".
- if sf is equal 1, openHAB is accepting pairing from new iOS device.
- if sf is equal 0 (as on screenshot), openHAB is already paired and does not accept any new pairing request. you can reset pairing using `openhab:homekit clearPairings` command in karaf console.
- if you see openHAB bridge and sf is equal 1 but you dont see openHAB in home app, probably you home app still think it is already paired with openHAB. remove your home from home app and restart iOS device.
- if you see openHAB bridge and sf is equal to 1 but you dont see openHAB in the Home app, the Home app probably still thinks it is already paired with openHAB. remove your home from the Home app and restart the iOS device.
### Re-adding the openHAB HomeKit bridge reports that a bridge is already added, even though it has clearly been removed.

View File

@ -55,6 +55,9 @@ public enum HomekitAccessoryType {
FAUCET("Faucet"),
MICROPHONE("Microphone"),
SLAT("Slat"),
TELEVISION("Television"),
INPUT_SOURCE("InputSource"),
TELEVISION_SPEAKER("TelevisionSpeaker"),
ACCESSORY_GROUP("AccessoryGroup"),
DUMMY("Dummy");

View File

@ -124,7 +124,24 @@ public enum HomekitCharacteristicType {
FILTER_CHANGE_INDICATION("FilterChangeIndication"),
FILTER_LIFE_LEVEL("FilterLifeLevel"),
FILTER_RESET_INDICATION("FilterResetIndication");
FILTER_RESET_INDICATION("FilterResetIndication"),
ACTIVE_IDENTIFIER("ActiveIdentifier"),
REMOTE_KEY("RemoteKey"),
SLEEP_DISCOVERY_MODE("SleepDiscoveryMode"),
POWER_MODE("PowerMode"),
CLOSED_CAPTIONS("ClosedCaptions"),
PICTURE_MODE("PictureMode"),
CONFIGURED("Configured"),
INPUT_SOURCE_TYPE("InputSourceType"),
CURRENT_VISIBILITY("CurrentVisibility"),
IDENTIFIER("Identifier"),
INPUT_DEVICE_TYPE("InputDeviceType"),
TARGET_VISIBILITY_STATE("TargetVisibilityState"),
VOLUME_SELECTOR("VolumeSelector"),
VOLUME_CONTROL_TYPE("VolumeControlType");
private static final Map<String, HomekitCharacteristicType> TAG_MAP = new HashMap<>();

View File

@ -30,6 +30,7 @@ import org.openhab.core.library.items.RollershutterItem;
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.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
@ -221,6 +222,22 @@ public class HomekitTaggedItem {
getName());
}
/**
* Send IncreaseDecreaseType command to a DimmerItem (or a Group:Dimmer)
*/
public void send(IncreaseDecreaseType command) {
if (getItem() instanceof GroupItem && getBaseItem() instanceof DimmerItem) {
((GroupItem) getItem()).send(command);
return;
} else if (getItem() instanceof DimmerItem) {
((DimmerItem) getItem()).send(command);
return;
}
logger.warn(
"Received IncreaseDecreaseType command for item {} that doesn't support it. This is probably a bug.",
getName());
}
/**
* Send PercentType command to a DimmerItem or RollershutterItem (or a Group:Dimmer/Group:Rollershutter)
*

View File

@ -19,7 +19,6 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@ -33,10 +32,10 @@ import org.openhab.core.items.GenericItem;
import org.openhab.core.items.Item;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
import org.openhab.io.homekit.internal.HomekitCharacteristicType;
import org.openhab.io.homekit.internal.HomekitException;
import org.openhab.io.homekit.internal.HomekitSettings;
import org.openhab.io.homekit.internal.HomekitTaggedItem;
import org.slf4j.Logger;
@ -71,6 +70,21 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
this.services = new ArrayList<>();
this.settings = settings;
this.rawCharacteristics = new HashMap<>();
// create raw characteristics for mandatory characteristics
characteristics.forEach(c -> {
var rawCharacteristic = HomekitCharacteristicFactory.createNullableCharacteristic(c, updater);
// not all mandatory characteristics are creatable via HomekitCharacteristicFactory (yet)
if (rawCharacteristic != null) {
rawCharacteristics.put(rawCharacteristic.getClass(), rawCharacteristic);
}
});
}
/**
* Gives an accessory an opportunity to populate additional characteristics after all optional
* charactericteristics have been added.
*/
public void init() throws HomekitException {
}
/**
@ -263,26 +277,18 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
* @param customEnumList list to store custom state enumeration
*/
@NonNullByDefault
protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map,
public <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map,
@Nullable List<T> customEnumList) {
getCharacteristic(characteristicType).ifPresent(c -> {
final Map<String, Object> configuration = c.getConfiguration();
if (configuration != null) {
map.forEach((k, current_value) -> {
final Object new_value = configuration.get(k.toString());
if (new_value instanceof String) {
map.put(k, (String) new_value);
if (customEnumList != null) {
customEnumList.add(k);
}
}
});
HomekitCharacteristicFactory.updateMapping(configuration, map, customEnumList);
}
});
}
@NonNullByDefault
protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
public <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
updateMapping(characteristicType, map, null);
}
@ -297,23 +303,11 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
* @return key for the value
*/
@NonNullByDefault
protected <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
public <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
T defaultValue) {
final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
if (c.isPresent()) {
final State state = c.get().getItem().getState();
logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(),
state, mapping);
if (state instanceof StringType) {
return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
.findAny().map(Entry::getKey).orElseGet(() -> {
logger.warn(
"Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(),
defaultValue);
return defaultValue;
});
}
return HomekitCharacteristicFactory.getKeyFromMapping(c.get(), mapping, defaultValue);
}
return defaultValue;
}
@ -326,6 +320,9 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
}
/**
* If the primary service does not yet exist, it won't be added to it. It's the resposibility
* of the caller to add characteristics when the primary service is created.
*
* @param type
* @param characteristic
*/
@ -339,10 +336,12 @@ public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
}
rawCharacteristics.put(characteristic.getClass(), characteristic);
var service = getPrimaryService();
if (service != null) {
// find the corresponding add method at service and call it.
service.getClass().getMethod("addOptionalCharacteristic", characteristic.getClass()).invoke(service,
characteristic);
}
}
@NonNullByDefault
public <T> Optional<T> getCharacteristic(Class<? extends T> klazz) {

View File

@ -105,6 +105,9 @@ public class HomekitAccessoryFactory {
put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
put(FAUCET, new HomekitCharacteristicType[] { ACTIVE_STATUS });
put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE });
put(INPUT_SOURCE, new HomekitCharacteristicType[] {});
put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE });
}
};
@ -144,6 +147,9 @@ public class HomekitAccessoryFactory {
put(SLAT, HomekitSlatImpl.class);
put(FAUCET, HomekitFaucetImpl.class);
put(MICROPHONE, HomekitMicrophoneImpl.class);
put(TELEVISION, HomekitTelevisionImpl.class);
put(INPUT_SOURCE, HomekitInputSourceImpl.class);
put(TELEVISION_SPEAKER, HomekitTelevisionSpeakerImpl.class);
}
};
@ -208,6 +214,8 @@ public class HomekitAccessoryFactory {
HomekitAccessoryUpdater.class, HomekitSettings.class)
.newInstance(taggedItem, foundCharacteristics, updater, settings);
addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
accessoryImpl.init();
addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
return accessoryImpl;
} else {
@ -387,6 +395,29 @@ public class HomekitAccessoryFactory {
});
}
/**
* add optional characteristics for given accessory from metadata
*
* @param taggedItem main item
* @param accessory accessory
*/
private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
AbstractHomekitAccessoryImpl accessory)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
// Check every metadata key looking for a characteristics we can create
var config = taggedItem.getConfiguration();
if (config == null) {
return;
}
for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
.collect(Collectors.toList())) {
var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
entry.getValue());
if (characteristic.isPresent())
accessory.addCharacteristic(characteristic.get());
}
}
/**
* creates HomeKit services for an openhab item that are members of this group item.
*
@ -411,7 +442,7 @@ public class HomekitAccessoryFactory {
for (var groupMember : ((GroupItem) item).getMembers().stream()
.sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
var accessoryTypes = characteristicTypes.stream().filter(c -> c.getValue() == EMPTY)
var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
.collect(Collectors.toList());
logger.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);

View File

@ -16,7 +16,9 @@ import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
@ -38,6 +40,7 @@ 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.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
@ -70,6 +73,7 @@ import io.github.hapjava.characteristics.impl.airquality.PM10DensityCharacterist
import io.github.hapjava.characteristics.impl.airquality.PM25DensityCharacteristic;
import io.github.hapjava.characteristics.impl.airquality.SulphurDioxideDensityCharacteristic;
import io.github.hapjava.characteristics.impl.airquality.VOCDensityCharacteristic;
import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic;
import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic;
import io.github.hapjava.characteristics.impl.battery.StatusLowBatteryCharacteristic;
import io.github.hapjava.characteristics.impl.battery.StatusLowBatteryEnum;
@ -79,7 +83,11 @@ import io.github.hapjava.characteristics.impl.carbonmonoxidesensor.CarbonMonoxid
import io.github.hapjava.characteristics.impl.carbonmonoxidesensor.CarbonMonoxidePeakLevelCharacteristic;
import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic;
import io.github.hapjava.characteristics.impl.common.ActiveEnum;
import io.github.hapjava.characteristics.impl.common.ActiveIdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic;
import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic;
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.StatusActiveCharacteristic;
@ -101,12 +109,38 @@ import io.github.hapjava.characteristics.impl.fan.TargetFanStateEnum;
import io.github.hapjava.characteristics.impl.filtermaintenance.FilterLifeLevelCharacteristic;
import io.github.hapjava.characteristics.impl.filtermaintenance.ResetFilterIndicationCharacteristic;
import io.github.hapjava.characteristics.impl.humiditysensor.CurrentRelativeHumidityCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateEnum;
import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeEnum;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeEnum;
import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateEnum;
import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic;
import io.github.hapjava.characteristics.impl.lightbulb.ColorTemperatureCharacteristic;
import io.github.hapjava.characteristics.impl.lightbulb.HueCharacteristic;
import io.github.hapjava.characteristics.impl.lightbulb.SaturationCharacteristic;
import io.github.hapjava.characteristics.impl.slat.CurrentTiltAngleCharacteristic;
import io.github.hapjava.characteristics.impl.slat.TargetTiltAngleCharacteristic;
import io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic;
import io.github.hapjava.characteristics.impl.television.ClosedCaptionsEnum;
import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic;
import io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum;
import io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.PictureModeEnum;
import io.github.hapjava.characteristics.impl.television.PowerModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.PowerModeEnum;
import io.github.hapjava.characteristics.impl.television.RemoteKeyCharacteristic;
import io.github.hapjava.characteristics.impl.television.RemoteKeyEnum;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeEnum;
import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic;
import io.github.hapjava.characteristics.impl.television.TargetMediaStateEnum;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeEnum;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorEnum;
import io.github.hapjava.characteristics.impl.thermostat.CoolingThresholdTemperatureCharacteristic;
import io.github.hapjava.characteristics.impl.thermostat.HeatingThresholdTemperatureCharacteristic;
import io.github.hapjava.characteristics.impl.valve.RemainingDurationCharacteristic;
@ -176,11 +210,38 @@ public class HomekitCharacteristicFactory {
put(FILTER_RESET_INDICATION, HomekitCharacteristicFactory::createFilterResetCharacteristic);
put(ACTIVE, HomekitCharacteristicFactory::createActiveCharacteristic);
put(CONFIGURED_NAME, HomekitCharacteristicFactory::createConfiguredNameCharacteristic);
put(ACTIVE_IDENTIFIER, HomekitCharacteristicFactory::createActiveIdentifierCharacteristic);
put(REMOTE_KEY, HomekitCharacteristicFactory::createRemoteKeyCharacteristic);
put(SLEEP_DISCOVERY_MODE, HomekitCharacteristicFactory::createSleepDiscoveryModeCharacteristic);
put(POWER_MODE, HomekitCharacteristicFactory::createPowerModeCharacteristic);
put(CLOSED_CAPTIONS, HomekitCharacteristicFactory::createClosedCaptionsCharacteristic);
put(PICTURE_MODE, HomekitCharacteristicFactory::createPictureModeCharacteristic);
put(CONFIGURED, HomekitCharacteristicFactory::createIsConfiguredCharacteristic);
put(INPUT_SOURCE_TYPE, HomekitCharacteristicFactory::createInputSourceTypeCharacteristic);
put(CURRENT_VISIBILITY, HomekitCharacteristicFactory::createCurrentVisibilityStateCharacteristic);
put(IDENTIFIER, HomekitCharacteristicFactory::createIdentifierCharacteristic);
put(INPUT_DEVICE_TYPE, HomekitCharacteristicFactory::createInputDeviceTypeCharacteristic);
put(TARGET_VISIBILITY_STATE, HomekitCharacteristicFactory::createTargetVisibilityStateCharacteristic);
put(VOLUME_SELECTOR, HomekitCharacteristicFactory::createVolumeSelectorCharacteristic);
put(VOLUME_CONTROL_TYPE, HomekitCharacteristicFactory::createVolumeControlTypeCharacteristic);
put(CURRENT_MEDIA_STATE, HomekitCharacteristicFactory::createCurrentMediaStateCharacteristic);
put(TARGET_MEDIA_STATE, HomekitCharacteristicFactory::createTargetMediaStateCharacteristic);
put(MUTE, HomekitCharacteristicFactory::createMuteCharacteristic);
}
};
public static @Nullable Characteristic createNullableCharacteristic(HomekitTaggedItem item,
HomekitAccessoryUpdater updater) {
final @Nullable HomekitCharacteristicType type = item.getCharacteristicType();
logger.trace("Create characteristic {}", item);
if (optional.containsKey(type)) {
return optional.get(type).apply(item, updater);
}
return null;
}
/**
* create optional HomeKit characteristic
* Create HomeKit characteristic
*
* @param item corresponding OH item
* @param updater update to keep OH item and HomeKit characteristic in sync
@ -188,17 +249,82 @@ public class HomekitCharacteristicFactory {
*/
public static Characteristic createCharacteristic(HomekitTaggedItem item, HomekitAccessoryUpdater updater)
throws HomekitException {
final @Nullable HomekitCharacteristicType type = item.getCharacteristicType();
logger.trace("Create characteristic {}", item);
if (optional.containsKey(type)) {
return optional.get(type).apply(item, updater);
Characteristic characteristic = createNullableCharacteristic(item, updater);
if (characteristic != null) {
return characteristic;
}
final @Nullable HomekitCharacteristicType type = item.getCharacteristicType();
logger.warn("Unsupported optional characteristic from item {}. Accessory type {}, characteristic type {}",
item.getName(), item.getAccessoryType(), type.getTag());
throw new HomekitException(
"Unsupported optional characteristic. Characteristic type \"" + type.getTag() + "\"");
}
public static <T extends Enum<T>> Map<T, String> createMapping(HomekitTaggedItem item, Class<T> klazz) {
EnumMap<T, String> map = new EnumMap(klazz);
for (var k : klazz.getEnumConstants()) {
map.put(k, k.toString());
}
var configuration = item.getConfiguration();
if (configuration != null) {
updateMapping(configuration, map);
}
return map;
}
/**
* Update mapping with values from item configuration.
* It checks for all keys from the mapping whether there is configuration at item with the same key and if yes,
* replace the value.
*
* @param configuration tagged item configuration
* @param map mapping to update
* @param customEnumList list to store custom state enumeration
*/
public static <T> void updateMapping(Map<String, Object> configuration, Map<T, String> map,
@Nullable List<T> customEnumList) {
map.forEach((k, current_value) -> {
final Object new_value = configuration.get(k.toString());
if (new_value instanceof String) {
map.put(k, (String) new_value);
if (customEnumList != null) {
customEnumList.add(k);
}
}
});
}
public static <T> void updateMapping(Map<String, Object> configuration, Map<T, String> map) {
updateMapping(configuration, map, null);
}
/**
* Takes item state as value and retrieves the key for that value from mapping.
* E.g. used to map StringItem value to HomeKit Enum
*
* @param characteristicType characteristicType to identify item
* @param mapping mapping
* @param defaultValue default value if nothing found in mapping
* @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();
logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", item.getAccessoryType().getTag(),
state, mapping);
if (state instanceof StringType) {
return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
.findAny().map(Map.Entry::getKey).orElseGet(() -> {
logger.warn(
"Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
state.toString(), item.getAccessoryType().getTag(), item.getName(), mapping.values(),
defaultValue);
return defaultValue;
});
}
return defaultValue;
}
// METHODS TO CREATE SINGLE CHARACTERISTIC FROM OH ITEM
// supporting methods
@ -208,6 +334,11 @@ public class HomekitCharacteristicFactory {
.getServiceReference(Homekit.class.getName()).getProperty("useFahrenheitTemperature") == Boolean.TRUE;
}
private static <T extends CharacteristicEnum> CompletableFuture<T> getEnumFromItem(HomekitTaggedItem item,
Map<T, String> mapping, T defaultValue) {
return CompletableFuture.completedFuture(getKeyFromMapping(item, mapping, defaultValue));
}
private static <T extends CharacteristicEnum> CompletableFuture<T> getEnumFromItem(HomekitTaggedItem item,
T offEnum, T onEnum, T defaultEnum) {
final State state = item.getItem().getState();
@ -228,6 +359,11 @@ public class HomekitCharacteristicFactory {
return CompletableFuture.completedFuture(defaultEnum);
}
private static <T extends Enum<T>> void setValueFromEnum(HomekitTaggedItem taggedItem, T value,
Map<T, String> map) {
taggedItem.send(new StringType(map.get(value)));
}
private static void setValueFromEnum(HomekitTaggedItem taggedItem, CharacteristicEnum value,
CharacteristicEnum offEnum, CharacteristicEnum onEnum) {
if (taggedItem.getBaseItem() instanceof SwitchItem) {
@ -914,8 +1050,8 @@ public class HomekitCharacteristicFactory {
private static ActiveCharacteristic createActiveCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new ActiveCharacteristic(
() -> getEnumFromItem(taggedItem, ActiveEnum.ACTIVE, ActiveEnum.INACTIVE, ActiveEnum.INACTIVE),
(value) -> setValueFromEnum(taggedItem, value, ActiveEnum.ACTIVE, ActiveEnum.INACTIVE),
() -> getEnumFromItem(taggedItem, ActiveEnum.INACTIVE, ActiveEnum.ACTIVE, ActiveEnum.INACTIVE),
(value) -> setValueFromEnum(taggedItem, value, ActiveEnum.INACTIVE, ActiveEnum.ACTIVE),
getSubscriber(taggedItem, ACTIVE, updater), getUnsubscriber(taggedItem, ACTIVE, updater));
}
@ -929,4 +1065,149 @@ public class HomekitCharacteristicFactory {
getSubscriber(taggedItem, CONFIGURED_NAME, updater),
getUnsubscriber(taggedItem, CONFIGURED_NAME, updater));
}
private static ActiveIdentifierCharacteristic createActiveIdentifierCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new ActiveIdentifierCharacteristic(getIntSupplier(taggedItem, 1), setIntConsumer(taggedItem),
getSubscriber(taggedItem, ACTIVE_IDENTIFIER, updater),
getUnsubscriber(taggedItem, ACTIVE_IDENTIFIER, updater));
}
private static RemoteKeyCharacteristic createRemoteKeyCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, RemoteKeyEnum.class);
return new RemoteKeyCharacteristic((value) -> setValueFromEnum(taggedItem, value, map));
}
private static SleepDiscoveryModeCharacteristic createSleepDiscoveryModeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new SleepDiscoveryModeCharacteristic(
() -> getEnumFromItem(taggedItem, SleepDiscoveryModeEnum.NOT_DISCOVERABLE,
SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE, SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE),
getSubscriber(taggedItem, SLEEP_DISCOVERY_MODE, updater),
getUnsubscriber(taggedItem, SLEEP_DISCOVERY_MODE, updater));
}
private static PowerModeCharacteristic createPowerModeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new PowerModeCharacteristic(
(value) -> setValueFromEnum(taggedItem, value, PowerModeEnum.HIDE, PowerModeEnum.SHOW));
}
private static ClosedCaptionsCharacteristic createClosedCaptionsCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new ClosedCaptionsCharacteristic(
() -> getEnumFromItem(taggedItem, ClosedCaptionsEnum.DISABLED, ClosedCaptionsEnum.ENABLED,
ClosedCaptionsEnum.DISABLED),
(value) -> setValueFromEnum(taggedItem, value, ClosedCaptionsEnum.DISABLED, ClosedCaptionsEnum.ENABLED),
getSubscriber(taggedItem, CLOSED_CAPTIONS, updater),
getUnsubscriber(taggedItem, CLOSED_CAPTIONS, updater));
}
private static PictureModeCharacteristic createPictureModeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, PictureModeEnum.class);
return new PictureModeCharacteristic(() -> getEnumFromItem(taggedItem, map, PictureModeEnum.OTHER),
(value) -> setValueFromEnum(taggedItem, value, map), getSubscriber(taggedItem, PICTURE_MODE, updater),
getUnsubscriber(taggedItem, PICTURE_MODE, updater));
}
private static IsConfiguredCharacteristic createIsConfiguredCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new IsConfiguredCharacteristic(
() -> getEnumFromItem(taggedItem, IsConfiguredEnum.NOT_CONFIGURED, IsConfiguredEnum.CONFIGURED,
IsConfiguredEnum.NOT_CONFIGURED),
(value) -> setValueFromEnum(taggedItem, value, IsConfiguredEnum.NOT_CONFIGURED,
IsConfiguredEnum.CONFIGURED),
getSubscriber(taggedItem, CONFIGURED, updater), getUnsubscriber(taggedItem, CONFIGURED, updater));
}
private static InputSourceTypeCharacteristic createInputSourceTypeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, InputSourceTypeEnum.class);
return new InputSourceTypeCharacteristic(() -> getEnumFromItem(taggedItem, map, InputSourceTypeEnum.OTHER),
getSubscriber(taggedItem, INPUT_SOURCE_TYPE, updater),
getUnsubscriber(taggedItem, INPUT_SOURCE_TYPE, updater));
}
private static CurrentVisibilityStateCharacteristic createCurrentVisibilityStateCharacteristic(
HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) {
return new CurrentVisibilityStateCharacteristic(
() -> getEnumFromItem(taggedItem, CurrentVisibilityStateEnum.HIDDEN, CurrentVisibilityStateEnum.SHOWN,
CurrentVisibilityStateEnum.HIDDEN),
getSubscriber(taggedItem, CURRENT_VISIBILITY, updater),
getUnsubscriber(taggedItem, CURRENT_VISIBILITY, updater));
}
private static IdentifierCharacteristic createIdentifierCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
return new IdentifierCharacteristic(getIntSupplier(taggedItem, 1));
}
private static InputDeviceTypeCharacteristic createInputDeviceTypeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var mapping = createMapping(taggedItem, InputDeviceTypeEnum.class);
return new InputDeviceTypeCharacteristic(() -> getEnumFromItem(taggedItem, mapping, InputDeviceTypeEnum.OTHER),
getSubscriber(taggedItem, INPUT_DEVICE_TYPE, updater),
getUnsubscriber(taggedItem, INPUT_DEVICE_TYPE, updater));
}
private static TargetVisibilityStateCharacteristic createTargetVisibilityStateCharacteristic(
HomekitTaggedItem taggedItem, HomekitAccessoryUpdater updater) {
return new TargetVisibilityStateCharacteristic(
() -> getEnumFromItem(taggedItem, TargetVisibilityStateEnum.HIDDEN, TargetVisibilityStateEnum.SHOWN,
TargetVisibilityStateEnum.HIDDEN),
(value) -> setValueFromEnum(taggedItem, value, TargetVisibilityStateEnum.HIDDEN,
TargetVisibilityStateEnum.SHOWN),
getSubscriber(taggedItem, TARGET_VISIBILITY_STATE, updater),
getUnsubscriber(taggedItem, TARGET_VISIBILITY_STATE, updater));
}
private static VolumeSelectorCharacteristic createVolumeSelectorCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
if (taggedItem.getItem() instanceof DimmerItem) {
return new VolumeSelectorCharacteristic((value) -> taggedItem
.send(value.equals(VolumeSelectorEnum.INCREMENT) ? IncreaseDecreaseType.INCREASE
: IncreaseDecreaseType.DECREASE));
} else {
var map = createMapping(taggedItem, VolumeSelectorEnum.class);
return new VolumeSelectorCharacteristic((value) -> setValueFromEnum(taggedItem, value, map));
}
}
private static VolumeControlTypeCharacteristic createVolumeControlTypeCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, VolumeControlTypeEnum.class);
return new VolumeControlTypeCharacteristic(() -> getEnumFromItem(taggedItem, map, VolumeControlTypeEnum.NONE),
getSubscriber(taggedItem, VOLUME_CONTROL_TYPE, updater),
getUnsubscriber(taggedItem, VOLUME_CONTROL_TYPE, updater));
}
private static CurrentMediaStateCharacteristic createCurrentMediaStateCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, CurrentMediaStateEnum.class);
return new CurrentMediaStateCharacteristic(
() -> getEnumFromItem(taggedItem, map, CurrentMediaStateEnum.UNKNOWN),
getSubscriber(taggedItem, CURRENT_MEDIA_STATE, updater),
getUnsubscriber(taggedItem, CURRENT_MEDIA_STATE, updater));
}
private static TargetMediaStateCharacteristic createTargetMediaStateCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
var map = createMapping(taggedItem, TargetMediaStateEnum.class);
return new TargetMediaStateCharacteristic(() -> getEnumFromItem(taggedItem, map, TargetMediaStateEnum.STOP),
(value) -> setValueFromEnum(taggedItem, value, map),
getSubscriber(taggedItem, TARGET_MEDIA_STATE, updater),
getUnsubscriber(taggedItem, TARGET_MEDIA_STATE, updater));
}
private static MuteCharacteristic createMuteCharacteristic(HomekitTaggedItem taggedItem,
HomekitAccessoryUpdater updater) {
BooleanItemReader muteReader = new BooleanItemReader(taggedItem.getItem(),
taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
return new MuteCharacteristic(() -> CompletableFuture.completedFuture(muteReader.getValue()),
(value) -> taggedItem.send(value ? OnOffType.ON : OnOffType.OFF),
getSubscriber(taggedItem, MUTE, updater), getUnsubscriber(taggedItem, MUTE, updater));
}
}

View File

@ -0,0 +1,101 @@
/**
* 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 java.util.concurrent.CompletableFuture;
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.accessories.HomekitAccessory;
import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic;
import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic;
import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateEnum;
import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeEnum;
import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateCharacteristic;
import io.github.hapjava.services.impl.InputSourceService;
/**
* Implements Input Source
*
* This is a little different in that we don't implement the accessory interface.
* This is because several of the "mandatory" characteristics we don't require,
* and wait until all optional attributes are added and if they don't exist
* it will create "default" values for them.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault({})
public class HomekitInputSourceImpl extends AbstractHomekitAccessoryImpl {
public HomekitInputSourceImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
}
@Override
public void init() throws HomekitException {
super.init();
// these charactereristics are technically mandatory, but we provide defaults if they're not provided
var configuredNameCharacteristic = getCharacteristic(ConfiguredNameCharacteristic.class)
.orElseGet(() -> new ConfiguredNameCharacteristic(() -> getName(), v -> {
}, v -> {
}, () -> {
}));
var inputSourceTypeCharacteristic = getCharacteristic(InputSourceTypeCharacteristic.class)
.orElseGet(() -> new InputSourceTypeCharacteristic(
() -> CompletableFuture.completedFuture(InputSourceTypeEnum.OTHER), v -> {
}, () -> {
}));
var isConfiguredCharacteristic = getCharacteristic(IsConfiguredCharacteristic.class)
.orElseGet(() -> new IsConfiguredCharacteristic(
() -> CompletableFuture.completedFuture(IsConfiguredEnum.CONFIGURED), v -> {
}, v -> {
}, () -> {
}));
var currentVisibilityStateCharacteristic = getCharacteristic(CurrentVisibilityStateCharacteristic.class)
.orElseGet(() -> new CurrentVisibilityStateCharacteristic(
() -> CompletableFuture.completedFuture(CurrentVisibilityStateEnum.SHOWN), v -> {
}, () -> {
}));
var identifierCharacteristic = getCharacteristic(IdentifierCharacteristic.class)
.orElseGet(() -> new IdentifierCharacteristic(() -> CompletableFuture.completedFuture(1)));
var service = new InputSourceService(configuredNameCharacteristic, inputSourceTypeCharacteristic,
isConfiguredCharacteristic, currentVisibilityStateCharacteristic);
getCharacteristic(NameCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
service.addOptionalCharacteristic(identifierCharacteristic);
getCharacteristic(InputDeviceTypeCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(TargetVisibilityStateCharacteristic.class)
.ifPresent(c -> service.addOptionalCharacteristic(c));
getServices().add(service);
}
@Override
public boolean isLinkable(HomekitAccessory parentAccessory) {
return parentAccessory instanceof HomekitTelevisionImpl;
}
}

View File

@ -0,0 +1,295 @@
/**
* 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 static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.io.homekit.internal.HomekitCharacteristicType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.impl.airquality.AirQualityCharacteristic;
import io.github.hapjava.characteristics.impl.airquality.AirQualityEnum;
import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic;
import io.github.hapjava.characteristics.impl.common.ActiveEnum;
import io.github.hapjava.characteristics.impl.common.ActiveIdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic;
import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic;
import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateCharacteristic;
import io.github.hapjava.characteristics.impl.heatercooler.CurrentHeaterCoolerStateEnum;
import io.github.hapjava.characteristics.impl.heatercooler.TargetHeaterCoolerStateCharacteristic;
import io.github.hapjava.characteristics.impl.heatercooler.TargetHeaterCoolerStateEnum;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateEnum;
import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeEnum;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeCharacteristic;
import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeEnum;
import io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic;
import io.github.hapjava.characteristics.impl.television.ClosedCaptionsEnum;
import io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.PictureModeEnum;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeEnum;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeEnum;
import io.github.hapjava.characteristics.impl.thermostat.CurrentHeatingCoolingStateCharacteristic;
import io.github.hapjava.characteristics.impl.thermostat.CurrentHeatingCoolingStateEnum;
import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateCharacteristic;
import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateEnum;
/**
* Creates an optional characteristics from metadata
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class HomekitMetadataCharacteristicFactory {
private static final Logger logger = LoggerFactory.getLogger(HomekitMetadataCharacteristicFactory.class);
// List of optional characteristics that can be set via metadata, and the corresponding method to create them.
private final static Map<HomekitCharacteristicType, Function<Object, Characteristic>> optional = new HashMap<>() {
{
put(ACTIVE_IDENTIFIER, HomekitMetadataCharacteristicFactory::createActiveIdentifierCharacteristic);
put(ACTIVE_STATUS, HomekitMetadataCharacteristicFactory::createActiveStatusCharacteristic);
put(AIR_QUALITY, HomekitMetadataCharacteristicFactory::createAirQualityCharacteristic);
put(CLOSED_CAPTIONS, HomekitMetadataCharacteristicFactory::createClosedCaptionsCharacteristic);
put(CONFIGURED, HomekitMetadataCharacteristicFactory::createIsConfiguredCharacteristic);
put(CONFIGURED_NAME, HomekitMetadataCharacteristicFactory::createConfiguredNameCharacteristic);
put(CURRENT_HEATER_COOLER_STATE,
HomekitMetadataCharacteristicFactory::createCurrentHeaterCoolerStateCharacteristic);
put(CURRENT_HEATING_COOLING_STATE,
HomekitMetadataCharacteristicFactory::createCurrentHeatingCoolingStateCharacteristic);
put(CURRENT_VISIBILITY, HomekitMetadataCharacteristicFactory::createCurrentVisibilityCharacteristic);
put(IDENTIFIER, HomekitMetadataCharacteristicFactory::createIdentifierCharacteristic);
put(INPUT_DEVICE_TYPE, HomekitMetadataCharacteristicFactory::createInputDeviceTypeCharacteristic);
put(INPUT_SOURCE_TYPE, HomekitMetadataCharacteristicFactory::createInputSourceTypeCharacteristic);
put(NAME, HomekitMetadataCharacteristicFactory::createNameCharacteristic);
put(PICTURE_MODE, HomekitMetadataCharacteristicFactory::createPictureModeCharacteristic);
put(SLEEP_DISCOVERY_MODE, HomekitMetadataCharacteristicFactory::createSleepDiscoveryModeCharacteristic);
put(TARGET_HEATER_COOLER_STATE,
HomekitMetadataCharacteristicFactory::createTargetHeaterCoolerStateCharacteristic);
put(TARGET_HEATING_COOLING_STATE,
HomekitMetadataCharacteristicFactory::createTargetHeatingCoolingStateCharacteristic);
put(VOLUME_CONTROL_TYPE, HomekitMetadataCharacteristicFactory::createVolumeControlTypeCharacteristic);
}
};
public static Optional<Characteristic> createCharacteristic(String characteristic, Object value) {
var type = HomekitCharacteristicType.valueOfTag(characteristic);
if (type.isEmpty() || !optional.containsKey(type.get()))
return Optional.empty();
return Optional.of(optional.get(type.get()).apply(value));
}
private static Supplier<CompletableFuture<Integer>> getInteger(Object value) {
int intValue;
if (value instanceof BigDecimal) {
intValue = ((BigDecimal) value).intValue();
} else if (value instanceof Float) {
intValue = ((Float) value).intValue();
} else if (value instanceof Integer) {
intValue = (Integer) value;
} else if (value instanceof Long) {
intValue = ((Long) value).intValue();
} else {
intValue = Integer.valueOf(value.toString());
}
return () -> CompletableFuture.completedFuture(intValue);
}
private static Supplier<CompletableFuture<String>> getString(Object value) {
return () -> CompletableFuture.completedFuture(value.toString());
}
private static <T extends Enum<T>> Supplier<CompletableFuture<T>> getEnum(Object value, Class<T> klazz) {
T enumValue = Enum.valueOf(klazz, value.toString());
return () -> CompletableFuture.completedFuture(enumValue);
}
private static <T extends Enum<T>> Supplier<CompletableFuture<T>> getEnum(Object value, Class<T> klazz, T trueValue,
T falseValue) {
if (value.equals(true) || value.equals("true")) {
return () -> CompletableFuture.completedFuture(trueValue);
} else if (value.equals(false) || value.equals("false")) {
return () -> CompletableFuture.completedFuture(falseValue);
}
return getEnum(value, klazz);
}
private static Characteristic createActiveIdentifierCharacteristic(Object value) {
return new ActiveIdentifierCharacteristic(getInteger(value), v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createActiveStatusCharacteristic(Object value) {
return new ActiveCharacteristic(getEnum(value, ActiveEnum.class, ActiveEnum.ACTIVE, ActiveEnum.INACTIVE), v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createAirQualityCharacteristic(Object value) {
return new AirQualityCharacteristic(getEnum(value, AirQualityEnum.class), v -> {
}, () -> {
});
}
private static Characteristic createClosedCaptionsCharacteristic(Object value) {
return new ClosedCaptionsCharacteristic(
getEnum(value, ClosedCaptionsEnum.class, ClosedCaptionsEnum.ENABLED, ClosedCaptionsEnum.DISABLED),
v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createIsConfiguredCharacteristic(Object value) {
return new IsConfiguredCharacteristic(
getEnum(value, IsConfiguredEnum.class, IsConfiguredEnum.CONFIGURED, IsConfiguredEnum.NOT_CONFIGURED),
v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createConfiguredNameCharacteristic(Object value) {
return new ConfiguredNameCharacteristic(getString(value), v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createCurrentVisibilityCharacteristic(Object value) {
return new CurrentVisibilityStateCharacteristic(getEnum(value, CurrentVisibilityStateEnum.class,
CurrentVisibilityStateEnum.SHOWN, CurrentVisibilityStateEnum.HIDDEN), v -> {
}, () -> {
});
}
private static Characteristic createCurrentHeaterCoolerStateCharacteristic(Object value) {
var enumSupplier = getEnum(value, CurrentHeaterCoolerStateEnum.class);
CurrentHeaterCoolerStateEnum enumValue;
try {
enumValue = enumSupplier.get().get();
} catch (InterruptedException | ExecutionException e) {
enumValue = CurrentHeaterCoolerStateEnum.INACTIVE;
}
return new CurrentHeaterCoolerStateCharacteristic(new CurrentHeaterCoolerStateEnum[] { enumValue },
enumSupplier, v -> {
}, () -> {
});
}
private static Characteristic createCurrentHeatingCoolingStateCharacteristic(Object value) {
var enumSupplier = getEnum(value, CurrentHeatingCoolingStateEnum.class);
CurrentHeatingCoolingStateEnum enumValue;
try {
enumValue = enumSupplier.get().get();
} catch (InterruptedException | ExecutionException e) {
enumValue = CurrentHeatingCoolingStateEnum.OFF;
}
return new CurrentHeatingCoolingStateCharacteristic(new CurrentHeatingCoolingStateEnum[] { enumValue },
enumSupplier, v -> {
}, () -> {
});
}
private static Characteristic createIdentifierCharacteristic(Object value) {
return new IdentifierCharacteristic(getInteger(value));
}
private static Characteristic createInputDeviceTypeCharacteristic(Object value) {
return new InputDeviceTypeCharacteristic(getEnum(value, InputDeviceTypeEnum.class), v -> {
}, () -> {
});
}
private static Characteristic createInputSourceTypeCharacteristic(Object value) {
return new InputSourceTypeCharacteristic(getEnum(value, InputSourceTypeEnum.class), v -> {
}, () -> {
});
}
private static Characteristic createNameCharacteristic(Object value) {
return new NameCharacteristic(getString(value));
}
private static Characteristic createPictureModeCharacteristic(Object value) {
return new PictureModeCharacteristic(getEnum(value, PictureModeEnum.class), v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createSleepDiscoveryModeCharacteristic(Object value) {
return new SleepDiscoveryModeCharacteristic(getEnum(value, SleepDiscoveryModeEnum.class,
SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE, SleepDiscoveryModeEnum.NOT_DISCOVERABLE), v -> {
}, () -> {
});
}
private static Characteristic createTargetHeaterCoolerStateCharacteristic(Object value) {
var enumSupplier = getEnum(value, TargetHeaterCoolerStateEnum.class);
TargetHeaterCoolerStateEnum enumValue;
try {
enumValue = enumSupplier.get().get();
} catch (InterruptedException | ExecutionException e) {
enumValue = TargetHeaterCoolerStateEnum.AUTO;
}
return new TargetHeaterCoolerStateCharacteristic(new TargetHeaterCoolerStateEnum[] { enumValue }, enumSupplier,
v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createTargetHeatingCoolingStateCharacteristic(Object value) {
var enumSupplier = getEnum(value, TargetHeatingCoolingStateEnum.class);
TargetHeatingCoolingStateEnum enumValue;
try {
enumValue = enumSupplier.get().get();
} catch (InterruptedException | ExecutionException e) {
enumValue = TargetHeatingCoolingStateEnum.OFF;
}
return new TargetHeatingCoolingStateCharacteristic(new TargetHeatingCoolingStateEnum[] { enumValue },
enumSupplier, v -> {
}, v -> {
}, () -> {
});
}
private static Characteristic createVolumeControlTypeCharacteristic(Object value) {
return new VolumeControlTypeCharacteristic(getEnum(value, VolumeControlTypeEnum.class), v -> {
}, () -> {
});
}
}

View File

@ -0,0 +1,95 @@
/**
* 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 java.util.concurrent.CompletableFuture;
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.impl.common.ActiveCharacteristic;
import io.github.hapjava.characteristics.impl.common.ActiveIdentifierCharacteristic;
import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic;
import io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic;
import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic;
import io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.PowerModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.RemoteKeyCharacteristic;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeCharacteristic;
import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeEnum;
import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic;
import io.github.hapjava.services.impl.TelevisionService;
/**
* Implements Television
*
* This is a little different in that we don't implement the accessory interface.
* This is because several of the "mandatory" characteristics we don't require,
* and wait until all optional attributes are added and if they don't exist
* it will create "default" values for them.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault({})
public class HomekitTelevisionImpl extends AbstractHomekitAccessoryImpl {
public HomekitTelevisionImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
}
@Override
public void init() throws HomekitException {
super.init();
// these charactereristics are technically mandatory, but we provide defaults if they're not provided
var activeIdentifierCharacteristic = getCharacteristic(ActiveIdentifierCharacteristic.class)
.orElseGet(() -> new ActiveIdentifierCharacteristic(() -> CompletableFuture.completedFuture(1), v -> {
}, v -> {
}, () -> {
}));
var configuredNameCharacteristic = getCharacteristic(ConfiguredNameCharacteristic.class)
.orElseGet(() -> new ConfiguredNameCharacteristic(() -> getName(), v -> {
}, v -> {
}, () -> {
}));
var remoteKeyCharacteristic = getCharacteristic(RemoteKeyCharacteristic.class)
.orElseGet(() -> new RemoteKeyCharacteristic((v) -> {
}));
var sleepDiscoveryModeCharacteristic = getCharacteristic(SleepDiscoveryModeCharacteristic.class)
.orElseGet(() -> new SleepDiscoveryModeCharacteristic(
() -> CompletableFuture.completedFuture(SleepDiscoveryModeEnum.ALWAYS_DISCOVERABLE), v -> {
}, () -> {
}));
var service = new TelevisionService(getCharacteristic(ActiveCharacteristic.class).get(),
activeIdentifierCharacteristic, configuredNameCharacteristic, remoteKeyCharacteristic,
sleepDiscoveryModeCharacteristic);
getCharacteristic(NameCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(BrightnessCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(PowerModeCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(ClosedCaptionsCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(CurrentMediaStateCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(TargetMediaStateCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getCharacteristic(PictureModeCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
getServices().add(service);
}
}

View File

@ -0,0 +1,87 @@
/**
* 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 java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
import org.openhab.io.homekit.internal.HomekitCharacteristicType;
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.impl.audio.MuteCharacteristic;
import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic;
import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeEnum;
import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic;
import io.github.hapjava.services.impl.TelevisionSpeakerService;
/**
* Implements Television Speaker
*
* This is a little different in that we don't implement the accessory interface.
* This is because several of the "mandatory" characteristics we don't require,
* and wait until all optional attributes are added and if they don't exist
* it will create "default" values for them.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault({})
public class HomekitTelevisionSpeakerImpl extends AbstractHomekitAccessoryImpl {
public HomekitTelevisionSpeakerImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException {
super(taggedItem, mandatoryCharacteristics, updater, settings);
}
@Override
public void init() throws HomekitException {
super.init();
var muteCharacteristic = (MuteCharacteristic) HomekitCharacteristicFactory
.createCharacteristic(getCharacteristic(HomekitCharacteristicType.MUTE).get(), getUpdater());
// this characteristic is technically optional, but we provide a default implementation if it's not provided
var volumeControlTypeCharacteristic = getCharacteristic(VolumeControlTypeCharacteristic.class);
// optional characteristics
var volumeCharacteristic = getCharacteristic(VolumeCharacteristic.class);
var volumeSelectorCharacteristic = getCharacteristic(VolumeSelectorCharacteristic.class);
if (!volumeControlTypeCharacteristic.isPresent()) {
VolumeControlTypeEnum type;
if (volumeCharacteristic.isPresent()) {
type = VolumeControlTypeEnum.ABSOLUTE;
} else if (volumeSelectorCharacteristic.isPresent()) {
type = VolumeControlTypeEnum.RELATIVE;
} else {
type = VolumeControlTypeEnum.NONE;
}
volumeControlTypeCharacteristic = Optional
.of(new VolumeControlTypeCharacteristic(() -> CompletableFuture.completedFuture(type), v -> {
}, () -> {
}));
}
var service = new TelevisionSpeakerService(muteCharacteristic);
getCharacteristic(ActiveCharacteristic.class).ifPresent(c -> service.addOptionalCharacteristic(c));
volumeCharacteristic.ifPresent(c -> service.addOptionalCharacteristic(c));
service.addOptionalCharacteristic(volumeControlTypeCharacteristic.get());
getServices().add(service);
}
}