Skip to content

Commit

Permalink
Now parsing YAML from Google Calendar event descriptions (#311)
Browse files Browse the repository at this point in the history
This commit contains the only curse word in the whole code base.
  • Loading branch information
climategadgets committed Apr 11, 2024
1 parent a8797ed commit e1b155c
Show file tree
Hide file tree
Showing 12 changed files with 472 additions and 54 deletions.
81 changes: 81 additions & 0 deletions docs/configuration/schedule-syntax-deprecated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Google Calendar Schedule Integration Syntax - deprecated

Context: [home-climate-control.schedule.google-calendar](./schedule.md#google-calendar) integration.

> **NOTE:** this syntax is about to be retired and replaced by [YAML syntax](./schedule-syntax.md).
---

HCC reads the `title` (contains period description), `start`, `end`, `all day`, `recurrence` calendar fields and ignores the rest.

**IMPORTANT:** historically, different calendars are used for heating and cooling, and the heating or cooling mode is set outside of the calendar, so you won't find any mention of it here. This may change in the future.

# General Syntax

## Title field

Contains a period name followed by a colon, and several sections, separated by `,` or `;`:

* on/off
* voting
* setpoint
* dump priority

### On/Off

String literals `on` and `enabled` mean that the zone is ON during this period. This is the default behavior.

String literals `off` and `disabled` mean that the zone is OFF during this period. You have to explicitly specify this.

Setpoint value is ignored for a disabled zone, to make it easier to do one-off edits. However, it is still required.

### Voting

String literal `voting` means that if the zone is calling for heat or cool during this period, it will cause the HVAC unit to turn on. This is the default behavior.

String literal `non-voting` or `not voting` means that the HVAC unit will only turn on if any other **voting** zone calls for heat or cool.

### Setpoint

String literal `setpoint` or `temperature` followed by space, then a numerical value, and optional `C` or `F` unit modifier specifies the setpoint temperature.

There is no default for the setpoint.

Default temperature unit is `C` (degrees Celsius).

## Start and end time, recurrence

**IMPORTANT:** Period can't cross midnight. This is an ancient design limitation, [comment on issue #5](https://github.com/home-climate-control/dz/issues/5) if this bothers you.

**IMPORTANT**: Google Calendar has a bug where recurrent events ending "never", in actuality, expire 2 years after the first event - but only under some circumstances. Don't forget to check your schedule into the future. Workaround: duplicate the event, and adjust the start date.

Other than that, self-explanatory.

# Examples

### Away: setpoint 32C, not voting

Period name is `Away`
Setpoint is at 32°C
This zone will **not** turn on the HVAC if it calls for heat or cool because it is `non-voting`, but its setpoint will be satisfied if any other voting zone does.

### Away: setpoint 32, not voting

Same as the above, but the temperature unit defaults to degrees Celsius.

### Away: setpoint 90F, not voting

Same as the above, but the temperature unit is explicitly specified to be degrees Fahrenheit.

### Away: setpoint 32, off

Same as the above, but the zone will not participate in HVAC exchange, dampers will stay closed no matter what happens to the rest of the house. This zone will appear grayed out on Swing and mobile consoles.

### Night: setpoint 24

Period name is `Night`
Setpoint is at 24°C
Zone **will** turn on the HVAC if it calls for heat or cool, and it will be turned off when all other voting zones that it serves are satisfied.

---
[^^^ Configuration](./index.md)
88 changes: 52 additions & 36 deletions docs/configuration/schedule-syntax.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,92 @@
# Google Calendar Schedule Integration Syntax

Context: [home-climate-control.schedule.google-calendar] integration.
Context: [home-climate-control.schedule.google-calendar](./schedule.md#google-calendar) integration.
See also: [deprecated schedule integration syntax](./schedule-syntax-deprecated.md)

---

HCC reads the `title` (contains period description), `start`, `end`, `all day`, `recurrence` calendar fields and ignores the rest.
HCC reads the `title` (contains period name), `description` (contains period settings), `start`, `end`, `all day`, `recurrence` calendar fields and ignores the rest.

**IMPORTANT:** historically, different calendars are used for heating and cooling, and the heating or cooling mode is set outside of the calendar, so you won't find any mention of it here. This may change in the future.

# General Syntax

Until the [old syntax](./schedule-syntax-deprecated.md) is retired the `description` field will be attempted to be parsed into period settings, if that fails,
then the old syntax will be attempted to be parsed (having issued a `WARN` level message in the log), and only then the scheduler will give up.

## Title field

Contains a period name followed by a colon, and several sections, separated by `,` or `;`:
Contains the period name. No restrictions.

## Description field

Contains the period settings. Best explained by example:

* on/off
* voting
* setpoint
* dump priority
```yaml
# Comments are allowed in the event description text
enabled: true | false # optional, defaults to true
voting: true | false # optional, defaults to true
setpoint: decimal number # mandatory
dump-priority: integer number # optional, defaults to 0
economizer: # optional section, defaults to "not present"
changeover-delta: decimal number # mandatory
target-temperature: decimal number # mandatory
keep-hvac-on: true | false # optional, defaults to true
max-power: decimal number between 0 and 1 # optional, defaults to 1
```
### On/Off
### enabled
String literals `on` and `enabled` mean that the zone is ON during this period. This is the default behavior.
The value of `true` mean that the zone is ON during this period. This is the default behavior.

String literals `off` and `disabled` mean that the zone is OFF during this period. You have to explicitly specify this.
The value of `false`` mean that the zone is OFF during this period. You have to explicitly specify this.

Setpoint value is ignored for a disabled zone, to make it easier to do one-off edits. However, it is still required.

### Voting
### voting

String literal `voting` means that if the zone is calling for heat or cool during this period, it will cause the HVAC unit to turn on. This is the default behavior.
The value of `true` means that if the zone is calling for heat or cool during this period, it will cause the HVAC unit to turn on. This is the default behavior.

String literal `non-voting` or `not voting` means that the HVAC unit will only turn on if any other **voting** zone calls for heat or cool.
The value of `false` means that the HVAC unit will only turn on if any other **voting** zone calls for heat or cool.

### Setpoint
### setpoint

String literal `setpoint` or `temperature` followed by space, then a numerical value, and optional `C` or `F` unit modifier specifies the setpoint temperature.
Setpoint temperature. Numerical value. There is no default for the setpoint.

There is no default for the setpoint.

Default temperature unit is `C` (degrees Celsius).
### dump-priority

## Start and end time, recurrence
Unused for now, but allowed to be present.

**IMPORTANT:** Period can't cross midnight. This is an ancient design limitation, [comment on issue #5](https://github.com/home-climate-control/dz/issues/5) if this bothers you.
### economizer

**IMPORTANT**: Google Calendar has a bug where recurrent events ending "never", in actuality, expire 2 years after the first event - but only under some circumstances. Don't forget to check your schedule into the future. Workaround: duplicate the event, and adjust the start date.
### economizer.changeover-delta

Other than that, self-explanatory.
Specifies temperature difference between indoor and outdoor temperature necessary to turn the device on.

# Examples
### economizer.target-temperature

### Away: setpoint 32C, not voting
When this temperature is reached, the economizer is shut off.

Period name is `Away`
Setpoint is at 32°C
This zone will **not** turn on the HVAC if it calls for heat or cool because it is `non-voting`, but its setpoint will be satisfied if any other voting zone does.
### economizer.keep-hvac-on

### Away: setpoint 32, not voting
The value of `true` means that the main HVAC unit for this zone will be kept on even if the economizer is on. This maximizes comfort.

Same as the above, but the temperature unit defaults to degrees Celsius.
The value of `false` means that when the conditions are suitable for the economizer to turn on, the main HVAC unit will be kept off. This maximizes energy savings.

### Away: setpoint 90F, not voting
### economizer.max-power

Same as the above, but the temperature unit is explicitly specified to be degrees Fahrenheit.
Specifies the maximum amount of power allowed to be applied to a variable output HVAC device acting as an economizer (example: max speed for a fan).

### Away: setpoint 32, off
The value will be ignored with a log warning if the configured economizer device doesn't support variable output.

Same as the above, but the zone will not participate in HVAC exchange, dampers will stay closed no matter what happens to the rest of the house. This zone will appear grayed out on Swing and mobile consoles.
## Start and end time, recurrence

### Night: setpoint 24
**IMPORTANT:** Period can't cross midnight. This is an ancient design limitation, [comment on issue #5](https://github.com/home-climate-control/dz/issues/5) if this bothers you.

**IMPORTANT**: Google Calendar has a bug where recurrent events ending "never", in actuality, expire 2 years after the first event - but only under some circumstances. Don't forget to check your schedule into the future. Workaround: duplicate the event, and adjust the start date.

Period name is `Night`
Setpoint is at 24°C
Zone **will** turn on the HVAC if it calls for heat or cool, and it will be turned off when all other voting zones that it serves are satisfied.
Other than that, self-explanatory.

---
[^^^ Configuration](./index.md)
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package net.sf.dz3r.device.actuator.economizer;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

/**
* Set of user changeable economizer settings.
*
* @author Copyright &copy; <a href="mailto:[email protected]">Vadim Tkachenko</a> 2001-2024
*/
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class)
public class EconomizerSettings {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package net.sf.dz3r.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import net.sf.dz3r.device.actuator.economizer.EconomizerSettings;
import net.sf.dz3r.signal.hvac.ZoneStatus;

import java.util.Objects;
import java.util.Optional;

/**
* Zone settings.
Expand All @@ -14,13 +20,20 @@
*
* @author Copyright &copy; <a href="mailto:[email protected]">Vadim Tkachenko</a> 2001-2024
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class)
public class ZoneSettings {

public final Boolean enabled;
public final Double setpoint;
public final Boolean voting;

@JsonIgnore
public final Boolean hold;

public final Integer dumpPriority;

@JsonProperty("economizer")
public final EconomizerSettings economizerSettings;

/**
Expand Down Expand Up @@ -100,18 +113,33 @@ public String toString() {
+ ", voting=" + voting
+ ", hold=" + hold
+ ", dump=" + dumpPriority
+ ", ecp=" + economizerSettings
+ ", economizer=" + economizerSettings
+ "}";
}

@JsonIgnore
public boolean isEnabled() {
return Optional.ofNullable(enabled).orElse(true);
}

@JsonIgnore
public boolean isVoting() {
return Optional.ofNullable(voting).orElse(true);
}

@JsonIgnore
public int getDumpPriority() {
return Optional.ofNullable(dumpPriority).orElse(0);
}

@Override
public boolean equals(Object o) {
return o instanceof ZoneSettings other
&& enabled.equals(other.enabled)
&& isEnabled() == other.isEnabled()
&& setpoint.equals(other.setpoint)
&& voting.equals(other.voting)
&& isVoting() == other.isVoting()
&& ((hold == null && other.hold == null) || (hold != null && hold.equals(other.hold)))
&& dumpPriority.equals(other.dumpPriority)
&& getDumpPriority() == other.getDumpPriority()
&& ((economizerSettings == null && other.economizerSettings == null) || (economizerSettings != null && economizerSettings.equals(other.economizerSettings)));
}

Expand Down
2 changes: 2 additions & 0 deletions modules/hcc-scheduler-gcal-v3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ dependencies {
implementation(libs.google.api.services.calendar)
implementation(libs.google.oauth.client.jetty)
implementation(libs.google.http.client.jackson2)
implementation(libs.jackson.databind)
implementation(libs.jackson.dataformat.yaml)

implementation(project(":modules:hcc-common"))
implementation(project(":modules:hcc-scheduler"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ private Flux<Map.Entry<SchedulePeriod, ZoneSettings>> convertEvent(Event event)
return Flux.empty();
}

var settings = settingsParser.parse(event.getSummary().substring(period.name.length() + 1));
var settingsAsString = event.getSummary().substring(period.name.length() + 1);
var settings = settingsParser.parse(event, settingsAsString);

return Flux.just(new AbstractMap.SimpleEntry<>(period, settings));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.sf.dz3r.scheduler.gcal.v3;

import java.nio.ByteBuffer;

public class HexFormat {

private HexFormat() {}

public static String format(byte value) {
return String.format("0x%02X", value & 0xFF);
}

public static String format(int value) {
return String.format("0x%02X", value);
}

public static String format(String data) {
return format(ByteBuffer.wrap(data.getBytes()));
}

public static String format(ByteBuffer data) {

var sb = new StringBuilder();

while (true) {

sb.append(HexFormat.format(data.get()));

if (data.hasRemaining()) {
sb.append(",");
} else {
return sb.toString();
}
}
}
}
Loading

0 comments on commit e1b155c

Please sign in to comment.