Skip to content

Commit 2ec7616

Browse files
committed
[shelly] Consolidate light API component handling into ShellyApiLightUtil with type-safe enum
Replace scattered profile.device.profile string-equality checks (SHELLY2_PROFILE_CCTX2.equals(...) etc.) and RPC-method-name comparisons with static isColorComponent/isRgbComponent/ isRgbwComponent/isCctComponent/isLightComponent(tag) helpers backed by the settings.lights[i].apiComponent tag rather than the whole- profile string. Add profile.isProRgbwwPm to replace the local isProRgbwwPmProfile() duplicate, and add profile.isCctComponent(idx) alongside the existing isColorComponent(idx). Also collapse ShellyUtils.buildControlGroupName/buildWhiteGroupName into profile.getControlGroup(), removing the now-redundant duplicate group resolution logic. The tag itself moves from a raw String ("rgb"/"rgbw"/"cct"/"light") to a ShellyLightApiComponent enum, making an invalid tag a compile-time impossibility instead of a silent runtime fallback to the LIGHT RPC methods in Shelly2ApiRpc.lightRpcMethods(). The field stays transient and is set only in Shelly2ApiClient.createRgbwLightSetting() (Gen2-only, never Gson-(de)serialized), so this is not a breaking change. Static logic doesn't belong on a DTO class, so the helpers land in a new ShellyApiLightUtil in the api package rather than on the component tag holder itself. While at it, fold in the other light-specific group/id helpers (getLightIdFromGroup, lightChannelGroupPrefix, buildWhiteGroupName) that were sitting in the generic ShellyUtils for the same reason, and add missing coverage for the tag-lookup helpers and lightChannelGroupPrefix. Also: - Rename ShellyDeviceProfile.isColorComponent(int) to hasColorTag(int) to resolve a name collision with the unrelated, differently-scoped ShellyApiLightUtil.isColorComponent(ShellyLightApiComponent). - Restore the setLightParms() fail-fast guard for non-RGBW2 profiles and unify its RGB/RGBW/CCT/Light dispatch onto the same lightRpcMethods() lookup already used elsewhere in this class. - Document on ShellyLightApiComponent that each settings.lights index is an independently addressed physical component, not a sub-channel of one combined light - relevant for PR openhab#21286's OH Core LightModel work, whose Pro RGBWW PM branch currently assumes one model per whole-device profile string. Signed-off-by: Markus Michels <markus7017@gmail.com>
1 parent 5e935e7 commit 2ec7616

17 files changed

