Skip to content

Commit

Permalink
Merge branch 'gh292-switch-overhaul' into dev (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
climategadgets committed Nov 2, 2023
2 parents fd35be3 + 8932aef commit 18761a4
Show file tree
Hide file tree
Showing 75 changed files with 2,035 additions and 582 deletions.
2 changes: 1 addition & 1 deletion docs/configuration/mqtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ They all share common properties:
id: <Client ID> # optional, defaults to internally generated stable hash code
host: <MQTT broker host>
port: <MQTT broker port> # optional, defaults to 1883
root-topic: <root topic for all messages for this connector>
root-topic: <root MQTT topic for all messages for this connector>
username: <MQTT broker username> # optional, but recommended
password: <MQTT broker password> # optional, but recommended
auto-reconnect: <boolean flag> #optional, see below
Expand Down
17 changes: 9 additions & 8 deletions docs/configuration/sensors-switches-fans.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ switches:
reversed: <boolean flag>
heartbeat: <Duration>
pace: <Duration>
optimistic: <boolean flag>
availability-topic: <MQTT topic for availability information>
```

#### reversed
Expand All @@ -73,8 +73,11 @@ Optional. Send the command to hardware this often even if the logical state hasn
#### pace
Optional. Send the same command to hardware no more often that this. Some bridges (notably `zigbee2mqtt`) are known to become unresponsive with no error indication when incoming traffic exceeds their bandwidth.

#### optimistic
Optional. Send the command to hardware and don't wait for confirmation. Normally, you wouldn't have to do this, but some firmware (notably, [ESPHome](./esphome.md)) doesn't provide reliable confirmation so this may save the situation (and is a default for known hardware types). Use only if you must, and consider using [heartbeat](#heartbeat) to offset the risk.
#### availability-topic
* Mandatory for [ESPHome](./esphome.md) devices (see [esphome #5030](https://github.com/esphome/issues/issues/5030) for more information);
* Disallowed for [Zigbee](./zigbee2mqtt.md) and [Z-Wave](./zwave2mqtt.md) devices.

Log messages at `ERROR` level will provide enough details to resolve the problem.

### fans
Similar to above:
Expand All @@ -85,11 +88,9 @@ fans:
availability: /esphome/550212/status
heartbeat: <Duration>
pace: <Duration>
availability-topic: <MQTT topic for availability information>
```
`id`, `address`, `heartbeat`, and `pace` parameters are identical to those above.

#### availability
Defines the topic where the device announces its availability.
`id`, `address`, `heartbeat`, `pace`, and `availability-topic` parameters are identical to those above.

This is how ESPHome configuration section looks like (`pin` and `min_power` depend on your particular hardware setup):

Expand All @@ -113,7 +114,7 @@ fan:

For more information, see [ESPHome Fan Component](https://esphome.io/components/fan/).

> **NOTE:** Leave `speed_count` at default (100), or this integration will not work.
> **NOTE:** Leave ESPHome `speed_count` at default (100), or this integration will not work.

### Property of
* [esphome](./esphome.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration/zones.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Best explained by example:
limit: 0.7
mode: cooling
hvac-device: economizer-a6
timeout: 75S

```

Expand Down Expand Up @@ -79,6 +80,7 @@ Cooling mode assumed:
* `controller`: just like the zone configuration above.
* `mode`: self-explanatory
* `hvac-device`: at this point, the economizer is an on/off device (multistage coming). This is the identifier of the [HVAC device](./hvac.md) acting as an economizer.
* `timeout`: treat both indoor and ambient sensors as stale and shut off the economizer after not receiving data from them for this long. Default is 90 seconds. The system will complain at `INFO` level if this is happening.

### Property of
* [home-climate-control](./home-climate-control.md)
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Home Climate Control: Release Notes
## Coming Up

* [#293 Implement variable output single mode HVAC device](https://github.com/home-climate-control/dz/issues/293)
* [#292 Decouple possibly faulty actuators from control logic so that the processing pipeline doesn't get stuck ](https://github.com/home-climate-control/dz/issues/292)
* [#291 Economizer: control a HVAC device, not a switch](https://github.com/home-climate-control/dz/issues/291)

## v4.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ public interface SwitchConfig {
Optional<Duration> heartbeat();
@JsonProperty("pace")
Optional<Duration> pace();
@JsonProperty("optimistic")
Optional<Boolean> optimistic();
@JsonProperty("availability-topic")
Optional<String> availabilityTopic();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import net.sf.dz3r.model.HvacMode;

import java.time.Duration;
import java.util.Optional;

public interface EconomizerConfig {
@JsonProperty("ambient-sensor")
String ambientSensor();
Expand All @@ -18,4 +21,6 @@ public interface EconomizerConfig {
HvacMode mode();
@JsonProperty("hvac-device")
String hvacDevice();
@JsonProperty("timeout")
Optional<Duration> timeout();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
package net.sf.dz3r.runtime.config.quarkus.protocol.mqtt;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.time.Duration;
import java.util.Optional;

public interface FanConfig {
@JsonProperty("id")
Optional<String> id();
@JsonProperty("address")
String address();
@JsonProperty("heartbeat")
Optional<Duration> heartbeat();
@JsonProperty("pace")
Optional<Duration> pace();
@JsonProperty("availability-topic")
Optional<String> availabilityTopic();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
public interface MqttDeviceConfig extends MqttGateway {

@JsonProperty("broker")
@Override
MqttBrokerConfig broker();

@JsonProperty("sensors")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,16 @@ public interface InterfaceRecordMapper {
@Mapping(expression = "java(source.reversed().orElse(false))", target = "reversed")
@Mapping(expression = "java(source.heartbeat().orElse(null))", target = "heartbeat")
@Mapping(expression = "java(source.pace().orElse(null))", target = "pace")
@Mapping(expression = "java(source.optimistic().orElse(null))", target = "optimistic")
@Mapping(expression = "java(source.availabilityTopic().orElse(null))", target = "availabilityTopic")
net.sf.dz3r.runtime.config.hardware.SwitchConfig switchConfig(SwitchConfig source);

@Mapping(expression = "java(source.id().orElse(null))", target = "id")
@Mapping(expression = "java(source.address())", target = "address")
@Mapping(expression = "java(source.heartbeat().orElse(null))", target = "heartbeat")
@Mapping(expression = "java(source.pace().orElse(null))", target = "pace")
@Mapping(expression = "java(source.availabilityTopic().orElse(null))", target = "availabilityTopic")
net.sf.dz3r.runtime.config.protocol.mqtt.FanConfig fanConfig(FanConfig source);

@Mapping(expression = "java(source.serialPort())", target = "serialPort")
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.sensors(source.sensors()))", target = "sensors")
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.switches(source.switches()))", target = "switches")
Expand Down Expand Up @@ -147,6 +154,7 @@ public interface InterfaceRecordMapper {
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.controller(source.controller()))", target = "controller")
@Mapping(expression = "java(source.mode())", target = "mode")
@Mapping(expression = "java(source.hvacDevice())", target = "hvacDevice")
@Mapping(expression = "java(source.timeout().orElse(null))", target = "timeout")
net.sf.dz3r.runtime.config.model.EconomizerConfig economizer(EconomizerConfig source);

@Mapping(expression = "java(source.min())", target = "min")
Expand Down Expand Up @@ -217,6 +225,14 @@ public interface InterfaceRecordMapper {
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.filter(source.filter().orElse(null)))", target = "filter")
net.sf.dz3r.runtime.config.hardware.HeatpumpConfig heatpump(HeatpumpConfig source);

@Mapping(expression = "java(source.id())", target = "id")
@Mapping(expression = "java(source.mode())", target = "mode")
@Mapping(expression = "java(source.actuator())", target = "actuator")
@Mapping(expression = "java(source.maxPower().orElse(null))", target = "maxPower")
@Mapping(expression = "java(source.bandCount().orElse(null))", target = "bandCount")
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.filter(source.filter().orElse(null)))", target = "filter")
net.sf.dz3r.runtime.config.hardware.VariableHvacConfig variable(VariableHvacConfig source);

@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.singleStage(source.singleStage()))", target = "singleStage")
@Mapping(expression = "java(InterfaceRecordMapper.INSTANCE.multiStage(source.multiStage()))", target = "multiStage")
net.sf.dz3r.runtime.config.hardware.UnitControllerConfig unit(UnitControllerConfig source);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ private void reportGitProperties() throws IOException {
logger.debug("git.branch={}", p.get("git.branch"));
logger.debug("git.commit.id={}", p.get("git.commit.id"));
logger.debug("git.commit.id.abbrev={}", p.get("git.commit.id.abbrev"));
logger.debug("git.commit.id.describe={}", p.get("git.commit.id.describe"));
logger.debug("git.build.version={}", p.get("git.build.version"));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.sf.dz3r.runtime.config;

import net.sf.dz3r.device.actuator.CqrsSwitch;
import net.sf.dz3r.device.actuator.HvacDevice;
import net.sf.dz3r.device.actuator.Switch;
import net.sf.dz3r.device.actuator.VariableOutputDevice;
import net.sf.dz3r.device.mqtt.v1.MqttAdapter;
import net.sf.dz3r.model.UnitController;
Expand Down Expand Up @@ -30,7 +30,7 @@ public class ConfigurationContext {

public final EntityProvider<MqttAdapter> mqtt = new EntityProvider<>("mqtt");
public final EntityProvider<Flux<Signal<Double, Void>>> sensors = new EntityProvider<>("sensor");
public final EntityProvider<Switch<?>> switches = new EntityProvider<>("switch");
public final EntityProvider<CqrsSwitch<?>> switches = new EntityProvider<>("switch");
public final EntityProvider<VariableOutputDevice> fans = new EntityProvider<>("fan");
public final EntityProvider<Zone> zones = new EntityProvider<>("zone");
public final EntityProvider<ScheduleUpdater> schedule = new EntityProvider<>("schedule");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.sf.dz3r.runtime.config;

import net.sf.dz3r.device.actuator.CqrsSwitch;
import net.sf.dz3r.device.actuator.HvacDevice;
import net.sf.dz3r.device.actuator.Switch;
import net.sf.dz3r.device.actuator.VariableOutputDevice;
import net.sf.dz3r.model.UnitController;
import net.sf.dz3r.model.Zone;
Expand Down Expand Up @@ -38,7 +38,7 @@ protected final Mono<Flux<Signal<Double, Void>>> getSensor(String address) {
.doOnNext(s -> logger.debug("getSensor({}) = {}", address, s));
}

protected final Switch<?> getSwitch(String address) {
protected final CqrsSwitch<?> getSwitch(String address) {
return context
.switches
.getMonoById("switches", address)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package net.sf.dz3r.runtime.config;

import net.sf.dz3r.device.actuator.CqrsSwitch;
import net.sf.dz3r.device.actuator.NullCqrsSwitch;
import net.sf.dz3r.runtime.config.hardware.MockConfig;
import net.sf.dz3r.runtime.config.hardware.SwitchConfig;
import net.sf.dz3r.device.actuator.NullSwitch;
import net.sf.dz3r.device.actuator.Switch;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand All @@ -17,16 +17,16 @@ protected MockConfigurationParser(ConfigurationContext context) {
super(context);
}

public Mono<List<Switch>> parse(Set<MockConfig> source) {
public Mono<List<CqrsSwitch>> parse(Set<MockConfig> source) {

// Trivial operation, no need to bother with parallelizing
return Flux
.fromIterable(Optional.ofNullable(source).orElse(Set.of()))
.flatMap(c -> Flux.fromIterable(c.switches()))
.map(SwitchConfig::address)
.map(NullSwitch::new)
.map(NullCqrsSwitch::new)
.doOnNext(s -> context.switches.register(s.getAddress(), s))
.map(Switch.class::cast)
.map(CqrsSwitch.class::cast)
.collectList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.commons.lang3.tuple.ImmutablePair;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -41,20 +42,25 @@ public void parse(Set<ZoneConfig> source) {
private Map.Entry<String, Zone> createZone(ZoneConfig cf) {

var ts = createThermostat(cf.name(), cf.settings().setpoint(), cf.settings().setpointRange(), cf.controller());
var eco = createEconomizer(cf.economizer());
var eco = createEconomizer(cf.name(), cf.economizer());
var zone = new Zone(ts, map(cf.settings()), eco);

return new ImmutablePair<>(cf.id(), zone);
}

private EconomizerContext createEconomizer(EconomizerConfig cf) {
private EconomizerContext createEconomizer(String zoneName, EconomizerConfig cf) {

if (cf == null) {
return null;
}

var ambientSensor = HCCObjects.requireNonNull(getSensorBlocking(cf.ambientSensor()), "can't resolve ambient-sensor=" + cf.ambientSensor());
var hvacDevice = HCCObjects.requireNonNull(getHvacDevice(cf.hvacDevice()), "can't resolve hvac-device=" + cf.hvacDevice());
var timeout = Optional.ofNullable(cf.timeout()).orElseGet(() -> {
var t = Duration.ofSeconds(90);
logger.info("{}: using default stale timeout of {} for the economizer", zoneName, t);
return t;
});

return new EconomizerContext(
new EconomizerSettings(
Expand All @@ -66,7 +72,8 @@ private EconomizerContext createEconomizer(EconomizerConfig cf) {
cf.controller().i(),
cf.controller().limit()),
ambientSensor,
hvacDevice);
hvacDevice,
timeout);
}

private Thermostat createThermostat(String name, Double setpoint, RangeConfig rangeConfig, PidControllerConfig cf) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package net.sf.dz3r.runtime.config.mqtt;

import net.sf.dz3r.device.esphome.v1.ESPHomeFan;
import net.sf.dz3r.device.esphome.v1.ESPHomeListener;
import net.sf.dz3r.device.esphome.v1.ESPHomeSwitch;
import net.sf.dz3r.device.esphome.v2.ESPHomeCqrsSwitch;
import net.sf.dz3r.device.esphome.v2.ESPHomeFan;
import net.sf.dz3r.device.mqtt.v1.MqttAdapter;
import net.sf.dz3r.runtime.config.protocol.mqtt.MqttDeviceConfig;
import net.sf.dz3r.runtime.config.protocol.mqtt.MqttEndpointSpec;

import java.time.Clock;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

public class ESPHomeDeviceResolver extends MqttDeviceResolver<MqttDeviceConfig, ESPHomeListener, ESPHomeSwitch, ESPHomeFan> {
public class ESPHomeDeviceResolver extends MqttDeviceResolver<MqttDeviceConfig, ESPHomeListener, ESPHomeCqrsSwitch, ESPHomeFan> {

public ESPHomeDeviceResolver(Set<MqttDeviceConfig> source, Map<MqttEndpointSpec, MqttAdapter> endpoint2adapter) {
super(source, endpoint2adapter);
Expand All @@ -30,22 +30,25 @@ protected ESPHomeListener createSensorListener(MqttAdapter adapter, String rootT
}

@Override
protected ESPHomeSwitch createSwitch(MqttAdapter adapter, String rootTopic, Boolean optimistic) {
protected ESPHomeCqrsSwitch createSwitch(String id, Duration heartbeat, Duration pace, MqttAdapter adapter, String rootTopic, String availabilityTopic) {

// Optimistic defaults to true for this switch only
// https://github.com/home-climate-control/dz/issues/280

return new ESPHomeSwitch(
return new ESPHomeCqrsSwitch(
id,
Clock.systemUTC(),
heartbeat,
pace,
adapter,
rootTopic,
Optional.ofNullable(optimistic).orElse(true),
null);
availabilityTopic);
}

@Override
protected ESPHomeFan createFan(String id, MqttAdapter adapter, String rootTopic, String availabilityTopic) {
protected ESPHomeFan createFan(String id, Duration heartbeat, Duration pace, MqttAdapter adapter, String rootTopic, String availabilityTopic) {
return new ESPHomeFan(
id,
Clock.systemUTC(),
heartbeat,
pace,
adapter,
rootTopic,
availabilityTopic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package net.sf.dz3r.runtime.config.mqtt;

import net.sf.dz3r.device.mqtt.v1.AbstractMqttSwitch;
import net.sf.dz3r.device.mqtt.v1.MqttAdapter;
import net.sf.dz3r.device.mqtt.v1.MqttEndpoint;
import net.sf.dz3r.device.mqtt.v2.AbstractMqttCqrsSwitch;
import net.sf.dz3r.instrumentation.Marker;
import net.sf.dz3r.runtime.config.ConfigurationContext;
import net.sf.dz3r.runtime.config.ConfigurationContextAware;
Expand Down Expand Up @@ -50,7 +50,7 @@ public MqttConfigurationParser(ConfigurationContext context) {
*/
public Mono<Tuple2<
List<Id2Flux>,
List<Map.Entry<String, AbstractMqttSwitch>>>> parse(
List<Map.Entry<String, AbstractMqttCqrsSwitch>>>> parse(
Set<MqttDeviceConfig> esphome,
Set<MqttDeviceConfig> zigbee2mqtt,
Set<MqttDeviceConfig> zwave2mqtt,
Expand Down Expand Up @@ -128,14 +128,14 @@ List<Map.Entry<String, AbstractMqttSwitch>>>> parse(
.publishOn(Schedulers.boundedElastic())
.flatMap(MqttDeviceResolver::getSwitches)
.doOnNext(kv -> context.switches.register(kv.getKey(), kv.getValue()))
.map(kv -> ((Map.Entry<String, AbstractMqttSwitch>) new ImmutablePair<>(kv.getKey(), kv.getValue())))
.map(kv -> ((Map.Entry<String, AbstractMqttCqrsSwitch>) new ImmutablePair<>(kv.getKey(), kv.getValue())))
.collectList();

var fans = mqttConfigs
.publishOn(Schedulers.boundedElastic())
.flatMap(MqttDeviceResolver::getFans)
.doOnNext(kv -> context.fans.register(kv.getKey(), kv.getValue()))
.map(kv -> (new ImmutablePair<>(kv.getKey(), kv.getValue())))
.map(kv -> new ImmutablePair<>(kv.getKey(), kv.getValue()))
.collectList();

logger.debug("waiting at the gate");
Expand Down
Loading

0 comments on commit 18761a4

Please sign in to comment.