[gardena] Improve and fix UoM support (#15523)

* [gardena] Improve and fix UoM support

Properly convert incoming UoM values for command durations, and output
measurements as UoM values where possible.

* [gardena] Fix signal strength channel value

Previously the binding sent 0..100, but the system expects 0..4 for the
system.signal-strength channel.

* [gardena] Update README
* [gardena] Use actual units in state description where appropriate

Signed-off-by: Danny Baumann <dannybaumann@web.de>
This commit is contained in:
maniac103 2023-09-02 23:50:58 +02:00 committed by GitHub
parent 38d45ca017
commit 8949d0d7b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 108 deletions

View File

@ -88,10 +88,10 @@ Sensor refresh commands are not yet supported by the Gardena smart system integr
```java
// smart Water Control
String WC_Valve_Activity "Valve Activity" { channel="gardena:water_control:home:myWateringComputer:valve#activity" }
Number WC_Valve_Duration "Last Watering Duration [%d min]" { channel="gardena:water_control:home:myWateringComputer:valve#duration" }
Number:Time WC_Valve_Duration "Last Watering Duration [%d min]" { channel="gardena:water_control:home:myWateringComputer:valve#duration" }
Number WC_Valve_cmd_Duration "Command Duration [%d min]" { channel="gardena:water_control:home:myWateringComputer:valve_commands#commandDuration" }
Switch WC_Valve_cmd_OpenWithDuration "Watering Timer [%d min]" { channel="gardena:water_control:home:myWateringComputer:valve_commands#start_seconds_to_override" }
Number:Time WC_Valve_cmd_Duration "Command Duration [%d min]" { channel="gardena:water_control:home:myWateringComputer:valve_commands#commandDuration" }
Switch WC_Valve_cmd_OpenWithDuration "Start Watering Timer" { channel="gardena:water_control:home:myWateringComputer:valve_commands#start_seconds_to_override" }
Switch WC_Valve_cmd_CloseValve "Stop Switch" { channel="gardena:water_control:home:myWateringComputer:valve_commands#stop_until_next_task" }
openhab:status WC_Valve_Duration // returns the duration of the last watering request if still active, or 0
@ -101,7 +101,7 @@ openhab:status WC_Valve_Activity // returns the current valve activity (CLOSED|
All channels are read-only, except the command group and the lastUpdate timestamp
```shell
openhab:send WC_Valve_cmd_Duration.sendCommand(10) // set the duration for the command to 10min
openhab:send WC_Valve_cmd_Duration.sendCommand(600) // set the duration for the command to 10min
openhab:send WC_Valve_cmd_OpenWithDuration.sendCommand(ON) // start watering
openhab:send WC_Valve_cmd_CloseValve.sendCommand(ON) // stop any active watering
```

View File

@ -16,10 +16,13 @@ import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.gardena.internal.GardenaSmart;
@ -47,6 +50,7 @@ import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@ -72,6 +76,7 @@ public class GardenaThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(GardenaThingHandler.class);
private TimeZoneProvider timeZoneProvider;
private @Nullable ScheduledFuture<?> commandResetFuture;
private Map<String, Integer> commandDurations = new HashMap<>();
public GardenaThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing);
@ -132,9 +137,13 @@ public class GardenaThingHandler extends BaseThingHandler {
*/
protected void updateChannel(ChannelUID channelUID) throws GardenaException, AccountHandlerNotAvailableException {
String groupId = channelUID.getGroupId();
if (groupId != null) {
boolean isCommand = groupId.endsWith("_commands");
if (!isCommand || (isCommand && isLocalDurationCommand(channelUID))) {
if (groupId == null) {
return;
}
if (isLocalDurationCommand(channelUID)) {
int commandDuration = getCommandDurationSeconds(getDeviceDataItemProperty(channelUID));
updateState(channelUID, new QuantityType<>(commandDuration, Units.SECOND));
} else if (!groupId.endsWith("_commands")) {
Device device = getDevice();
State state = convertToState(device, channelUID);
if (state != null) {
@ -142,40 +151,37 @@ public class GardenaThingHandler extends BaseThingHandler {
}
}
}
}
/**
* Converts a Gardena property value to an openHAB state.
*/
private @Nullable State convertToState(Device device, ChannelUID channelUID) throws GardenaException {
if (isLocalDurationCommand(channelUID)) {
String dataItemProperty = getDeviceDataItemProperty(channelUID);
return new DecimalType(Math.round(device.getLocalService(dataItemProperty).commandDuration / 60.0));
}
String propertyPath = channelUID.getGroupId() + ".attributes.";
String propertyName = channelUID.getIdWithoutGroup();
String unitPropertyPath = propertyPath;
if (propertyName.endsWith("_timestamp")) {
propertyPath += propertyName.replace("_", ".");
} else {
propertyPath += propertyName + ".value";
unitPropertyPath += propertyName + "Unit";
}
String acceptedItemType = null;
try {
Channel channel = getThing().getChannel(channelUID.getId());
if (channel != null) {
acceptedItemType = StringUtils.substringBefore(channel.getAcceptedItemType(), ":");
String acceptedItemType = channel != null ? channel.getAcceptedItemType() : null;
String baseItemType = StringUtils.substringBefore(acceptedItemType, ":");
if (acceptedItemType != null) {
boolean isNullPropertyValue = PropertyUtils.isNull(device, propertyPath);
boolean isDurationProperty = "duration".equals(propertyName);
if (isNullPropertyValue && !isDurationProperty) {
if (isNullPropertyValue) {
return UnDefType.NULL;
}
switch (acceptedItemType) {
if (baseItemType == null || acceptedItemType == null) {
return null;
}
try {
switch (baseItemType) {
case "String":
return new StringType(PropertyUtils.getPropertyValue(device, propertyPath, String.class));
case "Number":
@ -183,24 +189,32 @@ public class GardenaThingHandler extends BaseThingHandler {
return new DecimalType(0);
} else {
Number value = PropertyUtils.getPropertyValue(device, propertyPath, Number.class);
// convert duration from seconds to minutes
if (value != null) {
if (isDurationProperty) {
value = Math.round(value.longValue() / 60.0);
}
return new DecimalType(value.longValue());
}
Unit<?> unit = PropertyUtils.getPropertyValue(device, unitPropertyPath, Unit.class);
if (value == null) {
return UnDefType.NULL;
} else {
if ("rfLinkLevel".equals(propertyName)) {
// Gardena gives us link level as 0..100%, while the system.signal-strength
// channel type wants a 0..4 enum
int percent = value.intValue();
value = percent == 100 ? 4 : percent / 20;
unit = null;
}
if (acceptedItemType.equals(baseItemType) || unit == null) {
// No UoM or no unit found
return new DecimalType(value);
} else {
return new QuantityType<>(value, unit);
}
}
}
case "DateTime":
Date date = PropertyUtils.getPropertyValue(device, propertyPath, Date.class);
if (date != null) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(),
timeZoneProvider.getTimeZone());
return new DateTimeType(zdt);
}
if (date == null) {
return UnDefType.NULL;
}
} else {
ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(), timeZoneProvider.getTimeZone());
return new DateTimeType(zdt);
}
}
} catch (GardenaException e) {
@ -223,8 +237,15 @@ public class GardenaThingHandler extends BaseThingHandler {
logger.debug("Refreshing Gardena connection");
getGardenaSmart().restartWebsockets();
} else if (isLocalDurationCommand(channelUID)) {
QuantityType<?> quantityType = (QuantityType<?>) command;
getDevice().getLocalService(dataItemProperty).commandDuration = quantityType.intValue() * 60;
QuantityType<?> commandInSeconds = null;
if (command instanceof QuantityType<?> timeCommand) {
commandInSeconds = timeCommand.toUnit(Units.SECOND);
}
if (commandInSeconds != null) {
commandDurations.put(dataItemProperty, commandInSeconds.intValue());
} else {
logger.info("Invalid command '{}' for command duration channel, ignoring.", command);
}
} else if (isOnCommand) {
GardenaCommand gardenaCommand = getGardenaCommand(dataItemProperty, channelUID);
logger.debug("Received Gardena command: {}, {}", gardenaCommand.getClass().getSimpleName(),
@ -261,17 +282,15 @@ public class GardenaThingHandler extends BaseThingHandler {
String commandName = channelUID.getIdWithoutGroup().toUpperCase();
String groupId = channelUID.getGroupId();
if (groupId != null) {
int commandDuration = getCommandDurationSeconds(dataItemProperty);
if ("valveSet_commands".equals(groupId)) {
return new ValveSetCommand(ValveSetControl.valueOf(commandName));
} else if (groupId.startsWith("valve") && groupId.endsWith("_commands")) {
return new ValveCommand(ValveControl.valueOf(commandName),
getDevice().getLocalService(dataItemProperty).commandDuration);
return new ValveCommand(ValveControl.valueOf(commandName), commandDuration);
} else if ("mower_commands".equals(groupId)) {
return new MowerCommand(MowerControl.valueOf(commandName),
getDevice().getLocalService(dataItemProperty).commandDuration);
return new MowerCommand(MowerControl.valueOf(commandName), commandDuration);
} else if ("powerSocket_commands".equals(groupId)) {
return new PowerSocketCommand(PowerSocketControl.valueOf(commandName),
getDevice().getLocalService(dataItemProperty).commandDuration);
return new PowerSocketCommand(PowerSocketControl.valueOf(commandName), commandDuration);
}
}
throw new GardenaException("Command " + channelUID.getId() + " not found or groupId null");
@ -308,6 +327,11 @@ public class GardenaThingHandler extends BaseThingHandler {
throw new GardenaException("Can't extract dataItemProperty from channel group " + channelUID.getGroupId());
}
private int getCommandDurationSeconds(String dataItemProperty) {
Integer duration = commandDurations.get(dataItemProperty);
return duration != null ? duration : 3600;
}
/**
* Returns true, if the channel is the duration command.
*/

View File

@ -15,8 +15,6 @@ package org.openhab.binding.gardena.internal.model.dto;
import static org.openhab.binding.gardena.internal.GardenaBindingConstants.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.gardena.internal.exception.GardenaException;
import org.openhab.binding.gardena.internal.model.dto.api.CommonService;
@ -60,25 +58,10 @@ public class Device {
public ValveServiceDataItem valveSix;
public ValveSetServiceDataItem valveSet;
private Map<String, LocalService> localServices = new HashMap<>();
public Device(String id) {
this.id = id;
}
/**
* Returns the local service or creates one if it does not exist.
*/
public LocalService getLocalService(String key) {
LocalService localService = localServices.get(key);
if (localService == null) {
localService = new LocalService();
localServices.put(key, localService);
localService.commandDuration = 3600;
}
return localService;
}
/**
* Evaluates the device type.
*/

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) 2010-2023 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.binding.gardena.internal.model.dto;
/**
* A local service exists only in openHAB and the state is not saved on restarts.
*
* @author Gerhard Riegler - Initial contribution
*/
public class LocalService {
public Integer commandDuration;
}

View File

@ -12,6 +12,12 @@
*/
package org.openhab.binding.gardena.internal.model.dto.api;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.library.unit.Units;
/**
* Represents a Gardena object that is sent via the Gardena API.
*
@ -20,8 +26,10 @@ package org.openhab.binding.gardena.internal.model.dto.api;
public class CommonService {
public UserDefinedNameWrapper name;
public TimestampedIntegerValue batteryLevel;
public @NonNull Unit<@NonNull Dimensionless> batteryLevelUnit = Units.PERCENT;
public TimestampedStringValue batteryState;
public TimestampedIntegerValue rfLinkLevel;
public @NonNull Unit<@NonNull Dimensionless> rfLinkLevelUnit = Units.PERCENT;
public StringValue serial;
public StringValue modelType;
public TimestampedStringValue rfLinkState;

View File

@ -12,6 +12,12 @@
*/
package org.openhab.binding.gardena.internal.model.dto.api;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.library.unit.Units;
/**
* Represents a Gardena object that is sent via the Gardena API.
*
@ -23,4 +29,5 @@ public class MowerService {
public TimestampedStringValue activity;
public TimestampedStringValue lastErrorCode;
public IntegerValue operatingHours;
public @NonNull Unit<@NonNull Time> operatingHoursUnit = Units.HOUR;
}

View File

@ -12,6 +12,12 @@
*/
package org.openhab.binding.gardena.internal.model.dto.api;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.library.unit.Units;
/**
* Represents a Gardena object that is sent via the Gardena API.
*
@ -23,4 +29,5 @@ public class PowerSocketService {
public TimestampedStringValue state;
public TimestampedStringValue lastErrorCode;
public TimestampedIntegerValue duration;
public @NonNull Unit<@NonNull Time> durationUnit = Units.SECOND;
}

View File

@ -12,6 +12,15 @@
*/
package org.openhab.binding.gardena.internal.model.dto.api;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Illuminance;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
/**
* Represents a Gardena object that is sent via the Gardena API.
*
@ -19,7 +28,11 @@ package org.openhab.binding.gardena.internal.model.dto.api;
*/
public class SensorService {
public TimestampedIntegerValue soilHumidity;
public @NonNull Unit<@NonNull Dimensionless> soilHumidityUnit = Units.PERCENT;
public TimestampedIntegerValue soilTemperature;
public @NonNull Unit<@NonNull Temperature> soilTemperatureUnit = SIUnits.CELSIUS;
public TimestampedIntegerValue ambientTemperature;
public @NonNull Unit<@NonNull Temperature> ambientTemperatureUnit = SIUnits.CELSIUS;
public TimestampedIntegerValue lightIntensity;
public @NonNull Unit<@NonNull Illuminance> lightIntensityUnit = Units.LUX;
}

View File

@ -12,6 +12,12 @@
*/
package org.openhab.binding.gardena.internal.model.dto.api;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.library.unit.Units;
/**
* Represents a Gardena object that is sent via the Gardena API.
*
@ -23,4 +29,5 @@ public class ValveService {
public TimestampedStringValue state;
public TimestampedStringValue lastErrorCode;
public TimestampedIntegerValue duration;
public @NonNull Unit<@NonNull Time> durationUnit = Units.SECOND;
}

View File

@ -592,22 +592,22 @@
<channel-type id="duration">
<item-type>Number:Time</item-type>
<label>Duration</label>
<description>Duration in minutes</description>
<description>Duration</description>
<state readOnly="true" pattern="%d min"/>
</channel-type>
<channel-type id="soilHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Soil Humidity</label>
<description>Soil humidity in percent</description>
<state readOnly="true" pattern="%d %unit%"/>
<description>Soil humidity</description>
<state readOnly="true" pattern="%d %%"/>
</channel-type>
<channel-type id="lightIntensity">
<item-type>Number:Illuminance</item-type>
<label>Light Intensity</label>
<description>Light intensity in Lux</description>
<state readOnly="true" pattern="%d lux"/>
<description>Light intensity</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="temperature">
@ -621,7 +621,7 @@
<item-type>Number:Time</item-type>
<label>Operating Hours</label>
<description>The operating hours</description>
<state readOnly="true" pattern="%d %unit%"/>
<state readOnly="true" pattern="%d h"/>
</channel-type>
<channel-type id="batteryState">