Skip to content

Commit

Permalink
Many APP API features implemented
Browse files Browse the repository at this point in the history
Validate config => server can tell you if a config file is OK
Implemented and cleaned up APIs for operation_mode and override_mode
resume_normal_operations
set_default_mode
initialize_new_heater
  • Loading branch information
stevemidgley committed Oct 29, 2013
1 parent 81a3262 commit 47dd365
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 45 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ A configuration file looks like:
{
"debug": {"log_level": 9},
"operation_mode": "daily_schedule",
"default_mode": "daily_schedule"
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 60},
Expand Down Expand Up @@ -140,14 +141,13 @@ will be in effect until the temp_override option is removed from the file, or th
revert to normal daily schedule operations.

## Immediate
The immediate function serves to hold the temperature at a constant value for as long as the option is provided (the time_stamp on the option
must be less than the current time as well -- allowing you to set an immediate temperature for some time in the future as well). A
config file where the immediate function is active looks like:
The immediate function serves to hold the temperature at a constant value for as long as the option is provided (the time_stamp on the option must be less than the current time as well -- allowing you to set an immediate temperature for some time in the future as well). A config file where the immediate function is active looks like:

```json
{
"debug": {"log_level": 9},
"operation_mode": "immediate",
"default_mode": "daily_schedule"
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 60},
Expand All @@ -162,6 +162,8 @@ config file where the immediate function is active looks like:
```
In this mode of operation, the thermostat system will try to maintain the temperature at 74 degrees until the operation_mode or immediate temp_f value is changed.

The "default_mode" key is used to store the operation_mode that should be initiated when "immediate" mode is turned off. This can simplify the user experience, as the user can simply toggle immediate mode on/off and the heater can resume regularly scheduled operation. In the future additional modes of operation may be enabled, so this value will be more important then.

## Off
The off function serves to ensure the thermostat system keeps the heater off in all cases. An example of this config file is:

Expand Down
1 change: 1 addition & 0 deletions thermoclient/test/backbedroom.json.immediate.orig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"operation_mode": "immediate",
"default_mode": "daily_schedule",
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 62},
Expand Down
1 change: 1 addition & 0 deletions thermoclient/test/boot-server.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"config": {
"port": 8080,
"api_key": "abc123xyz",
"app_api_key": "xyz789xyz",
"base_folder": "./"
}
}
26 changes: 26 additions & 0 deletions thermoclient/test/test-thermoclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,32 @@ def test_temp_override_on_function
thermostat.process_schedule
assert_equal 70, thermostat.goal_temp_f
assert_heater_state_time({:heater_on => false, :last_on_time => last_time_on}, thermostat)

# advance clock to the following day at 10:01am 9/15/29
# set temp to 65
# daily_schedule wants temp at 62, so heater should be off
cur_time = Chronic.parse("9/15/29 10:01 am")
cur_temp = 65
assert_set_and_test_time_temp(cur_time, cur_temp, thermostat)
assert_heater_state_time({:heater_on => false, :last_on_time => last_time_on}, thermostat)
assert thermostat.heater_safe_to_turn_on?
thermostat.process_schedule
status = JSON.parse(thermostat.status_metadata)
assert_heater_state_time({:heater_on => false, :last_on_time => last_time_on}, thermostat)
assert_equal 62, thermostat.goal_temp_f

# advance clock to 10:17 9/15/29
# daily_schedule wants temp at 62, temp_override from previous day wants
# temp at 72. Heater should remain off
cur_time = Chronic.parse("9/15/29 10:17 am")
cur_temp = 65
assert_set_and_test_time_temp(cur_time, cur_temp, thermostat)
assert_heater_state_time({:heater_on => false, :last_on_time => last_time_on}, thermostat)
assert thermostat.heater_safe_to_turn_on?
thermostat.process_schedule
status = JSON.parse(thermostat.status_metadata)
assert_heater_state_time({:heater_on => false, :last_on_time => last_time_on}, thermostat)
assert_equal 62, thermostat.goal_temp_f
end

def test_temp_override_on_function_corner_cases
Expand Down
64 changes: 38 additions & 26 deletions thermoserver/docs/specification.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,54 @@ ThermoApp downloads status file from server

THERMOCLIENT API

