From aa7a09265ecb4afd2498bf99cf60d9b678b1a374 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sun, 5 Jun 2022 12:05:08 -0700 Subject: [PATCH] Add PI Heating/Cooling Demand Channels. This adds the "PI Heating Demand" and "PI Cooling Demand" standard thermostat channels. These channels are represented as a percentage (Dimensionless unit), and indicate whether there is a current heating or cooling call from the thermostat. They are fully supported by auto-discovery for the generic device type. Signed-off-by: Lucas Christian --- org.openhab.binding.zigbee/README.md | 2 + .../zigbee/ZigBeeBindingConstants.java | 10 + .../converter/ZigBeeBaseChannelConverter.java | 11 + ...BeeConverterThermostatPiCoolingDemand.java | 190 ++++++++++++++++++ ...BeeConverterThermostatPiHeatingDemand.java | 190 ++++++++++++++++++ ...ZigBeeDefaultChannelConverterProvider.java | 4 + .../main/resources/OH-INF/thing/channels.xml | 18 ++ 7 files changed, 425 insertions(+) create mode 100644 org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiCoolingDemand.java create mode 100644 org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiHeatingDemand.java diff --git a/org.openhab.binding.zigbee/README.md b/org.openhab.binding.zigbee/README.md index f22334b4d..7e6d433bf 100644 --- a/org.openhab.binding.zigbee/README.md +++ b/org.openhab.binding.zigbee/README.md @@ -408,6 +408,8 @@ The following channels are supported -: | thermostat_systemmode | `THERMOSTAT` (0x0201) | Number | | | thermostat_unoccupiedcooling | `THERMOSTAT` (0x0201) | Number | | | thermostat_unoccupiedheating | `THERMOSTAT` (0x0201) | Number | | +| thermostat_heatingdemand | `THERMOSTAT` (0x0201) | Number:Dimensionless | | +| thermostat_coolingdemand | `THERMOSTAT` (0x0201) | Number:Dimensionless | | | warning_device | `IAS_WD` (0x0502) | String | | | windowcovering_lift | `WINDOW_COVERING` (0x0102) | Rollershutter | | diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java index 68e442123..7e787c5d6 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/ZigBeeBindingConstants.java @@ -202,6 +202,16 @@ public class ZigBeeBindingConstants { public static final ChannelTypeUID CHANNEL_THERMOSTAT_RUNNINGMODE = new ChannelTypeUID( "zigbee:thermostat_runningmode"); + public static final String CHANNEL_NAME_THERMOSTAT_HEATING_DEMAND = "thermostatheatingdemand"; + public static final String CHANNEL_LABEL_THERMOSTAT_HEATING_DEMAND = "Heating Demand"; + public static final ChannelTypeUID CHANNEL_THERMOSTAT_HEATING_DEMAND = new ChannelTypeUID( + "zigbee:thermostat_heatingdemand"); + + public static final String CHANNEL_NAME_THERMOSTAT_COOLING_DEMAND = "thermostatcoolingdemand"; + public static final String CHANNEL_LABEL_THERMOSTAT_COOLING_DEMAND = "Cooling Demand"; + public static final ChannelTypeUID CHANNEL_THERMOSTAT_COOLING_DEMAND = new ChannelTypeUID( + "zigbee:thermostat_coolingdemand"); + public static final String CHANNEL_NAME_DOORLOCK_STATE = "doorlockstate"; public static final String CHANNEL_LABEL_DOORLOCK_STATE = "Door Lock State"; public static final ChannelTypeUID CHANNEL_DOORLOCK_STATE = new ChannelTypeUID("zigbee:door_state"); diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/converter/ZigBeeBaseChannelConverter.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/converter/ZigBeeBaseChannelConverter.java index 2e8a86a0f..954481f58 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/converter/ZigBeeBaseChannelConverter.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/converter/ZigBeeBaseChannelConverter.java @@ -32,6 +32,7 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingUID; @@ -427,6 +428,16 @@ protected QuantityType valueToTemperature(int value) { return new QuantityType<>(BigDecimal.valueOf(value, 2), SIUnits.CELSIUS); } + /** + * Converts an 0-100 numeric value into a Percentage {@link QuantityType}. + * + * @param value the integer value to convert + * @return the {@link QuantityType} + */ + protected QuantityType valueToPercentDimensionless(Number value) { + return new QuantityType<>(value, Units.PERCENT); + } + /** * Gets a {@link String} of the device type for the {@link ZigBeeEndpoint} to be used in device labels. * diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiCoolingDemand.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiCoolingDemand.java new file mode 100644 index 000000000..e4c710554 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiCoolingDemand.java @@ -0,0 +1,190 @@ +/** + * 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.binding.zigbee.internal.converter; + +import com.zsmartsystems.zigbee.CommandResult; +import com.zsmartsystems.zigbee.ZigBeeEndpoint; +import com.zsmartsystems.zigbee.zcl.ZclAttribute; +import com.zsmartsystems.zigbee.zcl.ZclAttributeListener; +import com.zsmartsystems.zigbee.zcl.clusters.ZclThermostatCluster; +import com.zsmartsystems.zigbee.zcl.protocol.ZclClusterType; +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.zigbee.ZigBeeBindingConstants; +import org.openhab.binding.zigbee.converter.ZigBeeBaseChannelConverter; +import org.openhab.binding.zigbee.handler.ZigBeeThingHandler; +import org.openhab.binding.zigbee.internal.converter.config.ZclReportingConfig; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +/** + * Converter for the thermostat PI cooling demand channel. The PI Cooling Demand attribute specifies the level + * of cooling currently demanded by the thermostat. + * + * @author Lucas Christian + * + */ +public class ZigBeeConverterThermostatPiCoolingDemand extends ZigBeeBaseChannelConverter implements ZclAttributeListener { + private Logger logger = LoggerFactory.getLogger(ZigBeeConverterThermostatPiCoolingDemand.class); + + private static BigDecimal CHANGE_DEFAULT = new BigDecimal(1); + private static BigDecimal CHANGE_MIN = new BigDecimal(1); + private static BigDecimal CHANGE_MAX = new BigDecimal(100); + + private ZclThermostatCluster cluster; + private ZclAttribute attribute; + private ZclReportingConfig configReporting; + + @Override + public Set getImplementedClientClusters() { + return Collections.singleton(ZclThermostatCluster.CLUSTER_ID); + } + + @Override + public Set getImplementedServerClusters() { + return Collections.emptySet(); + } + + @Override + public boolean initializeDevice() { + ZclThermostatCluster serverCluster = (ZclThermostatCluster) endpoint + .getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (serverCluster == null) { + logger.error("{}: Error opening device thermostat cluster", endpoint.getIeeeAddress()); + return false; + } + + ZclReportingConfig reporting = new ZclReportingConfig(channel); + + try { + CommandResult bindResponse = bind(serverCluster).get(); + if (bindResponse.isSuccess()) { + // Configure reporting + ZclAttribute attribute = serverCluster.getAttribute(ZclThermostatCluster.ATTR_PICOOLINGDEMAND); + + CommandResult reportingResponse = attribute.setReporting(reporting.getReportingTimeMin(), + reporting.getReportingTimeMax(), reporting.getReportingChange()).get(); + handleReportingResponse(reportingResponse, POLLING_PERIOD_DEFAULT, reporting.getPollingPeriod()); + } else { + logger.debug("{}: Failed to bind thermostat cluster", endpoint.getIeeeAddress()); + } + } catch (InterruptedException | ExecutionException e) { + logger.error("{}: Exception setting reporting ", endpoint.getIeeeAddress(), e); + } + + return true; + } + + @Override + public boolean initializeConverter(ZigBeeThingHandler thing) { + super.initializeConverter(thing); + cluster = (ZclThermostatCluster) endpoint.getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (cluster == null) { + logger.error("{}: Error opening device thermostat cluster", endpoint.getIeeeAddress()); + return false; + } + + attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PICOOLINGDEMAND); + if (attribute == null) { + logger.error("{}: Error opening device thermostat PI cooling demand attribute", endpoint.getIeeeAddress()); + return false; + } + + // Add reporting configuration + configReporting = new ZclReportingConfig(channel); + configReporting.setAnalogue(CHANGE_DEFAULT, CHANGE_MIN, CHANGE_MAX); + configOptions = new ArrayList<>(); + configOptions.addAll(configReporting.getConfiguration()); + + // Add a listener, then request the status + cluster.addAttributeListener(this); + return true; + } + + @Override + public void disposeConverter() { + cluster.removeAttributeListener(this); + } + + @Override + public int getPollingPeriod() { + return configReporting.getPollingPeriod(); + } + + @Override + public void handleRefresh() { + attribute.readValue(0); + } + + @Override + public void updateConfiguration(@NonNull final Configuration currentConfiguration, + final Map updatedParameters) { + + if (configReporting.updateConfiguration(currentConfiguration, updatedParameters)) { + try { + final ZclAttribute attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PICOOLINGDEMAND); + + final CommandResult reportingResponse = attribute.setReporting(configReporting.getReportingTimeMin(), + configReporting.getReportingTimeMax(), configReporting.getReportingChange()).get(); + handleReportingResponse(reportingResponse, configReporting.getPollingPeriod(), + configReporting.getReportingTimeMax()); + } catch (InterruptedException | ExecutionException e) { + logger.debug("{}: Thermostat PI cooling demand exception setting reporting", endpoint.getIeeeAddress(), e); + } + } + } + + @Override + public Channel getChannel(ThingUID thingUID, ZigBeeEndpoint endpoint) { + ZclThermostatCluster cluster = (ZclThermostatCluster) endpoint.getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (cluster == null) { + logger.trace("{}: Thermostat cluster not found", endpoint.getIeeeAddress()); + return null; + } + + // Try to read the setpoint attribute + ZclAttribute attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PICOOLINGDEMAND); + Object value = attribute.readValue(Long.MAX_VALUE); + if (value == null) { + logger.trace("{}: Thermostat PI cooling demand returned null", endpoint.getIeeeAddress()); + return null; + } + + return ChannelBuilder + .create(createChannelUID(thingUID, endpoint, ZigBeeBindingConstants.CHANNEL_NAME_THERMOSTAT_COOLING_DEMAND), + ZigBeeBindingConstants.ITEM_TYPE_NUMBER) + .withType(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_COOLING_DEMAND) + .withLabel(ZigBeeBindingConstants.CHANNEL_LABEL_THERMOSTAT_COOLING_DEMAND) + .withProperties(createProperties(endpoint)).build(); + } + + @Override + public void attributeUpdated(ZclAttribute attribute, Object val) { + logger.debug("{}: ZigBee attribute reports {}", endpoint.getIeeeAddress(), attribute); + if (attribute.getClusterType() == ZclClusterType.THERMOSTAT + && attribute.getId() == ZclThermostatCluster.ATTR_PICOOLINGDEMAND) { + Integer value = (Integer) val; + updateChannelState(valueToPercentDimensionless(value)); + } + } +} diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiHeatingDemand.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiHeatingDemand.java new file mode 100644 index 000000000..12d68b531 --- /dev/null +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeConverterThermostatPiHeatingDemand.java @@ -0,0 +1,190 @@ +/** + * 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.binding.zigbee.internal.converter; + +import com.zsmartsystems.zigbee.CommandResult; +import com.zsmartsystems.zigbee.ZigBeeEndpoint; +import com.zsmartsystems.zigbee.zcl.ZclAttribute; +import com.zsmartsystems.zigbee.zcl.ZclAttributeListener; +import com.zsmartsystems.zigbee.zcl.clusters.ZclThermostatCluster; +import com.zsmartsystems.zigbee.zcl.protocol.ZclClusterType; +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.zigbee.ZigBeeBindingConstants; +import org.openhab.binding.zigbee.converter.ZigBeeBaseChannelConverter; +import org.openhab.binding.zigbee.handler.ZigBeeThingHandler; +import org.openhab.binding.zigbee.internal.converter.config.ZclReportingConfig; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +/** + * Converter for the thermostat PI heating demand channel. The PI Heating Demand attribute specifies the level + * of heating currently demanded by the thermostat. + * + * @author Lucas Christian + * + */ +public class ZigBeeConverterThermostatPiHeatingDemand extends ZigBeeBaseChannelConverter implements ZclAttributeListener { + private Logger logger = LoggerFactory.getLogger(ZigBeeConverterThermostatPiHeatingDemand.class); + + private static BigDecimal CHANGE_DEFAULT = new BigDecimal(1); + private static BigDecimal CHANGE_MIN = new BigDecimal(1); + private static BigDecimal CHANGE_MAX = new BigDecimal(100); + + private ZclThermostatCluster cluster; + private ZclAttribute attribute; + private ZclReportingConfig configReporting; + + @Override + public Set getImplementedClientClusters() { + return Collections.singleton(ZclThermostatCluster.CLUSTER_ID); + } + + @Override + public Set getImplementedServerClusters() { + return Collections.emptySet(); + } + + @Override + public boolean initializeDevice() { + ZclThermostatCluster serverCluster = (ZclThermostatCluster) endpoint + .getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (serverCluster == null) { + logger.error("{}: Error opening device thermostat cluster", endpoint.getIeeeAddress()); + return false; + } + + ZclReportingConfig reporting = new ZclReportingConfig(channel); + + try { + CommandResult bindResponse = bind(serverCluster).get(); + if (bindResponse.isSuccess()) { + // Configure reporting + ZclAttribute attribute = serverCluster.getAttribute(ZclThermostatCluster.ATTR_PIHEATINGDEMAND); + + CommandResult reportingResponse = attribute.setReporting(reporting.getReportingTimeMin(), + reporting.getReportingTimeMax(), reporting.getReportingChange()).get(); + handleReportingResponse(reportingResponse, POLLING_PERIOD_DEFAULT, reporting.getPollingPeriod()); + } else { + logger.debug("{}: Failed to bind thermostat cluster", endpoint.getIeeeAddress()); + } + } catch (InterruptedException | ExecutionException e) { + logger.error("{}: Exception setting reporting ", endpoint.getIeeeAddress(), e); + } + + return true; + } + + @Override + public boolean initializeConverter(ZigBeeThingHandler thing) { + super.initializeConverter(thing); + cluster = (ZclThermostatCluster) endpoint.getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (cluster == null) { + logger.error("{}: Error opening device thermostat cluster", endpoint.getIeeeAddress()); + return false; + } + + attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PIHEATINGDEMAND); + if (attribute == null) { + logger.error("{}: Error opening device thermostat PI heating demand attribute", endpoint.getIeeeAddress()); + return false; + } + + // Add reporting configuration + configReporting = new ZclReportingConfig(channel); + configReporting.setAnalogue(CHANGE_DEFAULT, CHANGE_MIN, CHANGE_MAX); + configOptions = new ArrayList<>(); + configOptions.addAll(configReporting.getConfiguration()); + + // Add a listener, then request the status + cluster.addAttributeListener(this); + return true; + } + + @Override + public void disposeConverter() { + cluster.removeAttributeListener(this); + } + + @Override + public int getPollingPeriod() { + return configReporting.getPollingPeriod(); + } + + @Override + public void handleRefresh() { + attribute.readValue(0); + } + + @Override + public void updateConfiguration(@NonNull final Configuration currentConfiguration, + final Map updatedParameters) { + + if (configReporting.updateConfiguration(currentConfiguration, updatedParameters)) { + try { + final ZclAttribute attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PIHEATINGDEMAND); + + final CommandResult reportingResponse = attribute.setReporting(configReporting.getReportingTimeMin(), + configReporting.getReportingTimeMax(), configReporting.getReportingChange()).get(); + handleReportingResponse(reportingResponse, configReporting.getPollingPeriod(), + configReporting.getReportingTimeMax()); + } catch (InterruptedException | ExecutionException e) { + logger.debug("{}: Thermostat PI heating demand exception setting reporting", endpoint.getIeeeAddress(), e); + } + } + } + + @Override + public Channel getChannel(ThingUID thingUID, ZigBeeEndpoint endpoint) { + ZclThermostatCluster cluster = (ZclThermostatCluster) endpoint.getInputCluster(ZclThermostatCluster.CLUSTER_ID); + if (cluster == null) { + logger.trace("{}: Thermostat cluster not found", endpoint.getIeeeAddress()); + return null; + } + + // Try to read the setpoint attribute + ZclAttribute attribute = cluster.getAttribute(ZclThermostatCluster.ATTR_PIHEATINGDEMAND); + Object value = attribute.readValue(Long.MAX_VALUE); + if (value == null) { + logger.trace("{}: Thermostat PI heating demand returned null", endpoint.getIeeeAddress()); + return null; + } + + return ChannelBuilder + .create(createChannelUID(thingUID, endpoint, ZigBeeBindingConstants.CHANNEL_NAME_THERMOSTAT_HEATING_DEMAND), + ZigBeeBindingConstants.ITEM_TYPE_NUMBER) + .withType(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_HEATING_DEMAND) + .withLabel(ZigBeeBindingConstants.CHANNEL_LABEL_THERMOSTAT_HEATING_DEMAND) + .withProperties(createProperties(endpoint)).build(); + } + + @Override + public void attributeUpdated(ZclAttribute attribute, Object val) { + logger.debug("{}: ZigBee attribute reports {}", endpoint.getIeeeAddress(), attribute); + if (attribute.getClusterType() == ZclClusterType.THERMOSTAT + && attribute.getId() == ZclThermostatCluster.ATTR_PIHEATINGDEMAND) { + Integer value = (Integer) val; + updateChannelState(valueToPercentDimensionless(value)); + } + } +} diff --git a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java index db9f2e5ff..cec2db2b4 100644 --- a/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java +++ b/org.openhab.binding.zigbee/src/main/java/org/openhab/binding/zigbee/internal/converter/ZigBeeDefaultChannelConverterProvider.java @@ -83,6 +83,10 @@ public ZigBeeDefaultChannelConverterProvider() { ZigBeeConverterThermostatUnoccupiedHeating.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_RUNNINGMODE, ZigBeeConverterThermostatRunningMode.class); + channelMap.put(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_HEATING_DEMAND, + ZigBeeConverterThermostatPiHeatingDemand.class); + channelMap.put(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_COOLING_DEMAND, + ZigBeeConverterThermostatPiCoolingDemand.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_THERMOSTAT_SYSTEMMODE, ZigBeeConverterThermostatSystemMode.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_FANCONTROL, ZigBeeConverterFanControl.class); channelMap.put(ZigBeeBindingConstants.CHANNEL_WINDOWCOVERING_LIFT, ZigBeeConverterWindowCoveringLift.class); diff --git a/org.openhab.binding.zigbee/src/main/resources/OH-INF/thing/channels.xml b/org.openhab.binding.zigbee/src/main/resources/OH-INF/thing/channels.xml index e453f2858..c7f46b42c 100644 --- a/org.openhab.binding.zigbee/src/main/resources/OH-INF/thing/channels.xml +++ b/org.openhab.binding.zigbee/src/main/resources/OH-INF/thing/channels.xml @@ -380,6 +380,24 @@ + + + Number:Dimensionless + + The level of heating currently demanded by the thermostat + HVAC + + + + + + Number:Dimensionless + + The level of cooling currently demanded by the thermostat + HVAC + + + String