2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.insteon.internal.device;
15 import java.util.ArrayList;
16 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.insteon.internal.device.database.LinkDBRecord;
21 import org.openhab.binding.insteon.internal.device.feature.FeatureListener;
22 import org.openhab.binding.insteon.internal.handler.InsteonSceneHandler;
23 import org.openhab.core.library.types.OnOffType;
24 import org.openhab.core.types.State;
25 import org.openhab.core.types.UnDefType;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
30 * The {@link InsteonScene} represents an Insteon scene
32 * @author Jeremy Setton - Initial contribution
35 public class InsteonScene implements Scene {
36 public static final int GROUP_MIN = 2;
37 public static final int GROUP_MAX = 254;
38 // limit new scene group minimum to 25 matching the current Insteon app behavior
39 public static final int GROUP_NEW_MIN = 25;
40 public static final int GROUP_NEW_MAX = 254;
42 private final Logger logger = LoggerFactory.getLogger(InsteonScene.class);
45 private @Nullable InsteonModem modem;
46 private @Nullable InsteonSceneHandler handler;
47 private List<SceneEntry> entries = new ArrayList<>();
48 private boolean modemDBEntry = false;
50 public InsteonScene(int group) {
55 public int getGroup() {
59 public @Nullable InsteonModem getModem() {
63 public @Nullable InsteonSceneHandler getHandler() {
67 public List<SceneEntry> getEntries() {
68 synchronized (entries) {
69 return entries.stream().toList();
73 public List<SceneEntry> getEntries(InsteonAddress address) {
74 return getEntries().stream().filter(entry -> entry.getAddress().equals(address)).toList();
77 public List<InsteonAddress> getDevices() {
78 return getEntries().stream().map(SceneEntry::getAddress).distinct().toList();
81 public List<DeviceFeature> getFeatures() {
82 return getEntries().stream().map(SceneEntry::getFeature).toList();
85 public List<DeviceFeature> getFeatures(InsteonAddress address) {
86 return getEntries(address).stream().map(SceneEntry::getFeature).toList();
89 public State getState() {
90 return getEntries().stream().allMatch(entry -> entry.getState() == UnDefType.NULL) ? UnDefType.NULL
91 : OnOffType.from(getEntries().stream().filter(entry -> entry.getState() != UnDefType.NULL)
92 .allMatch(entry -> entry.getState().equals(entry.getOnState())));
95 public boolean hasEntry(InsteonAddress address) {
96 return getEntries().stream().anyMatch(entry -> entry.getAddress().equals(address));
99 public boolean hasEntry(InsteonAddress address, String featureName) {
100 return getEntries().stream().anyMatch(
101 entry -> entry.getAddress().equals(address) && entry.getFeature().getName().equals(featureName));
104 public boolean hasModemDBEntry() {
108 public boolean isComplete() {
109 InsteonModem modem = getModem();
110 return modem != null && modem.getDB().getRelatedDevices(group).stream().allMatch(this::hasEntry);
113 public void setModem(@Nullable InsteonModem modem) {
117 public void setHandler(InsteonSceneHandler handler) {
118 this.handler = handler;
121 public void setHasModemDBEntry(boolean modemDBEntry) {
122 this.modemDBEntry = modemDBEntry;
126 public String toString() {
127 return "group:" + group + "|entries:" + entries.size();
131 * Adds an entry to this scene
133 * @param entry the scene entry to add
135 private void addEntry(SceneEntry entry) {
136 logger.trace("adding entry to scene {}: {}", group, entry);
138 synchronized (entries) {
139 if (entries.add(entry)) {
146 * Deletes an entry from this scene
148 * @param entry the scene entry to delete
150 private void deleteEntry(SceneEntry entry) {
151 synchronized (entries) {
152 if (entries.remove(entry)) {
159 * Deletes all entries from this scene
161 public void deleteEntries() {
162 getEntries().forEach(this::deleteEntry);
166 * Deletes entries for a given device from this scene
168 * @param address the device address
170 public void deleteEntries(InsteonAddress address) {
171 logger.trace("removing entries from scene {} for device {}", group, address);
173 getEntries(address).forEach(this::deleteEntry);
177 * Updates all entries for this scene
179 public void updateEntries() {
180 synchronized (entries) {
184 InsteonModem modem = getModem();
186 for (InsteonAddress address : modem.getDB().getRelatedDevices(group)) {
187 InsteonDevice device = modem.getInsteonDevice(address);
188 if (device == null) {
189 logger.debug("device {} part of scene {} not enabled or configured, ignoring.", address, group);
191 updateEntries(device);
198 * Updates entries related to a given device for this scene
200 * @param device the device
202 public void updateEntries(InsteonDevice device) {
203 InsteonAddress address = device.getAddress();
205 logger.trace("updating entries for scene {} device {}", group, address);
207 getEntries(address).forEach(this::deleteEntry);
209 InsteonModem modem = getModem();
211 for (LinkDBRecord record : device.getLinkDB().getResponderRecords(modem.getAddress(), group)) {
212 device.getResponderFeatures().stream()
213 .filter(feature -> feature.getComponentId() == record.getComponentId()).findFirst()
214 .ifPresent(feature -> addEntry(new SceneEntry(address, feature, record.getData())));
220 * Resets state for this scene
222 public void resetState() {
223 logger.trace("resetting state for scene {}", group);
225 getEntries().forEach(entry -> entry.setState(UnDefType.NULL));
229 * Updates state for this scene
231 private void updateState() {
232 State state = getState();
233 InsteonSceneHandler handler = getHandler();
234 if (handler != null && state instanceof OnOffType) {
235 handler.updateState(state);
240 * Adds a device feature to this scene
242 * @param device the device
243 * @param onLevel the feature on level
244 * @param rampRate the feature ramp rate
245 * @param componentId the feature component id
247 public void addDeviceFeature(InsteonDevice device, int onLevel, @Nullable RampRate rampRate, int componentId) {
248 InsteonModem modem = getModem();
249 if (modem == null || !modem.getDB().isComplete() || !device.getLinkDB().isComplete()) {
253 modem.getDB().clearChanges();
254 modem.getDB().markRecordForAddOrModify(device.getAddress(), group, true);
255 modem.getDB().update();
257 device.getLinkDB().clearChanges();
258 device.getLinkDB().markRecordForAddOrModify(modem.getAddress(), group, false, new byte[] { (byte) onLevel,
259 (byte) (rampRate != null ? rampRate.getValue() : 0x00), (byte) componentId });
260 device.getLinkDB().update();
264 * Removes a device feature from this scene
266 * @param device the device
267 * @param componentId the feature component id
269 public void removeDeviceFeature(InsteonDevice device, int componentId) {
270 InsteonModem modem = getModem();
271 if (modem == null || !modem.getDB().isComplete() || !device.getLinkDB().isComplete()) {
275 modem.getDB().clearChanges();
276 modem.getDB().markRecordForDelete(device.getAddress(), group);
277 modem.getDB().update();
279 device.getLinkDB().clearChanges();
280 device.getLinkDB().markRecordForDelete(modem.getAddress(), group, false, componentId);
281 device.getLinkDB().update();
285 * Initializes this scene
287 public void initialize() {
288 InsteonModem modem = getModem();
289 if (modem == null || !modem.getDB().isComplete()) {
293 if (!modem.getDB().hasBroadcastGroup(group)) {
294 logger.warn("scene {} not found in the modem database.", group);
295 setHasModemDBEntry(false);
299 if (!hasModemDBEntry()) {
300 logger.debug("scene {} found in the modem database.", group);
301 setHasModemDBEntry(true);
308 * Refreshes this scene
311 public void refresh() {
312 logger.trace("refreshing scene {}", group);
316 InsteonSceneHandler handler = getHandler();
317 if (handler != null) {
323 * Class that represents a scene entry
325 public class SceneEntry implements FeatureListener {
326 private InsteonAddress address;
327 private DeviceFeature feature;
329 private State state = UnDefType.NULL;
331 public SceneEntry(InsteonAddress address, DeviceFeature feature, byte[] data) {
332 this.address = address;
333 this.feature = feature;
337 public InsteonAddress getAddress() {
341 public DeviceFeature getFeature() {
345 public State getOnState() {
346 return OnLevel.getState(Byte.toUnsignedInt(data[0]), feature.getType());
349 public RampRate getRampRate() {
350 return RampRate.valueOf(Byte.toUnsignedInt(data[1]));
353 public State getState() {
357 public void setState(State state) {
361 public void register() {
362 feature.registerListener(this);
364 stateUpdated(feature.getState());
367 public void unregister() {
368 feature.unregisterListener(this);
372 public String toString() {
373 String s = address + " " + feature.getName() + " currentState: " + state + " onState: " + getOnState();
374 if (RampRate.supportsFeatureType(feature.getType())) {
375 s += " rampRate: " + getRampRate();
381 public void stateUpdated(State state) {
387 public void eventTriggered(String event) {
393 * Returns if scene group is valid
395 * @param group the scene group
396 * @return true if group is an integer within supported range
398 public static boolean isValidGroup(String group) {
400 return isValidGroup(Integer.parseInt(group));
401 } catch (NumberFormatException e) {
407 * Returns if scene group is valid
409 * @param group the scene group
410 * @return true if group within supported range
412 public static boolean isValidGroup(int group) {
413 return group >= GROUP_MIN && group <= GROUP_MAX;
417 * Factory method for creating a InsteonScene from a scene group and modem
419 * @param group the scene group
420 * @param modem the scene modem
421 * @return the newly created InsteonScene
423 public static InsteonScene makeScene(int group, @Nullable InsteonModem modem) {
424 InsteonScene scene = new InsteonScene(group);
425 scene.setModem(modem);