POST: /api/[key]/file/[thermo-name]
JSON payload under key "file" - upload is written directly to file [thermo-name]
GET: /api/[key]/file/[thermo-name]
GETs [thermo-name file]
GET: /api/[key]/if-file/newer-date/[date-string]/[thermo-name]
POST: /api/:api_key/file/:thermoname
POST payload under key "file" - upload is written directly to file :thermoname
GET: /api/:api_key/file/:thermoname
GETs :thermoname file
GET: /api/:api_key/if-file/newer-date/:date-string/:thermoname
Only returns content if file is newer than date specified
[date-string] in URI::escaped, Chronic-parsable format *without* slashes such as:
:date-string in URI::escaped, Chronic-parsable format *without* slashes such as:
"10-18-22%2010%3A05am" (represents: 10-18-22 10:05am)
GET: /api/[key]/list/[pattern]
GET: /api/:api_key/list/:pattern
Returns JSON payload with array of filenames corresponding to files matching pattern
[pattern] can only be ascii chars 0-9 period and hyphen - no wild card matches
:pattern can only be ascii chars 0-9 period and hyphen - no wild card matches
Primary use-case is to obtain all "config" or "status" files on the server

WEB APP API
In-progress
PUT: /webapi/[web-key]/[thermo-name]/off
Sets the named heater to off state
PUT: /webapi/[web-key]/[thermo-name]/on
Unsets the named heater off state
PUT: /webapi/[web-key]/[thermo-name]/hold/[temp_f]
Sets the named heater to hold temp at temp_f (hold temp is permanent until manually disabled)
PUT: /webapi/[web-key]/[thermo-name]/override/[temp_f]
Sets the named heater to override temp at temp_f (override temp is temporary until next scheduled event)
PUT: /webapi/[web-key]/[thermo-name]/resume_schedule
Returns the named heater to normal scheduled operation
POST: /webapi/[web-key]/[thermo-name]/initialize
Creates a new file with default configuration for thermo-name. If thermo-name exists, returns error.
PUT: /app-api/:app_api_key/:thermoname/operation_mode/:mode
Sets the named heater to mode specified
Mode options are: off or daily_schedule
To set "immediate" or "temp_override" modes, use "override_mode" API
Actions: operation_mode is set to :mode
PUT: /app-api/:app_api_key/:thermoname/override_mode/:mode/:temp_f
Sets the named heater to override :mode specified at :temp_f
:mode values can be: temp_override, hold, immediate (hold and immediate are synonyms)
Actions: operation_mode is set to temp_override or immediate, as appropriate
PUT: /app-api/:app_api_key/:thermoname/resume_default
Returns the named heater to normal scheduled operation (or off status)
Actions: temp_override key is deleted and operation mode value is set to value of key "default_mode" (or "off" if default_mode key is not found)
PUT: /app-api/:app_api_key/:thermoname/default_mode/:mode
Sets the value of default_mode. Possible values are "off" and "daily_schedule"
POST: /app-api/:app_api_key/:thermoname/initialize
Creates a new heater configuration file with default configuration for thermoname. If thermoname exists, returns error.

Planned:
POST: /public-api/validate/config
Validates a configuration file to confirm it will work with Thermoclient
Recieves payload under key "file" in POST upload
Returns 200 payload {"json": "valid", "fields": []} if OK
or 200 {"json"=>"invalid", "fields" => ["invalid_field_name_1", "invalid_field_name_2"]}
Values in fields may be descriptive and not precisely a field name


Planned
GET: /api/[key]/if-file/not-match-hash/[hash-string]/thermo-name]
GET: /api/:api_key/if-file/not-match-hash/:hash-string/thermoname]
Only returns content if hash specified does not match hash of current file on disk
HEAD: /api/[key]/file/[thermo-name]
HEAD: /api/:api_key/file/:thermoname
Lets client check head to see if file has changed
GET: /api/[key]/if-file/changes/[thermo-name]
GETs [thermo-name] file but return content only after the file changes (changes during wait period)
GET: /api/:api_key/if-file/changes/:thermoname
GETs :thermoname file but return content only after the file changes (changes during wait period)
timeout is a string passed as Chronic compatible future time so that this will parse
Chronic.parse("#{params[:timeout] after now}")
Chronic.parse("#{params::timeout after now}")


