-
-
Notifications
You must be signed in to change notification settings - Fork 415
/
PageChangeListener.java
347 lines (313 loc) · 13.2 KB
/
PageChangeListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* Copyright (c) 2010-2022 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.core.io.rest.sitemap.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import org.eclipse.emf.common.util.EList;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.StateChangeListener;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.model.sitemap.sitemap.Chart;
import org.openhab.core.model.sitemap.sitemap.ColorArray;
import org.openhab.core.model.sitemap.sitemap.Frame;
import org.openhab.core.model.sitemap.sitemap.VisibilityRule;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.types.State;
import org.openhab.core.ui.items.ItemUIRegistry;
/**
* This is a class that listens on item state change events and creates sitemap events for a dedicated sitemap page.
*
* @author Kai Kreuzer - Initial contribution
*/
public class PageChangeListener implements StateChangeListener {
private static final int REVERT_INTERVAL = 300;
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private final String sitemapName;
private final String pageId;
private final ItemUIRegistry itemUIRegistry;
private EList<Widget> widgets;
private Set<Item> items;
private final List<SitemapSubscriptionCallback> callbacks = Collections.synchronizedList(new ArrayList<>());
private Set<SitemapSubscriptionCallback> distinctCallbacks = Collections.emptySet();
/**
* Creates a new instance.
*
* @param sitemapName the sitemap name of the page
* @param pageId the id of the page for which events are created
* @param itemUIRegistry the ItemUIRegistry which is needed for the functionality
* @param widgets the list of widgets that are part of the page.
*/
public PageChangeListener(String sitemapName, String pageId, ItemUIRegistry itemUIRegistry, EList<Widget> widgets) {
this.sitemapName = sitemapName;
this.pageId = pageId;
this.itemUIRegistry = itemUIRegistry;
updateItemsAndWidgets(widgets);
}
private void updateItemsAndWidgets(EList<Widget> widgets) {
if (this.widgets != null) {
// cleanup statechange listeners in case widgets were removed
items = getAllItems(this.widgets);
for (Item item : items) {
if (item instanceof GenericItem) {
((GenericItem) item).removeStateChangeListener(this);
}
}
}
this.widgets = widgets;
items = getAllItems(widgets);
for (Item item : items) {
if (item instanceof GenericItem) {
((GenericItem) item).addStateChangeListener(this);
}
}
}
public String getSitemapName() {
return sitemapName;
}
public String getPageId() {
return pageId;
}
public void addCallback(SitemapSubscriptionCallback callback) {
callbacks.add(callback);
// we transform the list of callbacks to a set in order to remove duplicates
distinctCallbacks = new HashSet<>(callbacks);
}
public void removeCallback(SitemapSubscriptionCallback callback) {
callbacks.remove(callback);
distinctCallbacks = new HashSet<>(callbacks);
}
/**
* Disposes this instance and releases all resources.
*/
public void dispose() {
for (Item item : items) {
if (item instanceof GenericItem) {
((GenericItem) item).removeStateChangeListener(this);
} else if (item instanceof GroupItem) {
((GroupItem) item).removeStateChangeListener(this);
}
}
}
/**
* Collects all items that are represented by a given list of widgets
*
* @param widgets
* the widget list to get the items for added to all bundles containing REST resources
* @return all items that are represented by the list of widgets
*/
private Set<Item> getAllItems(EList<Widget> widgets) {
Set<Item> items = new HashSet<>();
if (itemUIRegistry != null) {
for (Widget widget : widgets) {
addItemWithName(items, widget.getItem());
if (widget instanceof Frame) {
items.addAll(getAllItems(((Frame) widget).getChildren()));
}
// now scan visibility rules
for (VisibilityRule rule : widget.getVisibility()) {
addItemWithName(items, rule.getItem());
}
// now scan label color rules
for (ColorArray rule : widget.getLabelColor()) {
addItemWithName(items, rule.getItem());
}
// now scan value color rules
for (ColorArray rule : widget.getValueColor()) {
addItemWithName(items, rule.getItem());
}
}
}
return items;
}
private void addItemWithName(Set<Item> items, String itemName) {
if (itemName != null) {
try {
Item item = itemUIRegistry.getItem(itemName);
items.add(item);
} catch (ItemNotFoundException e) {
// ignore
}
}
}
private void constructAndSendEvents(Item item, State newState) {
Set<SitemapEvent> events = constructSitemapEvents(item, newState, widgets);
for (SitemapEvent event : events) {
for (SitemapSubscriptionCallback callback : distinctCallbacks) {
callback.onEvent(event);
}
}
}
@Override
public void stateChanged(Item item, State oldState, State newState) {
// For all items except group, send an event only when the event state is changed.
if (item instanceof GroupItem) {
return;
}
constructAndSendEvents(item, newState);
}
@Override
public void stateUpdated(Item item, State state) {
// For group item only, send an event each time the event state is updated.
// It allows updating the group label while the group state is unchanged,
// for example the count in label for Group:Switch:OR
if (!(item instanceof GroupItem)) {
return;
}
constructAndSendEvents(item, state);
}
public void keepCurrentState(Item item) {
scheduler.schedule(() -> {
constructAndSendEvents(item, item.getState());
}, REVERT_INTERVAL, TimeUnit.MILLISECONDS);
}
public void changeStateTo(Item item, State state) {
constructAndSendEvents(item, state);
}
private Set<SitemapEvent> constructSitemapEvents(Item item, State state, List<Widget> widgets) {
Set<SitemapEvent> events = new HashSet<>();
for (Widget w : widgets) {
if (w instanceof Frame) {
events.addAll(constructSitemapEvents(item, state, itemUIRegistry.getChildren((Frame) w)));
}
boolean itemBelongsToWidget = w.getItem() != null && w.getItem().equals(item.getName());
boolean skipWidget = !itemBelongsToWidget;
// We skip the chart widgets having a refresh argument
if (!skipWidget && w instanceof Chart) {
Chart chartWidget = (Chart) w;
skipWidget = chartWidget.getRefresh() > 0;
}
if (!skipWidget || definesVisibilityOrColor(w, item.getName())) {
SitemapWidgetEvent event = constructSitemapEventForWidget(item, state, w);
events.add(event);
}
}
return events;
}
private SitemapWidgetEvent constructSitemapEventForWidget(Item item, State state, Widget widget) {
SitemapWidgetEvent event = new SitemapWidgetEvent();
event.sitemapName = sitemapName;
event.pageId = pageId;
event.label = itemUIRegistry.getLabel(widget);
event.labelcolor = itemUIRegistry.getLabelColor(widget);
event.valuecolor = itemUIRegistry.getValueColor(widget);
event.widgetId = itemUIRegistry.getWidgetId(widget);
event.visibility = itemUIRegistry.getVisiblity(widget);
event.descriptionChanged = false;
// event.item contains the (potentially changed) data of the item belonging to
// the widget including its state (in event.item.state)
boolean itemBelongsToWidget = widget.getItem() != null && widget.getItem().equals(item.getName());
final Item itemToBeSent = itemBelongsToWidget ? item : getItemForWidget(widget);
if (itemToBeSent != null) {
String widgetTypeName = widget.eClass().getInstanceTypeName()
.substring(widget.eClass().getInstanceTypeName().lastIndexOf(".") + 1);
boolean drillDown = "mapview".equalsIgnoreCase(widgetTypeName);
Predicate<Item> itemFilter = (i -> CoreItemFactory.LOCATION.equals(i.getType()));
event.item = EnrichedItemDTOMapper.map(itemToBeSent, drillDown, itemFilter, null, null);
// event.state is an adjustment of the item state to the widget type.
final State stateToBeSent = itemBelongsToWidget ? state : itemToBeSent.getState();
event.state = itemUIRegistry.convertState(widget, itemToBeSent, stateToBeSent).toFullString();
// In case this state is identical to the item state, its value is set to null.
if (event.state != null && event.state.equals(event.item.state)) {
event.state = null;
}
}
return event;
}
private Item getItemForWidget(Widget w) {
final String itemName = w.getItem();
if (itemName != null) {
try {
return itemUIRegistry.getItem(itemName);
} catch (ItemNotFoundException e) {
// fall through to returning null
}
}
return null;
}
private boolean definesVisibilityOrColor(Widget w, String name) {
for (VisibilityRule rule : w.getVisibility()) {
if (name.equals(rule.getItem())) {
return true;
}
}
for (ColorArray rule : w.getLabelColor()) {
if (name.equals(rule.getItem())) {
return true;
}
}
for (ColorArray rule : w.getValueColor()) {
if (name.equals(rule.getItem())) {
return true;
}
}
return false;
}
public void sitemapContentChanged(EList<Widget> widgets) {
updateItemsAndWidgets(widgets);
SitemapChangedEvent changeEvent = new SitemapChangedEvent();
changeEvent.pageId = pageId;
changeEvent.sitemapName = sitemapName;
for (SitemapSubscriptionCallback callback : distinctCallbacks) {
callback.onEvent(changeEvent);
}
}
public void sendAliveEvent() {
ServerAliveEvent aliveEvent = new ServerAliveEvent();
aliveEvent.pageId = pageId;
aliveEvent.sitemapName = sitemapName;
for (SitemapSubscriptionCallback callback : distinctCallbacks) {
callback.onEvent(aliveEvent);
}
}
public void descriptionChanged(String itemName) {
try {
Item item = itemUIRegistry.getItem(itemName);
Set<SitemapEvent> events = constructSitemapEventsForUpdatedDescr(item, widgets);
for (SitemapEvent event : events) {
for (SitemapSubscriptionCallback callback : distinctCallbacks) {
callback.onEvent(event);
}
}
} catch (ItemNotFoundException e) {
// ignore
}
}
private Set<SitemapEvent> constructSitemapEventsForUpdatedDescr(Item item, List<Widget> widgets) {
Set<SitemapEvent> events = new HashSet<>();
for (Widget w : widgets) {
if (w instanceof Frame) {
events.addAll(constructSitemapEventsForUpdatedDescr(item, itemUIRegistry.getChildren((Frame) w)));
}
boolean itemBelongsToWidget = w.getItem() != null && w.getItem().equals(item.getName());
if (itemBelongsToWidget) {
SitemapWidgetEvent event = constructSitemapEventForWidget(item, item.getState(), w);
event.descriptionChanged = true;
events.add(event);
}
}
return events;
}
}