Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thermostat Schedule Retrieval Code #38

Merged
merged 26 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f097057
added first version of thermostat schedule retrieval code
john-b-yang Jan 10, 2019
8b64617
added working draft of schedule retrieval code. Added rrule conversio…
john-b-yang Jan 11, 2019
e595d59
cleaned up JSON struct field and type naming
john-b-yang Jan 11, 2019
f925fc5
debugging unknown variable issue
john-b-yang Jan 11, 2019
05d009b
created polling parameters, code for retrieving thermostat schedule i…
john-b-yang Jan 11, 2019
bd32c96
cleaned up method, struct, and variable naming. Replace id with noden…
john-b-yang Jan 11, 2019
79e33ee
edited thermostat scheduler code by recommendations
john-b-yang Jan 13, 2019
a46e042
removed unnecessary conditional case
john-b-yang Jan 14, 2019
fa4c6df
replaced structs for retrieving thermostat IDs with free form interfa…
john-b-yang Jan 14, 2019
a7ff14d
removed all decoding structs, reimplemented JSON parsing with free fo…
john-b-yang Jan 14, 2019
1e9aebd
reverted free form interface JSON decoding to struct based approach. …
john-b-yang Jan 15, 2019
92149c3
added small modification to interface name
john-b-yang Jan 16, 2019
420aed1
added msgpack aliases to return JSONs
john-b-yang Jan 16, 2019
ec0a841
added descriptions to the thermostat schedule result structs
john-b-yang Jan 16, 2019
7c6e4cc
some refs failed to push, repushing
john-b-yang Jan 16, 2019
686d8e0
adding markdown description of schedule interface, file is interface.md
john-b-yang Jan 18, 2019
91504f9
added more in depth explanations regarding schedule struct in interfa…
john-b-yang Jan 19, 2019
2ae10cd
corrected struct definition errors, added explanation regarding how R…
john-b-yang Jan 21, 2019
7739169
added examples regarding what time conversion might look like
john-b-yang Jan 22, 2019
a242229
corrected grammatical + syntax errors in interface doc
john-b-yang Jan 23, 2019
3d9501a
small grammatical correction
john-b-yang Jan 23, 2019
e93ed19
cut out intermediate day schedule struct and made time parsing more c…
john-b-yang Jan 24, 2019
1643010
added new schedule go request field to Pelican, corrected occupancy s…
john-b-yang Jan 24, 2019
e20fda0
created 3 new fields for Pelican struct (id, cookie, sitename). Recon…
john-b-yang Jan 26, 2019
266e819
added 2 clarifying comments regarding setCookieAndID method
john-b-yang Jan 26, 2019
aad2654
cleaned up syntax + conditional cases. Added new cookie expiration fi…
john-b-yang Jan 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions driver/pelican/interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
### Pelican Schedule Interface Outline

##### Context

The following structs define the way users interact with the Scheduling interface of the Pelican thermostats. These structs are used to define the weekly schedule and will be interpreted by the thermSchedule.go code to retrieve/view in addition to making changes to the existing schedule for individual thermostats within a particular site. These structs are public and accessible to anyone who subscribes to the allotted endpoint or publishes to the assigned signal.

##### Schedule Structs

```
// Struct mapping each day of the week to its daily schedule
type ThermostatSchedule struct {
DaySchedules map[string]([]ThermostatBlockSchedule) `msgpack:"day_schedules"`
}

// Struct containing data defining the settings of each schedule block
type ThermostatBlockSchedule struct {
// Cooling turns on when room temperature exceeds cool setting temp.
CoolSetting float64 `msgpack:"cool_setting"`
// Heating turns on when room temperature drops below heat setting temp.
HeatSetting float64 `msgpack:"heat_setting"`
// Indicates if system is heating, cooling, off, or set to auto
System string `msgpack:"system"`
// Indicates the time of day which the above settings are enacted.
Time string `msgpack:"time"`
}
```

