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 16 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
40 changes: 40 additions & 0 deletions driver/pelican/interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
### 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 interpretted 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.
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved

##### Schedule Structs

// Struct mapping each day of the week to its daily schedule <br>
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
type ThermostatSchedule struct {<br>
&nbsp;&nbsp;&nbsp;DaySchedules map[string]ThermostatDaySchedule `msgpack:"day_schedules"`<br>
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
}<br>

// Struct containing a series of blocks that describes a one day schedule <br>
type ThermostatDaySchedule struct { <br>
&nbsp;&nbsp;&nbsp;Blocks []ThermostatBlockSchedule `msgpack:blocks` <br>
} <br>

// Struct containing data defining the settings of each schedule block <br>
type ThermostatBlockSchedule struct { <br>
&nbsp;&nbsp;&nbsp;CoolSetting float64 `msgpack:"cool_setting"` <br>
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
&nbsp;&nbsp;&nbsp;HeatSetting float64 `msgpack:"heat_setting"` <br>
&nbsp;&nbsp;&nbsp;System string `msgpack:"system"` <br>
&nbsp;&nbsp;&nbsp;Time string `msgpack:"time"` <br>
}

##### 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 consist 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.
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved

Going one layer above, the ThermostatDaySchedule struct represents an array of blocks. The purpose of this struct is to represent the schedule of one day a.k.a a series of blocks. Last but not least, the outermost struct, "ThermostatSchedule", maps each day of the week (Sunday - Saturday) to their respective daily schedules (ThermostatDaySchedule struct). This is the struct that is delivered to the user for getting and setting purposes.

##### 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(sitename); 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>
252 changes: 252 additions & 0 deletions driver/pelican/types/thermSchedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package types

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/parnurzeal/gorequest"
rrule "github.com/teambition/rrule-go"
)

// Login, Authentication, Thermostat ID Retrieval Structs
type thermIDRequest struct {
Resources []thermIDResources `json:"resources"`
}

type thermIDResources struct {
Children []thermIDChild `json:"children"`
GroupId string `json:"groupId"`
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
Permissions string `json:"permissions"`
}

type thermIDChild struct {
Id string `json:"id"`
Permissions string `json:"permissions"`
}

// Thermostat Settings Structs
type settingsRequest struct {
Epnum float64 `json:"epnum"`
Id string `json:"id"`
Nodename string `json:"nodename"`
Userdata settingsWrapper `json:"userdata"`
}

type settingsWrapper struct {
Epnum float64 `json:"epnum"`
Fan string `json:"fan"`
Nodename string `json:"nodename"`
Repeat string `json:"repeat"`
}

// Thermostat Schedule By Day Decoding Structs
type scheduleRequest struct {
ClientData scheduleSetTimes `json:"clientdata"`
}

type scheduleSetTimes struct {
SetTimes []scheduleTimeBlock `json:"setTimes"`
}

type scheduleTimeBlock struct {
HeatSetting float64 `json:"heatSetting"`
CoolSetting float64 `json:"coolSetting"`
StartValue string `json:"startValue"`
System string `json:"systemDisplay"`
}

// Thermostat Schedule Structs

// Struct mapping each day of the week to its daily schedule
type ThermostatSchedule struct {
DaySchedules map[string]ThermostatDaySchedule `msgpack:"day_schedules"`
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
}

// Struct containing a series of blocks that describes a one day schedule
type ThermostatDaySchedule struct {
Blocks []ThermostatBlockSchedule `msgpack:blocks`
}

// Struct containing data defining the settings of each schedule block
type ThermostatBlockSchedule struct {
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
CoolSetting float64 `msgpack:"cool_setting"`
HeatSetting float64 `msgpack:"heat_setting"`
System string `msgpack:"system"`
Time string `msgpack:"time"`
}

