Skip to content

Commit

Permalink
[homekit] Allow configuring secondary services as members of a group (o…
Browse files Browse the repository at this point in the history
…penhab#13879)

* [homekit] allow configuring secondary services as members of a group

Required introduction of AccessoryGroup to represent the base
AccessoryInformationService for ease of configuring multiple of the
same service.

This is also "breaking" in that someone who previously had HomeKit
accessories nested directly inside of a group that was itself a
HomeKit accessory will now have those items grouped within the Home
app.

* [homekit] combine multiple readme sections on complex accessories

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer authored Dec 23, 2022
1 parent b1d4c40 commit b62e145
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 109 deletions.
184 changes: 95 additions & 89 deletions bundles/org.openhab.io.homekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ HomeKit integration supports following accessory types:

![settings_qrcode.png](doc/settings_qrcode.png)

- open home app on your iPhone or iPad
- open Home app on your iPhone or iPad
- create new home

![ios_add_new_home.png](doc/ios_add_new_home.png)
Expand All @@ -68,7 +68,7 @@ HomeKit integration supports following accessory types:

![ios_add_anyway.png](doc/ios_add_anyway.png)

- follow the instruction of the home app wizard
- follow the instruction of the Home app wizard

![ios_add_accessory_wizard.png](doc/ios_add_accessory_wizard.png)

Expand Down Expand Up @@ -133,7 +133,7 @@ Complex accessories require a tag on a Group Item indicating the accessory type,
A HomeKit accessory has mandatory and optional characteristics (listed below in the table).
The mapping between openHAB items and HomeKit accessory and characteristics is done by means of [metadata](https://www.openhab.org/docs/concepts/items.html#item-metadata)

If the first word of the item name match the room name in home app, home app will hide it.
If the first word of the item name match the room name in Home app, Home app will hide it.
E.g. item with the name "Kitchen Light" will be shown in "Kitchen" room as "Light". This is recommended naming convention for HomeKit items and rooms.

### UI based Configuration
Expand Down Expand Up @@ -166,12 +166,6 @@ In order to add metadata to an item:
Switch leaksensor_metadata "Leak Sensor" {homekit="LeakSensor"}
```

You can link one openHAB item to one or more HomeKit accessory, e.g.

```xtend
Switch occupancy_and_motion_sensor "Occupancy and Motion Sensor Tag" {homekit="OccupancySensor,MotionSensor"}
```

The tag can be:

- full qualified: i.e. with accessory type and characteristic, e.g. "LeakSensor.LeakDetectedState"
Expand All @@ -196,19 +190,104 @@ Switch leaksensor "Leak Sensor"
Switch leaksensor_battery "Leak Sensor Battery" (gLeakSensor) {homekit="LeakSensor.BatteryLowStatus"}
```

You can use openHAB group to manage state of multiple items. (see [Group items](https://www.openhab.org/docs/configuration/items.html#derive-group-state-from-member-items))
In this case, you can assign HomeKit accessory type to the group and to the group items
Following example defines 3 HomeKit accessories of type Lighting:
### Complex Multiple Service Accessories

Alternatively, you may want to have a choice of controlling the items individually, OR as a group, from HomeKit.
The following examples defines a single HomeKit accessory _with multiple services_ that the Home app will allow you to control together, or drill down and control individually.
Note that `AccessoryGroup` doesn't expose any services itself, but allows you to group other services together underneath it.
Also note that when nesting accessories, you cannot use the shorthand of naming only a characteristic, and not its accessory type, since it would be ambiguous if that item belongs to a secondary service, or to the primary service it's nested under.

```java
Group:Switch:OR(ON,OFF) gLight "Light Group" {homekit="AccessoryGroup"}
Switch light1 "Light 1" (gLight) {homekit="Lighting"}
Switch light2 "Light 2" (gLight) {homekit="Lighting"}
```

![Group of Lights](doc/group_of_lights.png)

- "Light 1" and "Light 2" as independent lights
- "Light Group" that controls "Light 1" and "Light 2" as group
You can also group additional accessories directly under another accessory.
In this example, HomeKit will show three separate light controls.
As this is somewhat confusing that Home will allow controlling all members as a group, and you also have the group as a distinct switch inside the HomeKit accessory, this is not a recommended configuration.

```xtend
Group:Switch:OR(ON,OFF) gLight "Light Group" {homekit="Lighting"}
Switch light1 "Light 1" (gLight) {homekit="Lighting.OnState"}
Switch light2 "Light 2" (gLight) {homekit="Lighting.OnState"}
Switch light1 "Light 1" (gLight) {homekit="Lighting"}
Switch light2 "Light 2" (gLight) {homekit="Lighting"}
```

![Light Group With Additional Lights](doc/group_of_lights_group_plus_lights.png)

You can also mix and match accessories:

```java
Group gFan {homekit="Fan"}
Switch fan1 "Fan" (gFan) {homekit="Fan.Active"}
Switch fan1_light "Fan Light" (gFan) {homekit="Lighting"}
```

![Fan With Light](doc/fan_with_light.png)

Another way to build complex accessories is to associate multiple accessory types with the root group, and then define all of the individual characteristics on group members.
When using this style, you cannot have multiple instance of the same accessory type.

```java
Group FanWithLight "Fan with Light" {homekit = "Fan,Lighting"}
Switch FanActiveStatus "Fan Active Status" (FanWithLight) {homekit = "Fan.ActiveStatus"}
Number FanRotationSpeed "Fan Rotation Speed" (FanWithLight) {homekit = "Fan.RotationSpeed"}
Switch Light "Light" (FanWithLight) {homekit = "Lighting.OnState"}
```

or in MainUI:

![ui_fan_with_light_group_view.png](doc/ui_fan_with_light_group_view.png)
![ui_fan_with_light_group_code.png](doc/ui_fan_with_light_group_code.png)
![ui_fan_with_light_group_config.png](doc/ui_fan_with_light_group_config.png)

Finally, you can link one openHAB item to one or more HomeKit accessories, as well:

```java
Switch occupancy_and_motion_sensor "Occupancy and Motion Sensor Tag" {homekit="OccupancySensor,MotionSensor"}
```

You can even form complex sensors this way.
Just be sure that you fully specify additional characteristics, so that the addon knows which root service to add it to.

```java
Group eBunkAirthings "Bunk Room Airthings Wave Plus" { homekit="AirQualitySensor,TemperatureSensor,HumiditySensor" }

String Bunk_AirQuality "Bunk Room Air Quality" (eBunkAirthings) { homekit="AirQualitySensor.AirQuality" }
Number:Dimensionless Bunk_Humidity "Bunk Room Relative Humidity [%d %%]" (eBunkAirthings) { homekit="HumiditySensor.RelativeHumidity" }
Number:Temperature Bunk_AmbTemp "Bunk Room Temperature [%.1f °F]" (eBunkAirthings) { homekit="TemperatureSensor.CurrentTemperature" }
Number:Dimensionless Bunk_tVOC "Bunk Room tVOC [%d ppb]" (eBunkAirthings) { homekit="AirQualitySensor.VOCDensity" [ maxValue=10000 ] }
```

A sensor with a battery configured in MainUI:

![ui_sensor_with_battery.png](doc/ui_sensor_with_battery.png)

The Home app uses the first accessory in a group as the icon for the group as a whole.
E.g. an accessory defined as `homekit="Fan,Light"` will be shown as a fan and an accessory defined as `homekit="Light,Fan"` will be shown as a light in the Home app.
You can also override the primary service by using adding `primary=<type>` to the HomeKit metadata configuration:

```java
Group FanWithLight "Fan with Light" {homekit = "Light,Fan" [primary = "Fan"]}
```

on in MainUI:

![ui_fan_with_light_primary.png](doc/ui_fan_with_light_primary.png)

Unusual combinations are also possible, e.g. you can combine temperature sensor with blinds and light.

It will be represented by the Home app as follows:

![ios_complex_accessory_detail_screen.png](doc/ios_complex_accessory_detail_screen.png)

Note that for sensors that aren't interactive, the Home app will show the constituent pieces in the room and home summaries, and you'll only be able to see the combined accessory when viewing the accessories associated with a particular bridge in the home settings:

![Triple Air Sensor](doc/triple_air_sensor.png)
![Triple Air Sensor Broken Out](doc/triple_air_sensor_broken_out.png)

## Dummy Accessories

OpenHAB is a highly dynamic system, and prone to occasional misconfigurations where items can't be loaded for various reasons, especially if you're using something besides the UI to manage your items.
Expand Down Expand Up @@ -528,79 +607,6 @@ Switch motionsensor_tampered "Motion Sensor Tampered"
or using UI

![sensor_ui_config.png](doc/sensor_ui_config.png)

### Complex accessory

Multiple HomeKit accessories can be combined to one accessory in order to group several functions provided by one or multiple physical devices.

For example, ceiling fans often include lighting functionality. Such fans can be modeled as:

- two separate HomeKit accessories - fan **and** light.

iOS home app would show them as **two tiles** that can be controlled directly from home screen.

![ios_fan_and_light_home_screen.png](doc/ios_fan_and_light_home_screen.png)

- one complex accessory - fan **with** light.

iOS home app would show them as **one tile** that opens view with two controls

![ios_fan_with_light_home_screen.png](doc/ios_fan_with_light_home_screen.png)

![ios_fan_with_light_details.png](doc/ios_fan_with_light_details.png)

The provided functionality is in both cases identical.

In order to combine multiple accessories to one HomeKit accessory you need:

- add corresponding openHAB items to one openHAB group
- configure HomeKit metadata of both HomeKit accessories at that group.

e.g. configuration for a fan with light would look as follows

```xtend
Group FanWithLight "Fan with Light" {homekit = "Fan,Lighting"}
Switch FanActiveStatus "Fan Active Status" (FanWithLight) {homekit = "Fan.ActiveStatus"}
Number FanRotationSpeed "Fan Rotation Speed" (FanWithLight) {homekit = "Fan.RotationSpeed"}
Switch Light "Light" (FanWithLight) {homekit = "Lighting.OnState"}
```

or in mainUI
![ui_fan_with_light_group_view.png](doc/ui_fan_with_light_group_view.png)
![ui_fan_with_light_group_code.png](doc/ui_fan_with_light_group_code.png)
![ui_fan_with_light_group_config.png](doc/ui_fan_with_light_group_config.png)


iOS home app uses by default the type of the first accessory on the list for the tile on home screen.
e.g. an accessory defined as homekit = "Fan,Light" will be shown as a fan and an accessory defined as homekit = "Light,Fan" as a light in iOS home app.

if you want to change the tile you can either change the order of types in homekit metadata or add "primary=<type>" to HomeKit metadata configuration.
e.g. following configuration will force "fan" to be used as tile

```xtend
Group FanWithLight "Fan with Light" {homekit = "Light,Fan" [primary = "Fan"]}
```

![ui_fan_with_light_primary.png](doc/ui_fan_with_light_primary.png)

Similarly, you can create a sensor with battery

![ui_sensor_with_battery.png](doc/ui_sensor_with_battery.png)

However, home app does not support changing of tiles for already added accessory.
If you want to change the tile after the accessory was added, you need either to rename the group, if you use textual item configuration, or to delete and to create a new group with a different name, if you use UI for configuration.

You can combine more than two accessories as well as accessories linked to different physical devices.
You can also do unusually combinations, e.g. you can combine temperature sensor with blinds and light.
It will be represented by home app as follows

![ios_complex_accessory_detail_screen.png](doc/ios_complex_accessory_detail_screen.png)


#### Limitations

Currently, it is not possible to combine multiple accessories of the same type, e.g. 2 lights.
Support for this is planned for the future release of openHAB HomeKit binding.

## Supported accessory type

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public enum HomekitAccessoryType {
FAUCET("Faucet"),
MICROPHONE("Microphone"),
SLAT("Slat"),
ACCESSORY_GROUP("AccessoryGroup"),
DUMMY("Dummy");

private static final Map<String, HomekitAccessoryType> TAG_MAP = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.openhab.io.homekit.internal;

import java.lang.reflect.InvocationTargetException;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
Expand Down Expand Up @@ -47,6 +48,7 @@
import org.slf4j.LoggerFactory;

import io.github.hapjava.accessories.HomekitAccessory;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.server.impl.HomekitRoot;

/**
Expand Down Expand Up @@ -438,11 +440,19 @@ private HomekitAccessoryType getPrimaryAccessoryType(Item item,
private void createRootAccessories(Item item) {
final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessoryTypes = HomekitAccessoryFactory
.getAccessoryTypes(item, metadataRegistry);
if (accessoryTypes.isEmpty()) {
return;
}

final List<GroupItem> groups = HomekitAccessoryFactory.getAccessoryGroups(item, itemRegistry, metadataRegistry);
// Don't create accessories that are sub-accessories of other accessories
if (groups.stream().anyMatch(g -> !HomekitAccessoryFactory.getAccessoryTypes(g, metadataRegistry).isEmpty())) {
return;
}

final @Nullable Map<String, Object> itemConfiguration = HomekitAccessoryFactory.getItemConfiguration(item,
metadataRegistry);
if (accessoryTypes.isEmpty() || !(groups.isEmpty() || groups.stream().noneMatch(g -> g.getBaseItem() == null))
|| !itemIsForThisBridge(item, itemConfiguration)) {
if (!itemIsForThisBridge(item, itemConfiguration)) {
return;
}

Expand All @@ -451,19 +461,37 @@ private void createRootAccessories(Item item) {
logger.trace("Item {} is a HomeKit accessory of types {}. Primary type is {}", item.getName(), accessoryTypes,
primaryAccessoryType);
final HomekitOHItemProxy itemProxy = new HomekitOHItemProxy(item);
final HomekitTaggedItem taggedItem = new HomekitTaggedItem(new HomekitOHItemProxy(item), primaryAccessoryType,
itemConfiguration);
final HomekitTaggedItem taggedItem = new HomekitTaggedItem(itemProxy, primaryAccessoryType, itemConfiguration);
try {
final AbstractHomekitAccessoryImpl accessory = HomekitAccessoryFactory.create(taggedItem, metadataRegistry,
updater, settings);
if (accessory.isLinkedServiceOnly()) {
logger.warn("Item '{}' is a '{}' which must be nested another another accessory.", taggedItem.getName(),
primaryAccessoryType);
return;
}

accessoryTypes.stream().filter(aType -> !primaryAccessoryType.equals(aType.getKey()))
.forEach(additionalAccessoryType -> {
final HomekitTaggedItem additionalTaggedItem = new HomekitTaggedItem(itemProxy,
additionalAccessoryType.getKey(), itemConfiguration);
try {
final HomekitAccessory additionalAccessory = HomekitAccessoryFactory
final AbstractHomekitAccessoryImpl additionalAccessory = HomekitAccessoryFactory
.create(additionalTaggedItem, metadataRegistry, updater, settings);
// Secondary accessories that don't explicitly specify a name will implicitly
// get a name characteristic based on the item's name
if (!additionalAccessory.getCharacteristic(HomekitCharacteristicType.NAME).isPresent()) {
try {
additionalAccessory.addCharacteristic(
new NameCharacteristic(() -> additionalAccessory.getName()));
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
// This should never happen; all services should support NameCharacteristic as an
// optional Characteristic.
// If HAP-Java defined a service that doesn't support
// addOptionalCharacteristic(NameCharacteristic), then it's a bug there, and we're
// just going to ignore the exception here.
}
}
accessory.getServices().add(additionalAccessory.getPrimaryService());
} catch (HomekitException e) {
logger.warn("Cannot create additional accessory {}", additionalTaggedItem);
Expand Down
Loading

0 comments on commit b62e145

Please sign in to comment.