2 * Copyright (c) 2010-2023 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.hdpowerview.internal.handler;
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*;
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;
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;
56 * Thing handler for shades in an HD PowerView Generation 3 Gateway.
58 * @author Andrew Fiddian-Green - Initial contribution
61 public class ShadeThingHandler extends BaseThingHandler {
63 private static final String INVALID_CHANNEL = "invalid channel";
64 private static final String INVALID_COMMAND = "invalid command";
66 private static final String COMMAND_IDENTIFY = "IDENTIFY";
68 private static final ShadeCapabilitiesDatabase DB = new ShadeCapabilitiesDatabase();
70 private final Logger logger = LoggerFactory.getLogger(ShadeThingHandler.class);
73 private boolean isInitialized;
74 private @Nullable Capabilities capabilities;
76 public ShadeThingHandler(Thing thing) {
81 * Getter for the hub handler.
83 * @return the hub handler.
84 * @throws IllegalStateException if the bridge or its handler are not initialized.
86 private GatewayBridgeHandler getBridgeHandler() throws IllegalStateException {
87 Bridge bridge = this.getBridge();
89 throw new IllegalStateException("Bridge not initialised.");
91 BridgeHandler handler = bridge.getHandler();
92 if (!(handler instanceof GatewayBridgeHandler)) {
93 throw new IllegalStateException("Bridge handler not initialised.");
95 return (GatewayBridgeHandler) handler;
98 public int getShadeId() {
102 private Type getType(Shade shade) {
103 Integer type = shade.getType();
104 return type != null ? DB.getType(type) : new ShadeCapabilitiesDatabase.Type(0);
108 public void handleCommand(ChannelUID channelUID, Command command) {
109 if (RefreshType.REFRESH == command) {
110 getBridgeHandler().handleCommand(channelUID, command);
114 GatewayWebTargets webTargets = getBridgeHandler().getWebTargets();
115 ShadePosition position = new ShadePosition();
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));
123 } else if (command instanceof UpDownType) {
124 position.setPosition(PRIMARY_POSITION,
125 (UpDownType.UP == command) && !Objects.requireNonNull(capabilities).isPrimaryInverted()
126 ? PercentType.HUNDRED
128 webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
130 } else if (StopMoveType.STOP == command) {
131 webTargets.stopShade(shadeId);
134 throw new IllegalArgumentException(INVALID_COMMAND);
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));
141 } else if (command instanceof UpDownType) {
142 position.setPosition(SECONDARY_POSITION,
143 (UpDownType.UP == command)
144 && !Objects.requireNonNull(capabilities).supportsSecondaryOverlapped()
146 : PercentType.HUNDRED);
147 webTargets.moveShade(shadeId, new Shade().setShadePosition(position));
149 } else if (StopMoveType.STOP == command) {
150 webTargets.stopShade(shadeId);
153 throw new IllegalArgumentException(INVALID_COMMAND);
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));
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));
166 throw new IllegalArgumentException(INVALID_COMMAND);
168 case CHANNEL_SHADE_COMMAND:
169 if ((command instanceof StringType stringCommand)
170 && COMMAND_IDENTIFY.equals(stringCommand.toString())) {
171 webTargets.jogShade(shadeId);
174 throw new IllegalArgumentException(INVALID_COMMAND);
177 throw new IllegalArgumentException(INVALID_CHANNEL);
179 } catch (HubProcessingException | IllegalArgumentException e) {
180 logger.warn("handleCommand() shadeId:{}, channelUID:{}, command:{}, exception:{}, message:{}", shadeId,
181 channelUID, command, e.getClass().getSimpleName(), e.getMessage());
185 private boolean hasPrimary() {
186 return Objects.requireNonNull(capabilities).supportsPrimary();
189 private boolean hasSecondary() {
190 Capabilities capabilities = Objects.requireNonNull(this.capabilities);
191 return capabilities.supportsSecondary() || capabilities.supportsSecondaryOverlapped();
194 private boolean hasVane() {
195 Capabilities capabilities = Objects.requireNonNull(this.capabilities);
196 return capabilities.supportsTilt180() || capabilities.supportsTiltAnywhere()
197 || capabilities.supportsTiltOnClosed();
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");
210 isInitialized = false;
211 updateStatus(ThingStatus.UNKNOWN);
215 * Handle shade state change notifications.
217 * @param shade the new shade state.
218 * @return true if we handled the call.
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;
229 updateChannels(shade);
236 * Update the capabilities object based on the data in the passed shade instance.
238 * @param shade containing the channel data.
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;
249 * Update channels based on the data in the passed shade instance.
251 * @param shade containing the channel data.
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());
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.
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.
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,
284 * Remove previously statically created channels if the shade does not support them or they are not relevant.
286 * @param shade containing the channel data.
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,
302 updateThing(editThing().withoutChannels(removeChannels).build());
307 * Update thing properties based on the data in the passed shade instance.
309 * @param shade containing the property data.
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])));
324 * Override base method to only update the channel if it actually exists.
326 * @param channelID id of the channel, which was updated
327 * @param state new state
330 protected void updateState(String channelID, State state) {
331 if (thing.getChannel(channelID) != null) {
332 super.updateState(channelID, state);