var week = [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
var weekRRule = [...]rrule.Weekday{rrule.SU, rrule.MO, rrule.TU, rrule.WE, rrule.TH, rrule.FR, rrule.SA}

func (pel *Pelican) GetSchedule(sitename string) (map[string]ThermostatSchedule, error) {
// Retrieve Login Authentication Cookies
loginInfo := map[string]interface{}{
"username": pel.username,
"password": pel.password,
"sitename": sitename,
}
respLogin, _, errsLogin := gorequest.New().Post(fmt.Sprintf("https://%s.officeclimatecontrol.net/#_loginPage", sitename)).Type("form").Send(loginInfo).End()
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
if (errsLogin != nil) || (respLogin.StatusCode != 200) {
return nil, fmt.Errorf("Error logging into climate control website: %v", errsLogin)
}
cookies := (*http.Response)(respLogin).Cookies()
cookie := cookies[0]

// Retrieve Thermostat IDs within given sitename
respTherms, _, errsTherms := gorequest.New().Get(fmt.Sprintf("https://%s.officeclimatecontrol.net/ajaxSchedule.cgi?request=getResourcesExtended&resourceType=Thermostats", sitename)).Type("form").AddCookie(cookie).End()
if (errsTherms != nil) || (respTherms.StatusCode != 200) {
return nil, fmt.Errorf("Error retrieving Thermostat IDs: %v", errsTherms)
}

var IDRequest thermIDRequest
decoder := json.NewDecoder(respTherms.Body)
if decodeError := decoder.Decode(&IDRequest); decodeError != nil {
return nil, fmt.Errorf("Failed to decode Thermostat ID response JSON: %v\n", decodeError)
}
thermostatIDs := IDRequest.Resources[0].Children

// Construct Weekly Schedules for each Thermostat ID
schedules := make(map[string]ThermostatSchedule, len(thermostatIDs))
for _, thermostatID := range thermostatIDs {
thermSchedule := ThermostatSchedule{
DaySchedules: make(map[string]ThermostatDaySchedule, len(week)),
}

// Retrieve Repeat Type (Daily, Weekly, Weekend/Weekday) and Nodename from Thermostat's Settings
settings, settingsErr := getSettings(sitename, thermostatID.Id, cookie)
if settingsErr != nil {
return nil, fmt.Errorf("Failed to determine repeat type for thermostat %v: %v", thermostatID, settingsErr)
}
repeatType := settings.Repeat
nodename := settings.Nodename
epnum := settings.Epnum

// Build Schedule by Repeat Type
if repeatType == "Daily" {
schedule, scheduleError := getScheduleByDay(0, epnum, sitename, nodename, cookie, pel.timezone)
if scheduleError != nil {
return nil, fmt.Errorf("Error retrieving schedule for thermostat %v: %v", nodename, scheduleError)
}
for _, day := range week {
thermSchedule.DaySchedules[day] = *schedule
}
} else if repeatType == "Weekly" {
for index, day := range week {
schedule, scheduleError := getScheduleByDay(index, epnum, sitename, nodename, cookie, pel.timezone)
if scheduleError != nil {
return nil, fmt.Errorf("Error retrieving schedule for thermostat %v on %v (day %v): %v", nodename, day, index, scheduleError)
}
thermSchedule.DaySchedules[day] = *schedule
}
} else if repeatType == "Weekday/Weekend" {
weekend, weekendError := getScheduleByDay(0, epnum, sitename, nodename, cookie, pel.timezone)
if weekendError != nil {
return nil, fmt.Errorf("Error retrieving schedule for thermostat %v on weekend (day 0): %v", nodename, weekendError)
}
for _, day := range []string{"Sunday", "Saturday"} {
thermSchedule.DaySchedules[day] = *weekend
}
weekday, weekdayError := getScheduleByDay(1, epnum, sitename, nodename, cookie, pel.timezone)
if weekdayError != nil {
return nil, fmt.Errorf("Error retrieving schedule for thermostat %v on weekday (day 1): %v", nodename, weekdayError)
}
for _, day := range []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"} {
thermSchedule.DaySchedules[day] = *weekday
}
} else {
return nil, fmt.Errorf("Failed to recognize repeat type of thermostat %v's schedule: %v", nodename, repeatType)
}

schedules[thermostatID.Id] = thermSchedule
}
return schedules, nil
}