##### Schedule Structs Explanation

Each Pelican Thermostat has three potential schedule settings.
1. Weekly: Each day of the week (Sun - Sat) has a unique daily schedule setting
2. Daily: Each day of the week has the same daily schedule
3. Weekday/Weekend: Per the name, weekdays and weekends have different schedules.

Next, it's wise if we attempt to define what a "daily schedule" actually looks like. Each day's schedule consists of a series of what we'll call "blocks". Each block details a certain number of settings that are enacted at a certain time of day. This is encapsulated by the ThermostatBlockSchedule struct. For example, one might have a series of four different blocks with time intervals at 6:00 a.m., 11:00 a.m., 4:00 p.m., and 6:00 p.m. At each of these times, the associated cool temperature, heat temperature, and system settings are all enacted.

The outermost struct, "ThermostatSchedule", maps each day of the week (Sunday - Saturday) to their respective daily schedules which is represented as an array of ThermostatBlockSchedule objects. Each day may have a different series of configurations that are enforced at different times, which is why there are multiple blocks per day. Ultimately, the pelican's "GetSchedule" function returns a pointer to this struct, which encapsulates the entire weekly schedule for that particular pelican thermostat.

##### Thermostat Block Schedule Struct Fields Explanation

- CoolSetting: The cool setting refers to the temperature at which the system begins cooling. In other words, if the room temperature surpasses this threshold, the cooling system is activated. The unit of temperature is Fahrenheit.
- HeatSetting: The heat setting refers to the temperature at which the system begins heating. In other words, if the room temperature falls below this threshold, the heating system is activated. The unit of temperature is Fahrenheit.
- System: This indicates what the system is currently doing. There are four possible settings (heat/cool/off/auto) which are pretty self explanatory. Heat and cool mean the systems heating or cooling the room. Auto means that the system will automatically heat or cool according to the room temperature and cool/heat thresholds.
- Time: Time describes what time of day the particular block's settings are enacted (e.g. 6:00:AM). This time is in the [RRule format](https://tools.ietf.org/html/rfc5545), and the rrule-go library is used to convert the given time into the designated format. The following section describes how time is formatted and defined in greater detail.

##### A Deeper Dive into Time Format (RRule)
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved

Within "thermoSchedule.go", this particular block in the "convertTimeToRRule" function is responsible for creating the RRule format.

```
rruleSched, _ := rrule.NewRRule(rrule.ROption{
Freq: rrule.WEEKLY,
Wkst: weekRRule[dayOfWeek],
Dtstart: time.Date(0, 0, 0, hour, minute, 0, 0, timezone),
})
```

Three fields are configured.
- Frequency indicates the interval with which this event occurs.
- Wkst tells us which day of the week (Sunday - Saturday) this event occurs.
- Dtstart is a required field that indicates the "start date" of the particular event. In Go, the Dtstart field is a time.Date object, which is initialized with the following parameters: year, month, day, hour minute, second, millisecond, timezone. For our purposes, there is no real concept of a "start date", just the time, so the year, month, and day parameters are filled with dummy values of 0. Only hour, minute, and timezone (which can be determined from the Pelican settings + schedule) are filled in. As long as an individual knows the time is in RRule format, he or she will be able to determine each field.