Lines changed: 519 additions & 233 deletions
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.shelly.internal.api;
14+
15+
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16+
import static org.openhab.binding.shelly.internal.util.ShellyUtils.substringAfter;
17+
18+
import java.util.List;
19+
20+
import org.eclipse.jdt.annotation.NonNullByDefault;
21+
import org.eclipse.jdt.annotation.Nullable;
22+
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
23+
24+
/**
25+
* The {@link ShellyApiLightUtil} provides light-specific helpers shared by Gen1 (api1) and Gen2+ (api2) code:
26+
* the {@code settings.lights[i].apiComponent} tag values used to identify which light API component family (RGB,
27+
* RGBW, CCT or plain Light) a {@code ShellySettingsRgbwLight} entry belongs to, and the light channel group/id
28+
* resolution shared across handlers.
29+
*
30+
* @author Markus Michels - Initial contribution
31+
*/
32+
@NonNullByDefault
33+
public class ShellyApiLightUtil {
34+
private ShellyApiLightUtil() {
35+
}
36+
37+
/**
38+
* The light API component family a {@code settings.lights[i]} entry belongs to. {@code NONE} marks an
39+
* untagged entry (a Gen1 RGBW2/Bulb entry never sets apiComponent) or an out-of-range index.
40+
* <p>
41+
* Each index is an independently switchable/dimmable/metered physical component - not a sub-channel of one
42+
* combined light. A hybrid Pro RGBWW PM profile (e.g. {@code rgbcct}, {@code rgbx2light}) reports its color
43+
* component (RGB/RGBW) and its secondary CCT/Light component(s) as separate {@code settings.lights} indices,
44+
* each with its own RPC methods and meter - never merged into a single multi-channel model.
45+
*/
46+
public enum ShellyLightApiComponent {
47+
RGB,
48+
RGBW,
49+
CCT,
50+
LIGHT,
51+
NONE
52+
}
53+
54+
/**
55+
* True when tag identifies an RGB/RGBW color component, as opposed to a CCT/Light one.
56+
*/
57+
public static boolean isColorComponent(ShellyLightApiComponent apiComponent) {
58+
return apiComponent == ShellyLightApiComponent.RGB || apiComponent == ShellyLightApiComponent.RGBW;
59+
}
60+
61+
public static boolean isRgbComponent(ShellyLightApiComponent apiComponent) {
62+
return apiComponent == ShellyLightApiComponent.RGB;
63+
}
64+
65+
public static boolean isRgbwComponent(ShellyLightApiComponent apiComponent) {
66+
return apiComponent == ShellyLightApiComponent.RGBW;
67+
}
68+
69+
public static boolean isCctComponent(ShellyLightApiComponent apiComponent) {
70+
return apiComponent == ShellyLightApiComponent.CCT;
71+
}
72+
73+
public static boolean isLightComponent(ShellyLightApiComponent apiComponent) {
74+
return apiComponent == ShellyLightApiComponent.LIGHT;
75+
}
76+
77+
/**
78+
* The apiComponent tag of {@code lights.get(idx)}, or {@code NONE} when lights is null, idx is out of range,
79+
* or the entry is untagged (a Gen1 RGBW2/Bulb entry never sets apiComponent).
80+
*/
81+
public static ShellyLightApiComponent tagAt(@Nullable List<ShellySettingsRgbwLight> lights, int idx) {
82+
return lights != null && idx >= 0 && idx < lights.size() ? lights.get(idx).apiComponent
83+
: ShellyLightApiComponent.NONE;
84+
}
85+
86+
/**
87+
* True when any entry in lights is an RGB/RGBW color component.
88+
*/
89+
public static boolean hasColorComponent(List<ShellySettingsRgbwLight> lights) {
90+
return lights.stream().map(l -> l.apiComponent).anyMatch(ShellyApiLightUtil::isColorComponent);
91+
}
92+
93+
public static Integer getLightIdFromGroup(String groupName) {
94+
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_INDEX)) {
95+
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_INDEX)) - 1;
96+
}
97+
if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
98+
return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1;
99+
}
100+
return 0; // only 1 light, e.g. bulb or rgbw2 in color mode
101+
}
102+
103+
// Gen2 RGBW PM ships on light1..n natively. A Gen1 RGBW2 Thing that already carries the
104+
// deprecated channel1..n group (from before this Thing was migrated) keeps publishing there,
105+
// dual-written to light1..n by ShellyBaseHandler.updateChannel(); a freshly discovered Gen1
106+
// RGBW2 Thing goes straight to light1..n and never gets a channel1..n group.
107+
public static String lightChannelGroupPrefix(ShellyDeviceProfile profile) {
108+
return profile.isGen2 || !profile.hasLegacyLightChannels ? CHANNEL_GROUP_LIGHT_INDEX
109+
: CHANNEL_GROUP_LIGHT_CHANNEL;
110+
}
111+
112+
// Bulb/Duo report white/temp under a dedicated group, distinct from their color group; everything else
113+
// (including a hybrid Pro RGBWW PM profile's secondary CCT/Light component) shares the same per-component
114+
// group resolution as the color channels, so this just delegates to getControlGroup().
115+
public static String buildWhiteGroupName(ShellyDeviceProfile profile, int lightId) {
116+
if (profile.isBulb || profile.isDuo) {
117+
return CHANNEL_GROUP_WHITE_CONTROL;
118+
}
119+
return profile.getControlGroup(lightId);
120+
}
121+
}

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
1616
import static org.openhab.binding.shelly.internal.ShellyDevices.*;
17+
import static org.openhab.binding.shelly.internal.api.ShellyApiLightUtil.*;
1718
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
1819
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
1920

