]> git.basschouten.com Git - openhab-addons.git/blob
6813f611f568b1ac3dff67ae905aceef8aea7e03
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.nikobus.internal.handler;
14
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CHANNEL_OUTPUT_PREFIX;
16
17 import java.util.EnumSet;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.Map;
21 import java.util.Set;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
25 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleCommandFactory;
26 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
27 import org.openhab.core.thing.Channel;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.RefreshType;
34 import org.openhab.core.types.State;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * The {@link NikobusSwitchModuleHandler} is responsible for communication between Nikobus modules and binding.
40  *
41  * @author Boris Krivonog - Initial contribution
42  */
43 @NonNullByDefault
44 abstract class NikobusModuleHandler extends NikobusBaseThingHandler {
45     private final EnumSet<SwitchModuleGroup> pendingRefresh = EnumSet.noneOf(SwitchModuleGroup.class);
46     private final Logger logger = LoggerFactory.getLogger(NikobusModuleHandler.class);
47     private final Map<String, Integer> cachedStates = new HashMap<>();
48
49     protected NikobusModuleHandler(Thing thing) {
50         super(thing);
51     }
52
53     @Override
54     public void initialize() {
55         super.initialize();
56
57         if (thing.getStatus() != ThingStatus.OFFLINE) {
58             // Fetch all linked channels to get initial values.
59             thing.getChannels().forEach(channel -> refreshChannel(channel.getUID()));
60         }
61     }
62
63     @Override
64     public void dispose() {
65         super.dispose();
66
67         synchronized (cachedStates) {
68             cachedStates.clear();
69         }
70
71         synchronized (pendingRefresh) {
72             pendingRefresh.clear();
73         }
74     }
75
76     @Override
77     public void handleCommand(ChannelUID channelUID, Command command) {
78         logger.debug("handleCommand '{}' for channel '{}'", command, channelUID.getId());
79
80         if (command instanceof RefreshType) {
81             refreshChannel(channelUID);
82         } else {
83             processWrite(channelUID, command);
84         }
85     }
86
87     private void refreshChannel(ChannelUID channelUID) {
88         logger.debug("Refreshing channel '{}'", channelUID.getId());
89
90         if (!isLinked(channelUID)) {
91             logger.debug("Refreshing channel '{}' skipped since it is not linked", channelUID.getId());
92             return;
93         }
94
95         updateGroup(SwitchModuleGroup.mapFromChannel(channelUID));
96     }
97
98     public void refreshModule() {
99         Set<SwitchModuleGroup> groups = new HashSet<>();
100         for (Channel channel : thing.getChannels()) {
101             ChannelUID channelUID = channel.getUID();
102             if (isLinked(channelUID)) {
103                 groups.add(SwitchModuleGroup.mapFromChannel(channelUID));
104             }
105         }
106
107         if (groups.isEmpty()) {
108             logger.debug("Nothing to refresh for '{}'", thing.getUID());
109             return;
110         }
111
112         logger.debug("Refreshing {} - {}", thing.getUID(), groups);
113
114         for (SwitchModuleGroup group : groups) {
115             updateGroup(group);
116         }
117     }
118
119     public void requestStatus(SwitchModuleGroup group) {
120         updateGroup(group);
121     }
122
123     private void updateGroup(SwitchModuleGroup group) {
124         synchronized (pendingRefresh) {
125             if (pendingRefresh.contains(group)) {
126                 logger.debug("Refresh already scheduled for group {} of module '{}'", group, getAddress());
127                 return;
128             }
129
130             pendingRefresh.add(group);
131         }
132
133         logger.debug("Refreshing group {} of switch module '{}'", group, getAddress());
134
135         NikobusPcLinkHandler pcLink = getPcLink();
136         if (pcLink != null) {
137             NikobusCommand command = SwitchModuleCommandFactory.createReadCommand(getAddress(), group,
138                     result -> processStatusUpdate(result, group));
139             pcLink.sendCommand(command);
140         }
141     }
142
143     private void processStatusUpdate(NikobusCommand.Result result, SwitchModuleGroup group) {
144         try {
145             String responsePayload = result.get();
146
147             logger.debug("processStatusUpdate '{}' for group {} in module '{}'", responsePayload, group, getAddress());
148
149             if (thing.getStatus() != ThingStatus.ONLINE) {
150                 updateStatus(ThingStatus.ONLINE);
151             }
152
153             // Update channel's statuses based on response.
154             for (int i = 0; i < group.getCount(); i++) {
155                 String channelId = CHANNEL_OUTPUT_PREFIX + (i + group.getOffset());
156                 String responseDigits = responsePayload.substring(9 + (i * 2), 11 + (i * 2));
157
158                 int value = Integer.parseInt(responseDigits, 16);
159
160                 updateStateAndCacheValue(channelId, value);
161             }
162         } catch (Exception e) {
163             logger.warn("Processing response for '{}'-{} failed with {}", getAddress(), group, e.getMessage(), e);
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
165         } finally {
166             synchronized (pendingRefresh) {
167                 pendingRefresh.remove(group);
168             }
169         }
170     }
171
172     private void updateStateAndCacheValue(String channelId, int value) {
173         if (value < 0x00 || value > 0xff) {
174             throw new IllegalArgumentException("Invalid range. 0x00 - 0xff expected but got value " + value);
175         }
176
177         logger.debug("setting channel '{}' to {}", channelId, value);
178
179         Integer previousValue;
180         synchronized (cachedStates) {
181             previousValue = cachedStates.put(channelId, value);
182         }
183
184         if (previousValue == null || previousValue.intValue() != value) {
185             updateState(channelId, stateFromValue(channelId, value));
186         }
187     }
188
189     private void processWrite(ChannelUID channelUID, Command command) {
190         StringBuilder commandPayload = new StringBuilder();
191         SwitchModuleGroup group = SwitchModuleGroup.mapFromChannel(channelUID);
192
193         for (int i = group.getOffset(); i < group.getOffset() + group.getCount(); i++) {
194             String channelId = CHANNEL_OUTPUT_PREFIX + i;
195             Integer digits;
196
197             if (channelId.equals(channelUID.getId())) {
198                 digits = valueFromCommand(channelId, command);
199                 updateStateAndCacheValue(channelId, digits.intValue());
200             } else {
201                 synchronized (cachedStates) {
202                     digits = cachedStates.get(channelId);
203                 }
204             }
205
206             if (digits == null) {
207                 commandPayload.append("00");
208                 logger.warn("no cached value found for '{}' in module '{}'", channelId, getAddress());
209             } else {
210                 commandPayload.append(String.format("%02X", digits.intValue()));
211             }
212         }
213
214         NikobusPcLinkHandler pcLink = getPcLink();
215         if (pcLink != null) {
216             pcLink.sendCommand(SwitchModuleCommandFactory.createWriteCommand(getAddress(), group,
217                     commandPayload.toString(), this::processWriteCommandResponse));
218         }
219     }
220
221     private void processWriteCommandResponse(NikobusCommand.Result result) {
222         try {
223             String responsePayload = result.get();
224
225             logger.debug("processWriteCommandResponse '{}'", responsePayload);
226
227             if (thing.getStatus() != ThingStatus.ONLINE) {
228                 updateStatus(ThingStatus.ONLINE);
229             }
230         } catch (Exception e) {
231             logger.warn("Processing write confirmation failed with {}", e.getMessage());
232             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
233         }
234     }
235
236     protected abstract int valueFromCommand(String channelId, Command command);
237
238     protected abstract State stateFromValue(String channelId, int value);
239 }