The translation from the above RRule format to a string is performed using the RRule-go module, specifically this function linked [here](https://github.com/teambition/rrule-go/blob/master/str.go#L123).

Within the thermSchedule.go code, the last line of the convertTimeToRRule function calls the ".string()" function of the RRule object. The implementation of this function is fairly straightforward. In a nutshell, the conversion function scans through each of the RRule object's fields. The "key-value" pair of each field is appended to a string object, which is ultimately returned. Some helper functions are used primarily for casting a variety of types into a string, such as appendIntsOption, timeToStr (for Dtstart), and append. In a general sense, the conversion function is meant to achieve two things:

1. Be as human readable as possible. If one reads the resulting string output, he or she should easily be able to identify the interval, frequency, count, and start/end dates of the respective event.
2. Can be converted back into RRule format. The RRule-go module has a complementary ".StrToRRule(rfcString string)" function that converts from string back to an RRule object.

gtfierro marked this conversation as resolved.
Show resolved Hide resolved
##### Time Format Conversion Examples

The following is an example of what we can expect the conversion to take in and output for different types of events with different settings

With the Pelican Thermostats, we will typically have the following settings:

```
Frequency: Weekly
Wkst: Day of Week (Sunday, Monday...Saturday)
Dtstart: time.Date(0, 0, 0, Hour, Minute, 0, 0, Timezone)
```

As a reminder, the parameters of the time.Date object are Year, Month, Date, Hour, Minute, Second, Millisecond, and Timezone. Frequency is always set to "Weekly". Wkst and the Hour/Minute/Timezone depend on the value that is being retrieved in addition to the Pelican's timezone. The NewRRule object's fields are populated with these values and the aforementioned ".String()" method is called. Assuming the Wkst is Sunday and Dtstart is 6:00 a.m. in U.S. Pacific Standard Time, the output will look like this:

```
FREQ=WEEKLY;DTSTART=-00011201T055258Z;WKST=SU
```

Another example can be found from the RRule-go Github [example page](https://github.com/teambition/rrule-go/blob/master/example/main.go). This contains a pretty comprehensive set of RRule's with different assortments of fields in them. In general, if a new field is added to the RRule, you can expect to see a concise, human readable string added to the output string.

##### XBOS Interface Configuration

The current version of XBOS uses YAML files to define the expectations for the output of different functionalities of the driver code from the bw2-contrib repository. There are a couple limitations regarding what the YAML files are able to represent. The incumbent version of XBOS features protobuf definitions for messages. When the next release of XBOS comes, both new and existing YAML files will be created and modified to reflect the outputs' types more accurately.
27 changes: 27 additions & 0 deletions driver/pelican/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
const TSTAT_PO_DF = "2.1.1.0"
const DR_PO_DF = "2.1.1.9"
const OCCUPANCY_PO_DF = "2.1.2.1"
const SCHED_PO_DF = "2.1.2.2"
gtfierro marked this conversation as resolved.
Show resolved Hide resolved

type setpointsMsg struct {
HeatingSetpoint *float64 `msgpack:"heating_setpoint"`
Expand Down Expand Up @@ -73,9 +74,17 @@ func main() {
os.Exit(1)
}

pollSchedStr := params.MustString("poll_interval_sched")
pollSched, schedErr := time.ParseDuration(pollSchedStr)
if schedErr != nil {
fmt.Printf("Invalid schedule poll interval specified: %v\n", schedErr)
os.Exit(1)
}

service := bwClient.RegisterService(baseURI, "s.pelican")
tstatIfaces := make([]*bw2.Interface, len(pelicans))
drstatIfaces := make([]*bw2.Interface, len(pelicans))
schedstatIfaces := make([]*bw2.Interface, len(pelicans))
occupancyIfaces := make([]*bw2.Interface, len(pelicans))
for i, pelican := range pelicans {
pelican := pelican
Expand All @@ -85,6 +94,7 @@ func main() {
fmt.Println("Transforming", pelican.Name, "=>", name)
tstatIfaces[i] = service.RegisterInterface(name, "i.xbos.thermostat")
drstatIfaces[i] = service.RegisterInterface(name, "i.xbos.demand_response")
schedstatIfaces[i] = service.RegisterInterface(name, "i.xbos.thermostat_schedule")
occupancyIfaces[i] = service.RegisterInterface(name, "i.xbos.occupancy")

// Ensure thermostat is running with correct number of stages
Expand Down Expand Up @@ -207,6 +217,7 @@ func main() {
currentPelican := pelican
currentIface := tstatIfaces[i]
currentDRIface := drstatIfaces[i]
currentSchedIface := schedstatIfaces[i]
currentOccupancyIface := occupancyIfaces[i]

go func() {
Expand Down Expand Up @@ -244,6 +255,22 @@ func main() {
}
}()

go func() {
for {
if schedStatus, schedErr := currentPelican.GetSchedule(); schedErr != nil {
fmt.Printf("Failed to retrieve Pelican's Schedule: %v\n", schedErr)
} else {
fmt.Printf("%s Schedule: %+v\n", currentPelican.Name, schedStatus)
po, err := bw2.CreateMsgPackPayloadObject(bw2.FromDotForm(SCHED_PO_DF), schedStatus)
if err != nil {
fmt.Printf("Failed to create Schedule msgpack PO: %v", err)
}
currentSchedIface.PublishSignal("info", po)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gtfierro In the Pelican's status retrieval loop, we write to the done channel if an error occurs, which prompts the driver to exit. We chose not to do this in the DR event retrieval loop because we figured a problem with the DR interface should be logged but not prompt an exit.

Do you have a preference on which approach we take here? As it stands, an error during schedule retrieval will be logged but the driver will keep running.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the "log but keep going" approach, like we did with DR event retrieval

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I think this is ready then!

}
time.Sleep(pollSched)
}
}()

occupancy, err := currentPelican.GetOccupancy()
if err != nil {
fmt.Printf("Failed to retrieve initial occupancy reading: %s\n", err)
Expand Down
1 change: 1 addition & 0 deletions driver/pelican/params.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ sitename: <pelican site name>
name: <thermostat name>
poll_interval: <status poll interval>
poll_interval_dr: <dr status poll interval>
poll_interval_sched: <schedule poll interval>
2 changes: 1 addition & 1 deletion driver/pelican/types/occupancy.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type childSensor struct {
}

func (pel *Pelican) GetOccupancy() (int, error) {
resp, _, errs := pel.occupanyReq.Get(pel.target).
resp, _, errs := pel.occupancyReq.Get(pel.target).
Param("username", pel.username).
Param("password", pel.password).
Param("request", "get").
Expand Down
21 changes: 17 additions & 4 deletions driver/pelican/types/pelican.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package types
import (
"encoding/xml"
"fmt"
"net/http"
"strings"
"time"

Expand Down Expand Up @@ -30,15 +31,20 @@ var stateMappings = map[string]int32{
type Pelican struct {
username string
password string
sitename string
id string
Name string
HeatingStages int32
CoolingStages int32
TimezoneName string
target string
cookieTime time.Time
timezone *time.Location
cookie *http.Cookie
req *gorequest.SuperAgent
drReq *gorequest.SuperAgent
occupanyReq *gorequest.SuperAgent
occupancyReq *gorequest.SuperAgent
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
scheduleReq *gorequest.SuperAgent
}

type PelicanStatus struct {
Expand Down Expand Up @@ -150,9 +156,11 @@ func NewPelican(params *NewPelicanParams) (*Pelican, error) {
if err != nil {
return nil, err
}
return &Pelican{

newPelican := &Pelican{
username: params.Username,
password: params.Password,
sitename: params.Sitename,
target: fmt.Sprintf("https://%s.officeclimatecontrol.net/api.cgi", params.Sitename),
Name: params.Name,
HeatingStages: params.HeatingStages,
Expand All @@ -161,8 +169,13 @@ func NewPelican(params *NewPelicanParams) (*Pelican, error) {
timezone: timezone,
req: gorequest.New(),
drReq: gorequest.New(),
occupanyReq: gorequest.New(),
}, nil
occupancyReq: gorequest.New(),
scheduleReq: gorequest.New(),
}
if error := newPelican.setCookieAndID(); error != nil {
return nil, error
}
return newPelican, nil
}

func DiscoverPelicans(username, password, sitename string) ([]*Pelican, error) {
Expand Down
Loading