16 changes: 16 additions & 0 deletions thermoserver/test/default-conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"debug": {"log_level": 1},
"operation_mode": "daily_schedule",
"default_mode": "daily_schedule",
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 60},
{"start": "6:30 am", "stop": "10:00 am", "temp_f": 68},
{"start": "10:00 am", "stop": "6:30 pm", "temp_f": 64},
{"start": "6:30 pm", "stop": "11:00 pm", "temp_f": 68},
{"start": "11:00 pm", "stop": "12:00 am", "temp_f": 60}
]},
"immediate": {"temp_f": 72, "time_stamp": "1/1/2001 10:15am"},
"temp_override": {"time_stamp": "1/1/2001 10:15am", "temp_f": 72},
"off": "off"
}
111 changes: 101 additions & 10 deletions thermoserver/test/test-thermoserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
require 'uri'

VALID_CONFIG_JSON_ORIG = 'valid-backbedroom.confg.json.orig'
VALID_CONFIG_DEFAULT_OFF_JSON_ORIG = 'valid-backbedroom.default-off.confg.json.orig'

STATUS_JSON = 'backbedroom.status.json'
CONFIG_JSON = 'backbedroom.config.json'
Expand All @@ -55,7 +56,7 @@ def setup

def teardown
FileUtils::safe_unlink(CONFIG_JSON)
raise if File::exist?(CONFIG_JSON)
raise if File::exists?(CONFIG_JSON)
end

def app
Expand All @@ -79,19 +80,34 @@ def test_webapp_get_file

## App API tests

def test_publicapi_validate_config
filedata = File::read(CONFIG_JSON)
filename = CONFIG_JSON
tempfile = Tempfile.new('validate_config')
tempfile.write(filedata)
tempfile.rewind
upload_file = Rack::Test::UploadedFile.new(tempfile.path, "text/json")
post "/public-api/validate/config", "file" => upload_file
assert_equal 200, last_response.status, last_response.body
retval = JSON.parse(last_response.body)
assert_equal "valid", retval["json"], retval.inspect
assert_equal [], retval["fields"], retval.inspect
tempfile.unlink
end

def test_webapi_turn_heater_off
config_json = JSON.parse(File::read(CONFIG_JSON))
# verify operation mode is not off
assert_equal "daily_schedule", config_json["operation_mode"]
get "/app-api/#{@app_api_key}/#{CONFIG_JSON}/operation_mode/off"
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/operation_mode/off"
assert_equal 200, last_response.status, last_response.body
# verify operation mode is off
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "off", config_json["operation_mode"]
assert_equal "off", config_json["off"]

# turn the operation mode back to "daily_schedule"
get "/app-api/#{@app_api_key}/#{CONFIG_JSON}/operation_mode/daily_schedule"
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/operation_mode/daily_schedule"
assert_equal 200, last_response.status, last_response.body
# verify operation mode is off
config_json = JSON.parse(File::read(CONFIG_JSON))
Expand All @@ -102,20 +118,99 @@ def test_webapi_turn_heater_off
def test_webapi_turn_heater_off_invalid_config_file
# verify trying to modify a non-existent heater config file
# results in an error
get "/app-api/#{@app_api_key}/invalid_heater.json/operation_mode/off"
put "/app-api/#{@app_api_key}/invalid_heater.json/operation_mode/off"
assert_equal 404, last_response.status, last_response.body
end

def test_webapi_turn_heater_to_hold
get "/app-api/#{@app_api_key}/#{CONFIG_JSON}/override_mode/hold/74"
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/override_mode/hold/74"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "74", config_json["immediate"]["temp_f"]
assert_equal Time::now.to_s, config_json["immediate"]["time_stamp"]
assert_equal "74", config_json["immediate"]["temp_f"]
assert_equal "immediate", config_json["operation_mode"]
end

def test_webapi_turn_heater_to_temp_override
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/override_mode/temp_override/68"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal Time::now.to_s, config_json["temp_override"]["time_stamp"]
assert_equal "68", config_json["temp_override"]["temp_f"]
assert_equal "daily_schedule", config_json["operation_mode"]
assert_equal "daily_schedule", config_json["default_mode"]
end