@@ -95,6 +96,8 @@ public class ShellyDeviceProfile {
9596
public boolean isBulb; // true only if it is a Bulb
9697
public boolean isDuo; // true only if it is a Duo
9798
public boolean isRGBW2; // true only if it a RGBW2
99+
public boolean isProRgbwwPm; // true only for a Shelly Pro RGBWW PM (device.profile alone can't tell it apart
100+
// from a Plus RGBW PM running the same rgb/rgbw/light profile)
98101
public boolean inColor; // true if bulb/rgbw2 is in color mode
99102
public boolean hasLegacyLightChannels; // true if Thing already has deprecated Gen1 RGBW2 channel1..n groups
100103

@@ -214,6 +217,7 @@ public void initFromThingType(ThingTypeUID thingTypeUID) {
214217
isBulb = THING_TYPE_SHELLYBULB.equals(thingTypeUID);
215218
isDuo = GROUP_DUO_THING_TYPES.contains(thingTypeUID);
216219
isRGBW2 = GROUP_RGBW2_THING_TYPES.contains(thingTypeUID);
220+
isProRgbwwPm = THING_TYPE_SHELLYPRORGBWWPM.equals(thingTypeUID);
217221
isLight = GROUP_LIGHT_THING_TYPES.contains(thingTypeUID);
218222
if (isLight) {
219223
minTemp = isBulb ? MIN_COLOR_TEMP_BULB : MIN_COLOR_TEMP_DUO;
@@ -366,13 +370,17 @@ public int getColorComponentCount() {
366370
* because a hybrid profile's (rgbcct, rgbx2light) secondary component(s) are not color even though the
367371
* whole-profile inColor flag is true. Untagged (Gen1 RGBW2) entries fall back to that whole-profile flag.
368372
*/
369-
public boolean isColorComponent(int idx) {
370-
List<ShellySettingsRgbwLight> lights = settings.lights;
371-
if (lights == null || idx < 0 || idx >= lights.size()) {
372-
return inColor;
373-
}
374-
String tag = lights.get(idx).apiComponent;
375-
return tag.isEmpty() ? inColor : ("rgb".equals(tag) || "rgbw".equals(tag));
373+
public boolean hasColorTag(int idx) {
374+
ShellyLightApiComponent tag = tagAt(settings.lights, idx);
375+
return tag == ShellyLightApiComponent.NONE ? inColor : ShellyApiLightUtil.isColorComponent(tag);
376+
}
377+
378+
/**
379+
* True when settings.lights[idx] is a CCT (color temperature) component - Gen2 only, untagged Gen1 entries are
380+
* never CCT.
381+
*/
382+
public boolean isCctComponent(int idx) {
383+
return ShellyApiLightUtil.isCctComponent(tagAt(settings.lights, idx));
376384
}
377385

378386
public String getInputGroup(int i) {

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.List;
1717

1818
import org.eclipse.jdt.annotation.Nullable;
19+
import org.openhab.binding.shelly.internal.api.ShellyApiLightUtil.ShellyLightApiComponent;
1920
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
2021
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2APClientList;
2122
import org.openhab.core.thing.CommonTriggerEvents;
@@ -522,8 +523,8 @@ public static class ShellySettingsRgbwLight {
522523
@SerializedName("out_off_url")
523524
public String outOffUrl; // output is deactivated
524525

525-
// Gen2 (Pro RGBWW PM) only: which RPC component this entry maps to, see Shelly2ApiRpc.API_COMPONENT_*
526-
public transient String apiComponent = "";
526+
// Gen2 (Pro RGBWW PM) only: which RPC component this entry maps to, see ShellyApiLightUtil
527+
public transient ShellyLightApiComponent apiComponent = ShellyLightApiComponent.NONE;
527528
}
528529

529530
public static class ShellyFavPos { // FW 1.9.2+ in roller mode

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTProtocol.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.openhab.binding.shelly.internal.api1;
1414

1515
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16+
import static org.openhab.binding.shelly.internal.api.ShellyApiLightUtil.*;
1617
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
1718
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
1819

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.CHANNEL_INPUT;
1616
import static org.openhab.binding.shelly.internal.ShellyDevices.THING_TYPE_SHELLYPRORGBWWPM;
17+
import static org.openhab.binding.shelly.internal.api.ShellyApiLightUtil.*;
1718
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
1819
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
1920
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
@@ -854,12 +855,6 @@ private void updateComponentMeter(ShellySettingsStatus status, int meterIdx, @Nu
854855
updateMeter(status, meterIdx, emeter, channelUpdate);
855856
}
856857

857-
private boolean isProRgbwwPmProfile(ShellyDeviceProfile profile) {
858-
String p = profile.device.profile;
859-
return SHELLY2_PROFILE_RGBCCT.equals(p) || SHELLY2_PROFILE_CCTX2.equals(p)
860-
|| SHELLY2_PROFILE_RGBX2LIGHT.equals(p);
861-
}
862-
863858
/**
864859
* Convert aenergy.by_minute (mWh per complete minute, up to 3 slots) to Wh.
865860
* Returns null when the device did not report a usable slot 0 (e.g. clock not synced).
@@ -1222,13 +1217,17 @@ protected void fillRgbwSettings(ShellyDeviceProfile profile, Shelly2GetConfigRes
12221217
return;
12231218
}
12241219

1225-
record Candidate(String apiComponent, @Nullable Shelly2GetConfigLight config) {
1220+
record Candidate(ShellyLightApiComponent apiComponent, @Nullable Shelly2GetConfigLight config) {
12261221
}
1227-
List<Candidate> candidates = List.of(new Candidate(API_COMPONENT_RGBW, dc.rgbw0),
1228-
new Candidate(API_COMPONENT_RGB, dc.rgb0), new Candidate(API_COMPONENT_CCT, dc.cct0),
1229-
new Candidate(API_COMPONENT_CCT, dc.cct1), new Candidate(API_COMPONENT_LIGHT, dc.light0),
1230-
new Candidate(API_COMPONENT_LIGHT, dc.light1), new Candidate(API_COMPONENT_LIGHT, dc.light2),
1231-
new Candidate(API_COMPONENT_LIGHT, dc.light3), new Candidate(API_COMPONENT_LIGHT, dc.light4));
1222+
List<Candidate> candidates = List.of(new Candidate(ShellyLightApiComponent.RGBW, dc.rgbw0),
1223+
new Candidate(ShellyLightApiComponent.RGB, dc.rgb0),
1224+
new Candidate(ShellyLightApiComponent.CCT, dc.cct0),
1225+
new Candidate(ShellyLightApiComponent.CCT, dc.cct1),
1226+
new Candidate(ShellyLightApiComponent.LIGHT, dc.light0),
1227+
new Candidate(ShellyLightApiComponent.LIGHT, dc.light1),
1228+
new Candidate(ShellyLightApiComponent.LIGHT, dc.light2),
1229+
new Candidate(ShellyLightApiComponent.LIGHT, dc.light3),
1230+
new Candidate(ShellyLightApiComponent.LIGHT, dc.light4));
12321231

12331232
ArrayList<ShellySettingsRgbwLight> lights = new ArrayList<>();
12341233
for (Candidate c : candidates) {
@@ -1240,12 +1239,12 @@ record Candidate(String apiComponent, @Nullable Shelly2GetConfigLight config) {
12401239
if (lights.isEmpty()) {
12411240
lights.add(new ShellySettingsRgbwLight());
12421241
}
1243-
profile.inColor = lights.stream()
1244-
.anyMatch(l -> API_COMPONENT_RGB.equals(l.apiComponent) || API_COMPONENT_RGBW.equals(l.apiComponent));
1242+
profile.inColor = hasColorComponent(lights);
12451243
profile.settings.lights = lights;
12461244
}
12471245

1248-
private ShellySettingsRgbwLight createRgbwLightSetting(Shelly2GetConfigLight src, String apiComponent) {
1246+
private ShellySettingsRgbwLight createRgbwLightSetting(Shelly2GetConfigLight src,
1247+
ShellyLightApiComponent apiComponent) {
12491248
ShellySettingsRgbwLight ls = new ShellySettingsRgbwLight();
12501249
ls.autoOn = src.autoOnDelay;
12511250
ls.autoOff = src.autoOffDelay;
@@ -1304,7 +1303,7 @@ private boolean updateRGBWStatus(int id, ShellySettingsStatus status, @Nullable
13041303
ds.ison = value.output;
13051304

13061305
status.lights.set(rgbwId, ds);
1307-
if (isProRgbwwPmProfile(profile)) {
1306+
if (profile.isProRgbwwPm) {
13081307
// the color component always sits at settings.lights[0]
13091308
updateComponentMeter(status, 0, value.apower, value.aenergy, value.voltage, value.current, channelUpdate);
13101309
}
@@ -1344,7 +1343,7 @@ private boolean updateLightModeStatus(int id, ShellySettingsStatus status, @Null
13441343
ds.temp = ct;
13451344
}
13461345
lights.set(lightId, ds);
1347-
if (isProRgbwwPmProfile(profile)) {
1346+
if (profile.isProRgbwwPm) {
13481347
// Plus RGBW PM's white-mode light0..3 channels also reach this point but must not be metered here
13491348
updateComponentMeter(status, lightId, value.apower, value.aenergy, value.voltage, value.current,
13501349
channelUpdate);

bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,6 @@ public class Shelly2ApiJsonDTO {
114114
public static final String SHELLY2_PROFILE_CCTX2 = "cctx2"; // Pro RGBWW PM: CCT:0 + CCT:1
115115
public static final String SHELLY2_PROFILE_RGBX2LIGHT = "rgbx2light"; // Pro RGBWW PM: RGB:0 + Light:0/1
116116

117-
// Pro RGBWW PM: settings.lights[i].apiComponent tag, selects which RPC component family (and thus which
118-
// Light.*/RGB.*/RGBW.*/CCT.* method) an index maps to - see Shelly2ApiRpc.LIGHT_RPC_METHODS
119-
public static final String API_COMPONENT_RGB = "rgb";
120-
public static final String API_COMPONENT_RGBW = "rgbw";
121-
public static final String API_COMPONENT_CCT = "cct";
122-
public static final String API_COMPONENT_LIGHT = "light";
123-
124117
// Button types/modes
125118
public static final String SHELLY2_BTNT_MOMENTARY = "momentary";
126119
public static final String SHELLY2_BTNT_FLIP = "flip";

0 commit comments

Comments
 (0)