func getSettings(sitename, thermostatID string, cookie *http.Cookie) (*settingsWrapper, error) {
var requestURL bytes.Buffer
requestURL.WriteString(fmt.Sprintf("https://%s.officeclimatecontrol.net/ajaxThermostat.cgi?id=", sitename))
requestURL.WriteString(thermostatID)
requestURL.WriteString(":Thermostat&request=GetSchedule")

resp, _, errs := gorequest.New().Get(requestURL.String()).Type("form").AddCookie(cookie).End()
if errs != nil {
return nil, fmt.Errorf("Failed to retrieve schedule settings for thermostat %v: %v", thermostatID, errs)
}
var result settingsRequest
decoder := json.NewDecoder(resp.Body)
if decodeError := decoder.Decode(&result); decodeError != nil {
return nil, fmt.Errorf("Failed to decode schedule settings for thermostat %v: %v", thermostatID, decodeError)
}
return &result.Userdata, nil
}

func getScheduleByDay(dayOfWeek int, epnum float64, sitename, thermostatID string, cookie *http.Cookie, timezone *time.Location) (*ThermostatDaySchedule, error) {
// Construct Request URL for Thermostat Schedule by Day of Week
var requestURL bytes.Buffer
requestURL.WriteString(fmt.Sprintf("https://%s.officeclimatecontrol.net/thermDayEdit.cgi?section=json&nodename=", sitename))
requestURL.WriteString(thermostatID)
requestURL.WriteString("&epnum=")
requestURL.WriteString(fmt.Sprintf("%.0f", epnum))
requestURL.WriteString("&dayofweek=")
requestURL.WriteString(strconv.Itoa(dayOfWeek))

// Make Request, Decode into Response Struct
resp, _, errs := gorequest.New().Get(requestURL.String()).Type("form").AddCookie(cookie).End()
if errs != nil {
return nil, fmt.Errorf("Failed to retrieve schedule for thermostat %v on day of week %v: %v", thermostatID, dayOfWeek, errs)
}
var result scheduleRequest
decoder := json.NewDecoder(resp.Body)
if decodeError := decoder.Decode(&result); decodeError != nil {
return nil, fmt.Errorf("Failed to decode schedule for thermostat %v on day of week %v: %v", thermostatID, dayOfWeek, decodeError)
}

// Transfer Response Struct Data into return struct
var daySchedule ThermostatDaySchedule
for _, block := range result.ClientData.SetTimes {
var returnBlock ThermostatBlockSchedule
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
returnBlock.CoolSetting = block.CoolSetting
returnBlock.HeatSetting = block.HeatSetting
returnBlock.System = block.System

if rruleTime, rruleError := convertTimeToRRule(dayOfWeek, block.StartValue, timezone); rruleError != nil {
return nil, fmt.Errorf("Failed to convert time in string format %v to rrule format: %v", block.StartValue, rruleError)
} else {
returnBlock.Time = rruleTime
}

daySchedule.Blocks = append(daySchedule.Blocks, returnBlock)
}
return &daySchedule, nil
}

func convertTimeToRRule(dayOfWeek int, blockTime string, timezone *time.Location) (string, error) {
timeSlice := strings.Split(blockTime, ":")
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
hour, hourErr := strconv.Atoi(timeSlice[0])
if hourErr != nil {
return "", fmt.Errorf("Failed to convert hour value of type string to type int: %v", hourErr)
}
if timeSlice[2] == "PM" {
hour += 12
if hour == 24 {
hour = 0
}
}
minute, minuteErr := strconv.Atoi(timeSlice[1])
if minuteErr != nil {
return "", fmt.Errorf("Failed to convert minute value of type string to type int: %v", minuteErr)
}

rruleSched, _ := rrule.NewRRule(rrule.ROption{
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
john-b-yang marked this conversation as resolved.
Show resolved Hide resolved
Freq: rrule.WEEKLY,
Wkst: weekRRule[dayOfWeek],
Dtstart: time.Date(0, 0, 0, hour, minute, 0, 0, timezone),
})

return rruleSched.String(), nil
}