]> git.basschouten.com Git - openhab-addons.git/blob
cca96c223817ccb2511e3d9703841ffc06b8fa8f
[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.hdpowerview.internal.handler;
14
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*;
17
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.StringJoiner;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hdpowerview.internal.GatewayWebTargets;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
29 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
30 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
31 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
32 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Type;
33 import org.openhab.binding.hdpowerview.internal.dto.gen3.Shade;
34 import org.openhab.binding.hdpowerview.internal.dto.gen3.ShadePosition;
35 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StopMoveType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.types.UpDownType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.BridgeHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * Thing handler for shades in an HD PowerView Generation 3 Gateway.
57  *
58  * @author Andrew Fiddian-Green - Initial contribution
59  */
60 @NonNullByDefault
61 public class ShadeThingHandler extends BaseThingHandler {
62
63     private static final String INVALID_CHANNEL = "invalid channel";
64     private static final String INVALID_COMMAND = "invalid command";
65
66     private static final String COMMAND_IDENTIFY = "IDENTIFY";
67
68     private static final ShadeCapabilitiesDatabase DB = new ShadeCapabilitiesDatabase();
69
70     private final Logger logger = LoggerFactory.getLogger(ShadeThingHandler.class);
71
72     private int shadeId;
73     private boolean isInitialized;
74     private @Nullable Capabilities capabilities;
75
76     public ShadeThingHandler(Thing thing) {
77         super(thing);
78     }
79
80     /**
81      * Getter for the hub handler.
82      *
83      * @return the hub handler.
84      * @throws IllegalStateException if the bridge or its handler are not initialized.
85      */
86     private GatewayBridgeHandler getBridgeHandler() throws IllegalStateException {
87         Bridge bridge = this.getBridge();
88         if (bridge == null) {
89             throw new IllegalStateException("Bridge not initialised.");
90         }
91         BridgeHandler handler = bridge.getHandler();
92         if (!(handler instanceof GatewayBridgeHandler)) {
93             throw new IllegalStateException("Bridge handler not initialised.");
94         }
95         return (GatewayBridgeHandler) handler;
96     }
97
98     public int getShadeId() {
99         return shadeId;
100     }
101
102     private Type getType(Shade shade) {
103         Integer type = shade.getType();
104         return type != null ? DB.getType(type) : new ShadeCapabilitiesDatabase.Type(0);
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109         if (RefreshType.REFRESH == command) {
110             getBridgeHandler().handleCommand(channelUID, command);
111             return;
112         }
113
114         GatewayWebTargets webTargets = getBridgeHandler().getWebTargets();
115         ShadePosition position = new ShadePosition();
116         try {
117             switch (channelUID.getId()) {
118                 case CHANNEL_SHADE_POSITION:
119                     if (command instanceof PercentType percentCommand) {
120                         position.setPosition(PRIMARY_POSITION, percentCommand);
121                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
122                         break;
123                     } else if (command instanceof UpDownType) {
124                         position.setPosition(PRIMARY_POSITION,
125                                 (UpDownType.UP == command) && !Objects.requireNonNull(capabilities).isPrimaryInverted()
126                                         ? PercentType.HUNDRED
127                                         : PercentType.ZERO);
128                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
129                         break;
130                     } else if (StopMoveType.STOP == command) {
131                         webTargets.stopShade(shadeId);
132                         break;
133                     }
134                     throw new IllegalArgumentException(INVALID_COMMAND);
135
136                 case CHANNEL_SHADE_SECONDARY_POSITION:
137                     if (command instanceof PercentType percentCommand) {
138                         position.setPosition(SECONDARY_POSITION, percentCommand);
139                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
140                         break;
141                     } else if (command instanceof UpDownType) {
142                         position.setPosition(SECONDARY_POSITION,
143                                 (UpDownType.UP == command)
144                                         && !Objects.requireNonNull(capabilities).supportsSecondaryOverlapped()
145                                                 ? PercentType.ZERO
146                                                 : PercentType.HUNDRED);
147                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
148                         break;
149                     } else if (StopMoveType.STOP == command) {
150                         webTargets.stopShade(shadeId);
151                         break;
152                     }
153                     throw new IllegalArgumentException(INVALID_COMMAND);
154
155                 case CHANNEL_SHADE_VANE:
156                     if (command instanceof PercentType percentCommand) {
157                         position.setPosition(VANE_TILT_POSITION, percentCommand);
158                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
159                         break;
160                     } else if (command instanceof UpDownType) {
161                         position.setPosition(VANE_TILT_POSITION,
162                                 UpDownType.UP == command ? PercentType.HUNDRED : PercentType.ZERO);
163                         webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
164                         break;
165                     }
166                     throw new IllegalArgumentException(INVALID_COMMAND);
167
168                 case CHANNEL_SHADE_COMMAND:
169                     if ((command instanceof StringType stringCommand)
170                             && COMMAND_IDENTIFY.equals(stringCommand.toString())) {
171                         webTargets.jogShade(shadeId);
172                         break;
173                     }
174                     throw new IllegalArgumentException(INVALID_COMMAND);
175
176                 default:
177                     throw new IllegalArgumentException(INVALID_CHANNEL);
178             }
179         } catch (HubProcessingException | IllegalArgumentException e) {
180             logger.warn("handleCommand() shadeId:{}, channelUID:{}, command:{}, exception:{}, message:{}", shadeId,
181                     channelUID, command, e.getClass().getSimpleName(), e.getMessage());
182         }
183     }
184
185     private boolean hasPrimary() {
186         return Objects.requireNonNull(capabilities).supportsPrimary();
187     }
188
189     private boolean hasSecondary() {
190         Capabilities capabilities = Objects.requireNonNull(this.capabilities);
191         return capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped();
192     }
193
194     private boolean hasVane() {
195         Capabilities capabilities = Objects.requireNonNull(this.capabilities);
196         return capabilities.supportsTilt180() || capabilities.supportsTiltAnywhere()
197                 || capabilities.supportsTiltOnClosed();
198     }
199
200     @Override
201     public void initialize() {
202         shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
203         Bridge bridge = getBridge();
204         BridgeHandler bridgeHandler = bridge != null ? bridge.getHandler() : null;
205         if (!(bridgeHandler instanceof GatewayBridgeHandler)) {
206             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
207                     "@text/offline.conf-error.invalid-bridge-handler");
208             return;
209         }
210         isInitialized = false;
211         updateStatus(ThingStatus.UNKNOWN);
212     }
213
214     /**
215      * Handle shade state change notifications.
216      *
217      * @param shade the new shade state.
218      * @return true if we handled the call.
219      */
220     public boolean notify(Shade shade) {
221         if (shadeId == shade.getId()) {
222             updateStatus(ThingStatus.ONLINE);
223             if (!isInitialized && shade.hasFullState()) {
224                 updateCapabilities(shade);
225                 updateProperties(shade);
226                 updateDynamicChannels(shade);
227                 isInitialized = true;
228             }
229             updateChannels(shade);
230             return true;
231         }
232         return false;
233     }
234
235     /**
236      * Update the capabilities object based on the data in the passed shade instance.
237      *
238      * @param shade containing the channel data.
239      */
240     private void updateCapabilities(Shade shade) {
241         Capabilities capabilities = this.capabilities;
242         if (capabilities == null) {
243             capabilities = DB.getCapabilities(shade.getCapabilities());
244             this.capabilities = capabilities;
245         }
246     }
247
248     /**
249      * Update channels based on the data in the passed shade instance.
250      *
251      * @param shade containing the channel data.
252      */
253     private void updateChannels(Shade shade) {
254         updateState(CHANNEL_SHADE_POSITION, hasPrimary() ? shade.getPosition(PRIMARY_POSITION) : UnDefType.UNDEF);
255         updateState(CHANNEL_SHADE_VANE, hasVane() ? shade.getPosition(VANE_TILT_POSITION) : UnDefType.UNDEF);
256         updateState(CHANNEL_SHADE_SECONDARY_POSITION,
257                 hasSecondary() ? shade.getPosition(SECONDARY_POSITION) : UnDefType.UNDEF);
258         if (shade.hasFullState()) {
259             updateState(CHANNEL_SHADE_LOW_BATTERY, shade.getLowBattery());
260             updateState(CHANNEL_SHADE_BATTERY_LEVEL, shade.getBatteryLevel());
261             updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, shade.getSignalStrength());
262         }
263     }
264
265     /**
266      * If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to
267      * be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning.
268      *
269      * @param removeList the list of channels to be removed from the thing.
270      * @param channelId the id of the channel to be (eventually) removed.
271      * @param channelRequired true if the thing requires this channel.
272      */
273     private void updateDynamicChannel(List<Channel> removeList, String channelId, boolean channelRequired) {
274         Channel channel = thing.getChannel(channelId);
275         if (!channelRequired && channel != null) {
276             removeList.add(channel);
277         } else if (channelRequired && channel == null) {
278             logger.warn("updateDynamicChannel() shadeId:{} is missing channel:{} => please recreate the thing", shadeId,
279                     channelId);
280         }
281     }
282
283     /**
284      * Remove previously statically created channels if the shade does not support them or they are not relevant.
285      *
286      * @param shade containing the channel data.
287      */
288     private void updateDynamicChannels(Shade shade) {
289         List<Channel> removeChannels = new ArrayList<>();
290         updateDynamicChannel(removeChannels, CHANNEL_SHADE_POSITION, hasPrimary());
291         updateDynamicChannel(removeChannels, CHANNEL_SHADE_SECONDARY_POSITION, hasSecondary());
292         updateDynamicChannel(removeChannels, CHANNEL_SHADE_VANE, hasVane());
293         updateDynamicChannel(removeChannels, CHANNEL_SHADE_BATTERY_LEVEL, !shade.isMainsPowered());
294         updateDynamicChannel(removeChannels, CHANNEL_SHADE_LOW_BATTERY, !shade.isMainsPowered());
295         if (!removeChannels.isEmpty()) {
296             if (logger.isDebugEnabled()) {
297                 StringJoiner joiner = new StringJoiner(", ");
298                 removeChannels.forEach(c -> joiner.add(c.getUID().getId()));
299                 logger.debug("updateDynamicChannels() shadeId:{}, removing unsupported channels:{}", shadeId,
300                         joiner.toString());
301             }
302             updateThing(editThing().withoutChannels(removeChannels).build());
303         }
304     }
305
306     /**
307      * Update thing properties based on the data in the passed shade instance.
308      *
309      * @param shade containing the property data.
310      */
311     private void updateProperties(Shade shade) {
312         thing.setProperties(Stream.of(new String[][] { //
313                 { HDPowerViewBindingConstants.PROPERTY_NAME, shade.getName() },
314                 { HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE, getType(shade).toString() },
315                 { HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES,
316                         Objects.requireNonNull(capabilities).toString() },
317                 { HDPowerViewBindingConstants.PROPERTY_POWER_TYPE, shade.getPowerType().name().toLowerCase() },
318                 { HDPowerViewBindingConstants.PROPERTY_BLE_NAME, shade.getBleName() },
319                 { Thing.PROPERTY_FIRMWARE_VERSION, shade.getFirmware() } //
320         }).collect(Collectors.toMap(data -> data[0], data -> data[1])));
321     }
322
323     /**
324      * Override base method to only update the channel if it actually exists.
325      *
326      * @param channelID id of the channel, which was updated
327      * @param state new state
328      */
329     @Override
330     protected void updateState(String channelID, State state) {
331         if (thing.getChannel(channelID) != null) {
332             super.updateState(channelID, state);
333         }
334     }
335 }