def test_webapi_resume_normal_operations
# first we turn on hold mode
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/override_mode/hold/74"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "74", config_json["immediate"]["temp_f"]
assert_equal Time::now.to_s, config_json["immediate"]["time_stamp"]
assert_equal "immediate", config_json["operation_mode"]
assert_equal "daily_schedule", config_json["default_mode"]
# then we call resume_default
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/resume/default"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "74", config_json["immediate"]["temp_f"]
assert !config_json["temp_override"], "temp_override key should have been deleted but was not."
assert_equal "daily_schedule", config_json["operation_mode"]
assert_equal "daily_schedule", config_json["default_mode"]
# verify that if we set default_operation to "off", resuming will leave heater in off state
FileUtils::cp(VALID_CONFIG_DEFAULT_OFF_JSON_ORIG, CONFIG_JSON)
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/override_mode/hold/74"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "74", config_json["immediate"]["temp_f"]
assert_equal Time::now.to_s, config_json["immediate"]["time_stamp"]
assert_equal "immediate", config_json["operation_mode"]
assert_equal "off", config_json["default_mode"]
# then we call resume_default
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/resume/default"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "74", config_json["immediate"]["temp_f"]
assert !config_json["temp_override"], "temp_override key should have been deleted but was not."
assert_equal "off", config_json["operation_mode"]
assert_equal "off", config_json["default_mode"]
end

def test_webapi_set_default_mode
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "daily_schedule", config_json["default_mode"]
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/default/off"
assert_equal 200, last_response.status, last_response.body
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "off", config_json["default_mode"]
put "/app-api/#{@app_api_key}/#{CONFIG_JSON}/default/daily_schedule"
config_json = JSON.parse(File::read(CONFIG_JSON))
assert_equal "daily_schedule", config_json["default_mode"]
end

def test_webapi_initialize_new_heater
assert !File::exists?(SECOND_CONFIG_JSON)
begin
post "/app-api/#{@app_api_key}/#{SECOND_CONFIG_JSON}/initialize"
assert_equal 200, last_response.status, last_response.body
assert File::exists?(SECOND_CONFIG_JSON)
config_json = JSON.parse(File::read(SECOND_CONFIG_JSON))
assert_equal "daily_schedule", config_json["operation_mode"]
# change an element so we can test that re-posting doesn't overwrite existing config file
config_json["operation_mode"] = "off"
File::write(SECOND_CONFIG_JSON, JSON.generate(config_json))
config_json = JSON.parse(File::read(SECOND_CONFIG_JSON))
assert_equal "off", config_json["operation_mode"]
# post to the same config file, and verify that it doesn't overwrite existing file
post "/app-api/#{@app_api_key}/#{SECOND_CONFIG_JSON}/initialize"
assert_equal 409, last_response.status, last_response.body
assert File::exists?(SECOND_CONFIG_JSON)
config_json = JSON.parse(File::read(SECOND_CONFIG_JSON))
assert_equal "off", config_json["operation_mode"]
ensure
FileUtils::safe_unlink(SECOND_CONFIG_JSON)
end
end

## Thermoclient API tests
Expand Down Expand Up @@ -203,10 +298,6 @@ def test_post_config_file_with_no_data
end
end

def test_functional_config_change

end

def test_favicon
get "/favicon.ico"
assert_equal 200, last_response.status, last_response.body
Expand Down
2 changes: 2 additions & 0 deletions thermoserver/test/valid-backbedroom.confg.json.orig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"debug": {"log_level": 9},
"operation_mode": "daily_schedule",
"default_mode": "daily_schedule",
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 60},
Expand All @@ -10,5 +11,6 @@
{"start": "11:00 pm", "stop": "12:00 am", "temp_f": 60}
]},
"immediate": {"temp_f": 77, "time_stamp": "9/14/13 10:15am"},
"temp_override": {"time_stamp": "9/14/29 10:15am", "temp_f": 72},
"off": "off"
}
16 changes: 16 additions & 0 deletions thermoserver/test/valid-backbedroom.default-off.confg.json.orig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"debug": {"log_level": 9},
"operation_mode": "daily_schedule",
"default_mode": "off",
"daily_schedule": {
"times_of_operation": [
{"start": "12:00 am", "stop": "6:30 am", "temp_f": 60},
{"start": "6:30 am", "stop": "10:00 am", "temp_f": 72},
{"start": "10:00 am", "stop": "6:00 pm", "temp_f": 74},
{"start": "7:00 pm", "stop": "11:00 pm", "temp_f": 73},
{"start": "11:00 pm", "stop": "12:00 am", "temp_f": 60}
]},
"immediate": {"temp_f": 77, "time_stamp": "9/14/13 10:15am"},
"temp_override": {"time_stamp": "9/14/29 10:15am", "temp_f": 72},
"off": "off"
}
Loading

0 comments on commit 47dd365

Please sign in to comment.