-- [The following is from Hugh Eaves' I_RTCOA_Wifi_ZoneThermostat1.xml. -- He figured out how to work around problems in deploying compressed -- modules. Please cite him as copyright owner under the GPL license terms.] -- Using "require" to access compressed modules doesn't work if the -- module is declared without using the "module" function. -- (see http://bugs.micasaverde.com/view.php?id=2276 ) -- -- We work around this with a shell script that executes pluto-lzo -- to decompress the module. The temp file is used to -- avoid a race condition when multiple instances of this module -- start at the same time. (to prevent one instance from loading a -- partially decompressed file from another instance) local decompressScript = [[ decompress_lzo_file() { SRC_FILE=/etc/cmh-ludl/$1.lzo DEST_FILE=/etc/cmh-ludl/$1 if [ ! -e $DEST_FILE -o $SRC_FILE -nt $DEST_FILE ] then TEMP_FILE=$(mktemp) pluto-lzo d $SRC_FILE $TEMP_FILE mv $TEMP_FILE $DEST_FILE fi } ]] os.execute(decompressScript .. "decompress_lzo_file L_ecobee_dkjson.lua") local MSG_CLASS = "ecobee" local DEBUG_MODE = true local taskHandle = -1 local TASK_ERROR = 2 local TASK_ERROR_PERM = -2 local TASK_SUCCESS = 4 local TASK_BUSY = 1 -- utility functions local function log(text, level) luup.log(MSG_CLASS .. ": " .. text, (level or 1)) end local function debug(text) if (DEBUG_MODE) then log("debug: " .. text, 35) end end local function task(text, mode) local mode = mode or TASK_ERROR if (mode ~= TASK_SUCCESS) then log("task: " .. text, 50) end taskHandle = luup.task(text, (mode == TASK_ERROR_PERM) and TASK_ERROR or mode, MSG_CLASS, taskHandle) end local function readVariableOrInit(lul_device, serviceId, name, defaultValue) local var = luup.variable_get(serviceId, name, lul_device) if (var == nil) then var = defaultValue luup.variable_set(serviceId, name, var, lul_device) end return var end local function writeVariable(lul_device, serviceId, name, value) luup.variable_set(serviceId, name, value, lul_device) end local function writeVariableIfChanged(lul_device, serviceId, name, value) local curValue = luup.variable_get(serviceId, name, lul_device) if value ~= curValue then writeVariable(lul_device, serviceId, name, value) end return value ~= curValue end local function findChild(deviceId, label) for k, v in pairs(luup.devices) do if (v.device_num_parent == deviceId and v.id == label) then return k end end end -- constants local PLUGIN_VERSION = "1.9" local ECOBEE_SID = "urn:ecobee-com:serviceId:Ecobee1" local TEMP_SENSOR_SID = "urn:upnp-org:serviceId:TemperatureSensor1" local TEMP_SETPOINT_HEAT_SID = "urn:upnp-org:serviceId:TemperatureSetpoint1_Heat" local TEMP_SETPOINT_COOL_SID = "urn:upnp-org:serviceId:TemperatureSetpoint1_Cool" local TEMP_SETPOINT_SID = "urn:upnp-org:serviceId:TemperatureSetpoint1" local HUMIDITY_SENSOR_SID = "urn:micasaverde-com:serviceId:HumiditySensor1" local HVAC_FAN_SID = "urn:upnp-org:serviceId:HVAC_FanOperatingMode1" local HVAC_USER_SID = "urn:upnp-org:serviceId:HVAC_UserOperatingMode1" local HVAC_STATE_SID = "urn:micasaverde-com:serviceId:HVAC_OperatingState1" local HA_DEVICE_SID = "urn:micasaverde-com:serviceId:HaDevice1" local SWITCH_POWER_SID = "urn:upnp-org:serviceId:SwitchPower1" local MCV_ENERGY_METERING_SID = "urn:micasaverde-com:serviceId:EnergyMetering1" local SECURITY_SENSOR_SID = "urn:micasaverde-com:serviceId:SecuritySensor1" local DEFAULT_POLL = 180 local MIN_POLL = 180 local SOON = "5" -- seconds local PARENT_DEVICE local syncDevices = false local dkjson = require("L_ecobee_dkjson") local json = L_ecobee_dkjson local ecobee = require("L_ecobee") local veraTemperatureScale = "F" local function getVeraTemperatureScale() local code, data = luup.inet.wget("http://localhost:3480/data_request?id=lu_sdata") if (code == 0) then data = json.decode(data) end veraTemperatureScale = ((code == 0) and (data ~= nil) and (data.temperature ~= nil)) and data.temperature or "F" end -- child device altids local THERM_ID_PREFIX = "therm." local HUMID_ID_PREFIX = "humid." local HOUSE_ID_PREFIX = "house." local SENSOR_ID_PREFIX = "sensor." -- isolate the number after the "." but before an "_" local function getThermostatId(lul_device) local altid = luup.devices[lul_device].id return string.sub(altid, string.find(altid, "%d+")) end -- find the sibling device that is really the kind of device we are looking for -- defaults to finding the thermostat sibling if prefix not specified local function findSibling(lul_device, prefix) prefix = prefix or THERM_ID_PREFIX local id = getThermostatId(lul_device) local therm_id = prefix .. id for k,v in pairs(luup.devices) do if luup.devices[lul_device].device_num_parent == v.device_num_parent and v.id == therm_id then return k end end end local function round(value, precision) return (value >= 0) and (math.floor(value * precision + 0.5) / precision) or (math.ceil(value * precision - 0.5) / precision) end local TemperaturePrecision = 1 -- convert thermostat format (F*10) to local format (C or F) local function localizeTemp(temperature) temperature = temperature or -5002 return (veraTemperatureScale == "F") and round(temperature/10, TemperaturePrecision) or round(((temperature/10 + 0.0) - 32.0) / 1.8, TemperaturePrecision) end -- convert local format (C or F) to thermostat format (F*10) local function delocalizeTemp(temperature) return (veraTemperatureScale == "F") and temperature*10 or round((((temperature + 0.0) * 1.8) + 32)*10, 1) end -- convert "2013-02-20 23:23:44" to number of seconds since 1/1/1970 local function toSeconds(dateString, useLocal) useLocal = useLocal or false local year, month, day, hour, min, sec = string.match(dateString, "(%d+)%-(%d+)%-(%d+) (%d+):(%d+):(%d+)") local offset = useLocal and 0 or (os.time() - os.time(os.date("!*t"))) return os.time{year=year, month=month, day=day, hour=hour, min=min, sec=sec} + offset end -- find the event that is considered the current event and return it local function getCurrentEvent(events) if not events then return nil end for i=1,#events do if events[i].running then return events[i] end end return nil end -- the plugin defines the current climate as either the climate used for the current hold, -- or if there is no current hold, the current program's climate. If there is a current hold -- but it's not based on a climate, then the current climate is the empty string "" local function getCurrentClimateRef(t) local climate = t.program and t.program.currentClimateRef local event = getCurrentEvent(t.events) if event then climate = event.holdClimateRef end return climate or "" end -- "Home" is defined as there being an active hold event with a holdClimateRef of "home", OR -- there being no current hold event but the current program's climate ref is "home" local function isHome(t) return getCurrentClimateRef(t) == "home" or getCurrentClimateRef(t) == "sleep" or (t.events and #t.events > 0 and t.events[1]["type"] == "switchOccupancy" and t.events[1].name == "occupied") end -- convert ecobee values to UPnP values local ECOBEE_TO_UPNP = { [ECOBEE_SID] = { ["thermostatRev"] = function(r) return r.thermostatRev end, ["runtimeRev"] = function(r) return r.runtimeRev end, ["equipmentStatus"] = function(r) return r.equipmentStatus or "unknown" end, ["quickSaveSetBack"] = function(t) return tostring(t.settings.quickSaveSetBack) end, ["quickSaveSetForward"] = function(t) return tostring(t.settings.quickSaveSetForward) end, ["holdType"] = function(t) return "indefinite" end, -- default value on device creation ["currentEventType"] = function(t) local event = getCurrentEvent(t.events) return (event and event["type"]) and event["type"] or "none" end, ["currentClimateRef"] = function(t) return getCurrentClimateRef(t) end, ["StreetAddress"] = function(t) return t.location.streetAddress end, ["City"] = function(t) return t.location.city end, ["ProvinceState"] = function(t) return t.location.provinceState end, ["Country"] = function(t) return t.location.country end, ["PostalCode"] = function(t) return t.location.postalCode end, ["PhoneNumber"] = function(t) return t.location.phoneNumber end, ["MapCoordinates"] = function(t) return t.location.mapCoordinates end, ["HumidityModeState"] = function(t) -- return "Humidifying", "Dehumidifying", "Idle" from list of humidifier, dehumidifier if (string.find(t.equipmentStatus, "dehumid")) then return "Dehumidifying" elseif (string.find(t.equipmentStatus, "humid")) then return "Humidifying" else return "Idle" end end }, [SWITCH_POWER_SID] = { ["Status"] = function(t) return isHome(t) and "1" or "0" end }, [TEMP_SENSOR_SID] = { ["Application"] = function() return "Room" end, ["CurrentTemperature"] = function(t, cap) return (not cap) and tostring(localizeTemp(t.runtime.actualTemperature)) or tostring(localizeTemp(tonumber(cap.value))) end }, [TEMP_SETPOINT_HEAT_SID] = { ["Application"] = function() return "Heating" end, ["CurrentSetpoint"] = function(t) return tostring(localizeTemp(t.runtime.desiredHeat)) end }, [TEMP_SETPOINT_COOL_SID] = { ["Application"] = function() return "Cooling" end, ["CurrentSetpoint"] = function(t) return tostring(localizeTemp(t.runtime.desiredCool)) end }, [TEMP_SETPOINT_SID] = { ["Application"] = function() return "DualHeatingCooling" end, ["CurrentSetpoint"] = function(t) local desiredTemp = (t.settings.hvacMode == "heat") and t.runtime.desiredHeat or ((t.settings.hvacMode == "cool") and t.runtime.desiredCool or ((t.runtime.desiredHeat + t.runtime.desiredCool) / 2)) return tostring(localizeTemp(desiredTemp)) end }, [HUMIDITY_SENSOR_SID] = { ["CurrentLevel"] = function(t, cap) return (not cap) and tostring(t.runtime.actualHumidity) or (cap.value == "unknown" and "0" or cap.value) end }, [HVAC_FAN_SID] = { ["Mode"] = function(t) local fan = (t.events and #t.events > 0) and t.events[1].fan or nil -- TODO: add in "and t.events[1].running" above once it can be relied on -- if there is no current event, inspect the current climate if not fan and t.settings and t.program and t.program.climates then for i,v in ipairs(t.program.climates) do if v.climateRef == t.program.currentClimateRef then fan = t.settings.hvacMode == "cool" and v.coolFan or v.heatFan break end end end if fan == "auto" then return "Auto" elseif fan == "on" then return "ContinuousOn" else log("Unknown fan '" .. tostring(fan) .. "'.") return "Unknown" end end, ["FanStatus"] = function(t) return t.equipmentStatus and (string.find(t.equipmentStatus, "fan") and "On" or "Off") or "Unknown" end }, [HVAC_USER_SID] = { ["ModeStatus"] = function(t) if (t.settings.hvacMode == "heat") then return "HeatOn" elseif (t.settings.hvacMode == "cool") then return "CoolOn" elseif (t.settings.hvacMode == "auto") then return "AutoChangeOver" elseif (t.settings.hvacMode == "off") then return "Off" else return "InDeadBand" end end }, [HVAC_STATE_SID] = { ["ModeState"] = function(t) -- return "Heating", "Cooling", "FanOnly", "Idle", "PendingHeat", "PendingCool", "Vent" -- from list of heatPump, compCool1, compCool2, auxHeat1, auxHeat2, auxHeat3, fan, -- humidifier, dehumidifier, ventilator, economizer, compHotWater, auxHotWater if (t.equipmentStatus == "") then return "Idle" elseif (t.equipmentStatus == "fan") then return "FanOnly" elseif (string.find(t.equipmentStatus, "eat")) then return "Heating" elseif (string.find(t.equipmentStatus, "ool")) then return "Cooling" elseif (string.find(t.equipmentStatus, "vent")) then return "Vent" elseif (t.settings.hvacMode == "off") then return "Off" else return "" end end }, [HA_DEVICE_SID] = { ["LastUpdate"] = function(t) return tostring(toSeconds(t.runtime.lastModified)) end, ["CommFailure"] = function(r,cap) return (not cap) and (r.connected and "0" or "1") or (cap.value == "unknown" and "1" or "0") end, ["Commands"] = function(t) local commands = { "hvac_off", "hvac_auto", "hvac_cool", "hvac_heat", "fan_auto", "fan_on", "hvac_state", "resume_program" } if t.runtime.desiredHeat ~= ecobee.HEAT_OFF then commands[#commands + 1] = "heating_setpoint" end if t.runtime.desiredCool ~= ecobee.COOL_OFF then commands[#commands + 1] = "cooling_setpoint" end return table.concat(commands, ",") end }, [MCV_ENERGY_METERING_SID] = { ["UserSuppliedWattage"] = function(t) return "0,0,0" end }, [SECURITY_SENSOR_SID] = { ["Tripped"] = function(t,cap) return (cap and cap.value == "true") and "1" or "0" end, ["LastTrip"] = function(t, cap) return "0" end, ["Armed"] = function(t, cap) return "0" end } } local function ecobeeToUpnp(serviceId, variableName, ...) return ECOBEE_TO_UPNP[serviceId][variableName](...) end local function ecobeeToUpnpParam(serviceId, variableName, ...) return serviceId .. "," .. variableName .. "=" .. ecobeeToUpnp(serviceId, variableName, ...) end local function writeVariableFromEcobee(lul_device, serviceId, name, ...) writeVariable(lul_device, serviceId, name, ecobeeToUpnp(serviceId, name, ...)) end local function writeVariableFromEcobeeIfChanged(lul_device, serviceId, name, ...) return writeVariableIfChanged(lul_device, serviceId, name, ecobeeToUpnp(serviceId, name, ...)) end -- convert UPnP values to ecobee values local UPNP_TO_ECOBEE = { [TEMP_SETPOINT_HEAT_SID] = { ["SetCurrentSetpoint"] = { ["NewCurrentSetpoint"] = function(lul_settings) return delocalizeTemp(lul_settings.NewCurrentSetpoint) end } }, [TEMP_SETPOINT_COOL_SID] = { ["SetCurrentSetpoint"] = { ["NewCurrentSetpoint"] = function(lul_settings) return delocalizeTemp(lul_settings.NewCurrentSetpoint) end } }, [TEMP_SETPOINT_SID] = { ["SetCurrentSetpoint"] = { ["NewCurrentSetpoint"] = function(lul_settings) return delocalizeTemp(lul_settings.NewCurrentSetpoint) end } }, [HVAC_FAN_SID] = { ["SetMode"] = { ["NewMode"] = function(lul_settings) if (lul_settings.NewMode == "ContinuousOn") then return "on" elseif (lul_settings.NewMode == "Auto") then return "auto" end end } }, [HVAC_USER_SID] = { ["SetModeTarget"] = { ["NewModeTarget"] = function(lul_settings) if (lul_settings.NewModeTarget == "HeatOn") then return "heat" elseif (lul_settings.NewModeTarget == "CoolOn") then return "cool" elseif (lul_settings.NewModeTarget == "AutoChangeOver") then return "auto" elseif (lul_settings.NewModeTarget == "Off") then return "off" elseif (lul_settings.NewModeTarget == "AuxHeatOn") then return "auxHeatOnly" end end } } } local function upnpToEcobee(serviceId, actionName, variableName, lul_settings) return UPNP_TO_ECOBEE[serviceId][actionName][variableName](lul_settings) end local auth_token_failures = 0 local function loadSession() local session = {} -- Config variables session.poll = tonumber(readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "poll", tostring(DEFAULT_POLL))) session.poll = session.poll or DEFAULT_POLL session.poll = (session.poll < MIN_POLL) and MIN_POLL or session.poll session.selectionType = readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "selectionType", "registered") session.selectionMatch = readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "selectionMatch", "") session.scope = readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "scope", "smartWrite") -- Session variables session.auth_token = luup.variable_get(ECOBEE_SID, "auth_token", PARENT_DEVICE) if session.auth_token == "" then session.auth_token = nil end session.auth_token_failures = auth_token_failures session.access_token = luup.variable_get(ECOBEE_SID, "access_token", PARENT_DEVICE) if session.access_token == "" then session.access_token = nil end session.token_type = luup.variable_get(ECOBEE_SID, "token_type", PARENT_DEVICE) if session.token_type == "" then session.token_type = nil end session.refresh_token = luup.variable_get(ECOBEE_SID, "refresh_token", PARENT_DEVICE) if session.refresh_token == "" then session.refresh_token = nil end return session end local function saveSession(session) if session.error then log("Error: " .. tostring(session.error) .. ": " .. tostring(session.error_description)) end writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "auth_token", session.auth_token or "") auth_token_failures = session.auth_token_failures or 0 writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "access_token", session.access_token or "") writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "token_type", session.token_type or "") writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "refresh_token", session.refresh_token or "") local status = (session.access_token and session.access_token ~= "" and not session.error) writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "status", status and "1" or "0") end -- wrapper ecobee API calls so saveSession is called after each API call local function getPin(session) local pin = ecobee.getPin(session) saveSession(session) return pin end local function getTokens(session) local access_token, token_type, refresh_token, scope = ecobee.getTokens(session) saveSession(session) return access_token, token_type, refresh_token, scope end local function getThermostatSummary(session, thermostatSummaryOptions) local revisions = ecobee.getThermostatSummary(session, thermostatSummaryOptions) saveSession(session) return revisions end local function getThermostats(session, thermostatsOptions) local thermostats = ecobee.getThermostats(session, thermostatsOptions) saveSession(session) return thermostats end local function updateThermostats(session, thermostatsUpdateOptions) local success = ecobee.updateThermostats(session, thermostatsUpdateOptions) saveSession(session) return success end local function getSelection(session, lul_device) return lul_device == PARENT_DEVICE and { selectionType = session.selectionType, selectionMatch = session.selectionMatch } or { selectionType = "thermostats", selectionMatch = getThermostatId(lul_device) } end local statusOutstanding = false local function getStatusSoon() if not statusOutstanding then statusOutstanding = true debug("Scheduling status poll in " .. SOON .. " seconds.") luup.call_timer("getStatus", 1, SOON, "", "") end end -- get status from ecobee function getStatus() -- debug("in getStatus()") statusOutstanding = false local session = loadSession() if not session.auth_token then task("Not yet authorized. Press 'Get PIN' once; wait for PIN; enter at ecobee.com.") else if not session.refresh_token then debug("About to getTokens...") getTokens(session) end if not session.refresh_token then log("Skipping this status update due to previous errors.") return end debug("Fetching revisions...") local revisions = getThermostatSummary(session, ecobee.selectionObject(session.selectionType, session.selectionMatch, { equipmentStatus = true })) if not revisions then log("Unable to getThermostatSummary; skipping status update.") return end local count = 0 for k,v in pairs(revisions) do count = count + 1 end writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "DisplayLabel", "Thermostats") writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "DisplayValue", tostring(count)) -- first, see if Vera doesn't know the same thermostats, humidistats and houses as ecobee.com knows thermostats local newDevices = 0 for id,revision in pairs(revisions) do if not findChild(PARENT_DEVICE, THERM_ID_PREFIX .. id) then newDevices = newDevices + 1 end if not findChild(PARENT_DEVICE, HUMID_ID_PREFIX .. id) then newDevices = newDevices + 1 end if not findChild(PARENT_DEVICE, HOUSE_ID_PREFIX .. id) then newDevices = newDevices + 1 end end -- also check to see if Vera knows thermostats that are no longer reported by ecobee.com local oldDevices = 0 for device_num, v in pairs(luup.devices) do if v.device_num_parent == PARENT_DEVICE then if not revisions[getThermostatId(device_num)] then oldDevices = oldDevices + 1 end end end -- if the sets of devices are out of sync... if newDevices > 0 or oldDevices > 0 or syncDevices then syncDevices = false -- do a full thermostat fetch and create child devices log("Synchronizing devices with ecobee.com...") local includes = { settings=true, runtime=true, events=true, program=true, location=true, equipmentStatus=true, sensors=true } local options = ecobee.selectionObject(session.selectionType, session.selectionMatch, includes) local thermostats = getThermostats(session, options) if thermostats then local ptr = luup.chdev.start(PARENT_DEVICE) for id,t in pairs(thermostats) do local name = t.name == "" and id or t.name local r = revisions[id] local altid = THERM_ID_PREFIX .. id luup.chdev.append(PARENT_DEVICE, ptr, altid, name, "urn:schemas-upnp-org:device:HVAC_ZoneThermostat:1", "D_EcobeeThermostat1.xml", "", ecobeeToUpnpParam(ECOBEE_SID, "thermostatRev", r) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "runtimeRev", r) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "equipmentStatus", r) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "quickSaveSetBack", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "quickSaveSetForward", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "holdType", t) .. "\n" .. ecobeeToUpnpParam(TEMP_SENSOR_SID, "CurrentTemperature", t) .. "\n" .. ecobeeToUpnpParam(TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", t) .. "\n" .. ecobeeToUpnpParam(TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", t) .. "\n" .. ecobeeToUpnpParam(TEMP_SETPOINT_SID, "CurrentSetpoint", t) .. "\n" .. ecobeeToUpnpParam(HVAC_FAN_SID, "Mode", t) .. "\n" .. ecobeeToUpnpParam(HVAC_FAN_SID, "FanStatus", t) .. "\n" .. ecobeeToUpnpParam(HVAC_USER_SID, "ModeStatus", t) .. "\n" .. ecobeeToUpnpParam(HVAC_STATE_SID, "ModeState", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "Commands", t) .. "\n" .. ecobeeToUpnpParam(MCV_ENERGY_METERING_SID, "UserSuppliedWattage", t) , false, false) altid = HUMID_ID_PREFIX .. id luup.chdev.append(PARENT_DEVICE, ptr, altid, name, "urn:schemas-ecobee-com:device:EcobeeHumidistat:1", "D_EcobeeHumidistat1.xml", "", ecobeeToUpnpParam(HUMIDITY_SENSOR_SID, "CurrentLevel", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "HumidityModeState", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r) , false, false) altid = HOUSE_ID_PREFIX .. id luup.chdev.append(PARENT_DEVICE, ptr, altid, name, "urn:schemas-ecobee-com:device:EcobeeHouse:1", "D_EcobeeHouse1.xml", "", ecobeeToUpnpParam(SWITCH_POWER_SID, "Status", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "StreetAddress", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "City", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "ProvinceState", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "Country", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "PostalCode", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "PhoneNumber", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "MapCoordinates", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "currentEventType", t) .. "\n" .. ecobeeToUpnpParam(ECOBEE_SID, "currentClimateRef", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r) , false, false) -- loop through any remote sensors we received for this thermostat if t.remoteSensors then for irs,rs in pairs(t.remoteSensors) do for icap,cap in pairs(rs.capability) do altid = SENSOR_ID_PREFIX .. id .. "_" .. rs.id .. "_" .. cap.id if cap.type == "humidity" then luup.chdev.append(PARENT_DEVICE, ptr, altid, rs.name, "urn:schemas-micasaverde-com:device:HumiditySensor:1", "D_HumiditySensor1.xml", "", ecobeeToUpnpParam(HUMIDITY_SENSOR_SID, "CurrentLevel", t, cap) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r, cap) , false, false) elseif cap.type == "temperature" then luup.chdev.append(PARENT_DEVICE, ptr, altid, rs.name, "urn:schemas-micasaverde-com:device:TemperatureSensor:1", "D_TemperatureSensor1.xml", "", ecobeeToUpnpParam(TEMP_SENSOR_SID, "CurrentTemperature", t, cap) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r, cap) , false, false) elseif cap.type == "occupancy" then luup.chdev.append(PARENT_DEVICE, ptr, altid, rs.name, "urn:schemas-micasaverde-com:device:MotionSensor:1", "D_MotionSensor1.xml", "", ecobeeToUpnpParam(SECURITY_SENSOR_SID, "Tripped", t, cap) .. "\n" .. ecobeeToUpnpParam(SECURITY_SENSOR_SID, "LastTrip", t, cap) .. "\n" .. ecobeeToUpnpParam(SECURITY_SENSOR_SID, "Armed", t, cap) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "LastUpdate", t) .. "\n" .. ecobeeToUpnpParam(HA_DEVICE_SID, "CommFailure", r, cap) , false, false) end end end end end luup.chdev.sync(PARENT_DEVICE, ptr) log("Updated children device(s); awaiting restart...") return end else -- see which thermostats have changed settings or runtime values, equipment status or connected state local changed = {} for id,revision in pairs(revisions) do local child = findChild(PARENT_DEVICE, THERM_ID_PREFIX .. id) if not child then log("Failed to find device for thermostat " .. id) else if (revision.thermostatRev ~= luup.variable_get(ECOBEE_SID, "thermostatRev", child)) or (revision.runtimeRev ~= luup.variable_get(ECOBEE_SID, "runtimeRev", child)) or (revision.equipmentStatus ~= luup.variable_get(ECOBEE_SID, "equipmentStatus", child)) or (revision.connected ~= (luup.variable_get(HA_DEVICE_SID, "CommFailure", child) == "0")) then changed[#changed + 1] = id end end end debug(tostring(#changed) .. " update(s) found from ecobee.com.") if #changed > 0 then -- just fetch the changed thermostats from ecobee.com local includes = { settings=true, runtime=true, events=true, program=true, equipmentStatus=true, sensors=true } local thermostats = getThermostats(session, ecobee.selectionObject("thermostats", changed, includes)) if thermostats then for id,t in pairs(thermostats) do local r = revisions[id] local altid = THERM_ID_PREFIX .. id local child = findChild(PARENT_DEVICE, altid) if not child then log("failed to find device for thermostat " .. altid) else -- make sure this device has category_num=5 if luup.attr_get("category_num", child) ~= "5" then luup.attr_set("category_num", "5", child) end if luup.attr_get("manufacturer", child) ~= "ecobee" then luup.attr_set("manufacturer", "ecobee", child) end if luup.attr_get("model", child) ~= t.modelNumber then luup.attr_set("model", t.modelNumber, child) end writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "thermostatRev", r) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "runtimeRev", r) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "equipmentStatus", r) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "quickSaveSetBack", t) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "quickSaveSetForward", t) readVariableOrInit(child, ECOBEE_SID, "holdType", "indefinite") -- create device variable if doesn't exist writeVariableFromEcobeeIfChanged(child, TEMP_SENSOR_SID, "CurrentTemperature", t) writeVariableFromEcobeeIfChanged(child, TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", t) writeVariableFromEcobeeIfChanged(child, TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", t) writeVariableFromEcobeeIfChanged(child, TEMP_SETPOINT_SID, "CurrentSetpoint", t) writeVariableFromEcobeeIfChanged(child, HVAC_FAN_SID, "Mode", t) writeVariableFromEcobeeIfChanged(child, HVAC_FAN_SID, "FanStatus", t) writeVariableFromEcobeeIfChanged(child, HVAC_USER_SID, "ModeStatus", t) writeVariableFromEcobeeIfChanged(child, HVAC_STATE_SID, "ModeState", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "Commands", t) end altid = HUMID_ID_PREFIX .. id child = findChild(PARENT_DEVICE, altid) if not child then log("failed to find device for humidistat " .. altid) else -- make sure this device has category_num=16 if luup.attr_get("category_num", child) ~= "16" then luup.attr_set("category_num", "16", child) end writeVariableFromEcobeeIfChanged(child, HUMIDITY_SENSOR_SID, "CurrentLevel", t) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "HumidityModeState", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r) end altid = HOUSE_ID_PREFIX .. id child = findChild(PARENT_DEVICE, altid) if not child then log("failed to find device for house " .. altid) else -- make sure this device has category_num=3 if luup.attr_get("category_num", child) ~= "3" then luup.attr_set("category_num", "3", child) end writeVariableFromEcobeeIfChanged(child, SWITCH_POWER_SID, "Status", t) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "currentEventType", t) writeVariableFromEcobeeIfChanged(child, ECOBEE_SID, "currentClimateRef", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r) end -- loop through any remote sensors we received for this thermostat if t.remoteSensors then for irs,rs in pairs(t.remoteSensors) do for icap,cap in pairs(rs.capability) do altid = SENSOR_ID_PREFIX .. id .. "_" .. rs.id .. "_" .. cap.id local child = findChild(PARENT_DEVICE, altid) if not child then log("failed to find device for sensor " .. altid .. "; will sync on next poll") syncDevices = true else if cap.type == "humidity" then -- make sure this device has category_num=16 (Humidity Sensor) if luup.attr_get("category_num", child) ~= "16" then luup.attr_set("category_num", "16", child) end writeVariableFromEcobeeIfChanged(child, HUMIDITY_SENSOR_SID, "CurrentLevel", t, cap) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r, cap) elseif cap.type == "temperature" then -- make sure this device has category_num=17 (Temperature Sensor) if luup.attr_get("category_num", child) ~= "17" then luup.attr_set("category_num", "17", child) end writeVariableFromEcobeeIfChanged(child, TEMP_SENSOR_SID, "CurrentTemperature", t, cap) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r, cap) elseif cap.type == "occupancy" then -- make sure this device has category_num=4 (Security Sensor) if luup.attr_get("category_num", child) ~= "4" then luup.attr_set("category_num", "4", child) end -- make sure this device has subcategory_num=3 (Motion Sensor) if (luup.attr_get("subcategory_num", child) ~= "3") then luup.attr_set("subcategory_num", "3", child) end -- set LastTrip to now if Tripped is transitioning from "0" to "1" local newTripped = ecobeeToUpnp(SECURITY_SENSOR_SID, "Tripped", t, cap) if newTripped == "1" and luup.variable_get(SECURITY_SENSOR_SID, "Tripped", child) ~= "1" then writeVariableIfChanged(child, SECURITY_SENSOR_SID, "LastTrip", tostring(os.time())) end writeVariableIfChanged(child, SECURITY_SENSOR_SID, "Tripped", newTripped) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "LastUpdate", t) writeVariableFromEcobeeIfChanged(child, HA_DEVICE_SID, "CommFailure", r, cap) end end end end end -- if t.remoteSensors end -- for end -- if thermostats end -- if #changed > 0 end -- if new or old (out of sync) end -- session state end -- getStatus() --[[ Functions that change thermostat state ]]-- local function setHold(session, selection, lul_device, func) debug("in setHold()") lul_device = findSibling(lul_device) if not lul_device then task("Unable to find sibling thermostat device; aborting setHold.") return false end -- determine which type of hold to set func.params.holdType = readVariableOrInit(lul_device, ECOBEE_SID, "holdType", "indefinite") -- if selection is nil, we will make our own. selection = selection or getSelection(session, lul_device) if not func.params.holdClimateRef then -- if func.params.coolHoldTemp, func.params.heatHoldTemp or func.params.fan are nil, -- use the current device's state to supply the missing values. if not func.params.heatHoldTemp then local heatSetpoint = luup.variable_get(TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", lul_device) func.params.heatHoldTemp = heatSetpoint and upnpToEcobee(TEMP_SETPOINT_HEAT_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", { NewCurrentSetpoint = heatSetpoint }) or ecobee.HEAT_OFF end if not func.params.coolHoldTemp then local coolSetpoint = luup.variable_get(TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", lul_device) func.params.coolHoldTemp = coolSetpoint and upnpToEcobee(TEMP_SETPOINT_COOL_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", { NewCurrentSetpoint = coolSetpoint }) or ecobee.COOL_OFF end if not func.params.fan then local fan = luup.variable_get(HVAC_FAN_SID, "Mode", lul_device) func.params.fan = fan and upnpToEcobee(HVAC_FAN_SID, "SetMode", "NewMode", { NewMode=fan }) or "auto" end end local success = updateThermostats(session, ecobee.thermostatsUpdateOptions(selection, { func })) if success then getStatusSoon() end return success end local function setClimateHold(session, selection, lul_device, holdClimateRef) local func = ecobee.setHoldFunction() func.params.holdClimateRef = holdClimateRef return setHold(session, selection, lul_device, func) end -- "away" function for binary switch device against Si thermostats -- want to use a real quickSave event but must use this for now. local function quickSave(session, selection, lul_device) local heatSetpoint = luup.variable_get(TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", lul_device) heatSetpoint = tonumber(heatSetpoint) local heatHoldTemp = (not heatSetpoint) and ecobee.HEAT_OFF or upnpToEcobee(TEMP_SETPOINT_HEAT_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", { NewCurrentSetpoint = heatSetpoint }) local coolSetpoint = luup.variable_get(TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", lul_device) coolSetpoint = tonumber(coolSetpoint) local coolHoldTemp = (not coolSetpoint) and ecobee.COOL_OFF or upnpToEcobee(TEMP_SETPOINT_COOL_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", { NewCurrentSetpoint = coolSetpoint }) local quickSaveSetBack = luup.variable_get(ECOBEE_SID, "quickSaveSetBack", lul_device) quickSaveSetBack = tonumber(quickSaveSetBack) or 40 -- TODO local quickSaveSetForward = luup.variable_get(ECOBEE_SID, "quickSaveSetForward", lul_device) quickSaveSetForward = tonumber(quickSaveSetForward) or 40 -- TODO local func = ecobee.setHoldFunction() func.params.coolRelativeTemp = quickSaveSetForward func.params.heatRelativeTemp = quickSaveSetBack func.params.isTemperatureRelative = false func.params.coolHoldTemp = (coolHoldTemp == ecobee.COOL_OFF) and coolHoldTemp or coolHoldTemp + quickSaveSetForward func.params.heatHoldTemp = (heatHoldTemp == ecobee.HEAT_OFF) and heatHoldTemp or heatHoldTemp - quickSaveSetBack func.params.isTemperatureAbsolute = true return setHold(session, selection, lul_device, func) end -- "away" function for binary switch device against EMS thermostats local function setOccupied(session, selection, lul_device, occupied) local success = updateThermostats(session, ecobee.thermostatsUpdateOptions(selection, { ecobee.setOccupiedFunction(occupied, "indefinite") })) if success then getStatusSoon() end return success end -- "home" function for all thermostats local function resumeProgram(session, selection, calls) calls = calls or 1 local functions = {} for i = 1,calls do functions[#functions + 1] = ecobee.resumeProgramFunction() end local success = updateThermostats(session, ecobee.thermostatsUpdateOptions(selection, functions)) if success then getStatusSoon() end return success end local function isEmsThermostat(lul_device) local model = luup.attr_get("model", lul_device) return model and string.find(model, "Ems") ~= nil end -- setAway will set the thermostat into "away" mode. -- For non-EMS thermostats, Away -> setClimateHold to "home" or "away" -- For EMS thermostats, Away -> setOccupied occupied=false; and Home -> resumeProgram x3. -- The home/away state is calculated based on whether one of these holds is the currently -- running event. local function setAway(session, lul_device, away) debug("in setAway()") lul_device = findSibling(lul_device) if not lul_device then task("Unable to find sibling thermostat device; aborting setAway.") return false end local selection = getSelection(session, lul_device) if isEmsThermostat(lul_device) then return away and setOccupied(session, selection, lul_device, false) or resumeProgram(session, selection, 3) else return setClimateHold(session, selection, lul_device, away and "away" or "home") end end function poll_ecobee() -- debug("in poll_ecobee()") task("Clearing...", TASK_SUCCESS) getStatus() -- set up the next poll local poll = tonumber(readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "poll", tostring(DEFAULT_POLL))) or DEFAULT_POLL if (poll < MIN_POLL) then poll = MIN_POLL end poll = tostring(poll) writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "poll", poll) debug("polling device " .. PARENT_DEVICE .. " again in " .. poll .. " seconds") luup.call_timer("poll_ecobee", 1, poll, "", "") end function init(lul_device) log("plugin version " .. PLUGIN_VERSION .. " starting up...", 50) PARENT_DEVICE = lul_device TemperaturePrecision = tonumber(readVariableOrInit(PARENT_DEVICE, ECOBEE_SID, "TemperaturePrecision", "1")) TemperaturePrecision = TemperaturePrecision or 1 if TemperaturePrecision < 1 or TemperaturePrecision > 1000 then TemperaturePrecision = 1 end getVeraTemperatureScale() -- perform the first poll 5-10 seconds from now local soon = tonumber(SOON) soon = soon + math.random(0,soon) debug("polling ecobee.com " .. PARENT_DEVICE .. " in " .. soon .. " seconds") luup.call_timer("poll_ecobee", 1, tostring(soon), "", "") end init urn:ecobee-com:serviceId:Ecobee1 GetPin local session = loadSession() debug("Attempting to getPin...") local ecobeePin = getPin(session) if ecobeePin then task("Register at ecobee.com now: " .. ecobeePin) writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "DisplayLabel", "My Apps PIN:") writeVariableIfChanged(PARENT_DEVICE, ECOBEE_SID, "DisplayValue", ecobeePin) end urn:ecobee-com:serviceId:Ecobee1 SetStatus luup.variable_set(ECOBEE_SID, "status", lul_settings.status or "0", lul_device) urn:ecobee-com:serviceId:Ecobee1 GetStatus return luup.variable_get(ECOBEE_SID, "status", lul_device) urn:upnp-org:serviceId:SwitchPower1 GetStatus return luup.variable_get(SWITCH_POWER_SID, "Status", lul_device) urn:upnp-org:serviceId:SwitchPower1 SetTarget local away if (lul_settings.newTargetValue == "1") then away = false elseif (lul_settings.newTargetValue == "0") then away = true else log("SetTarget received invalid arg: " .. tostring(lul_settings.newTargetValue)) return end if not setAway(loadSession(), lul_device, away) then task("Failed to send away command.") end urn:upnp-org:serviceId:SwitchPower1 GetTarget return luup.variable_get(SWITCH_POWER_SID, "Status", lul_device) urn:upnp-org:serviceId:TemperatureSensor1 GetApplication return ecobeeToUpnp(TEMP_SENSOR_SID, "Application") urn:upnp-org:serviceId:TemperatureSensor1 SetApplication -- no point yet that I know urn:upnp-org:serviceId:TemperatureSensor1 GetCurrentTemperature return luup.variable_get(TEMP_SENSOR_SID, "CurrentTemperature", lul_device) urn:upnp-org:serviceId:TemperatureSetpoint1_Heat GetApplication return ecobeeToUpnp(TEMP_SETPOINT_HEAT_SID, "Application") urn:upnp-org:serviceId:TemperatureSetpoint1_Heat SetCurrentSetpoint local heatHoldTemp = upnpToEcobee(TEMP_SETPOINT_HEAT_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", lul_settings) local session = loadSession() local func = ecobee.setHoldFunction(nil, heatHoldTemp) if not setHold(session, nil, lul_device, func) then task("Failed to set heat temperature setpoint.") end urn:upnp-org:serviceId:TemperatureSetpoint1_Heat GetCurrentSetpoint return luup.variable_get(TEMP_SETPOINT_HEAT_SID, "CurrentSetpoint", lul_device) urn:upnp-org:serviceId:TemperatureSetpoint1_Cool GetApplication return ecobeeToUpnp(TEMP_SETPOINT_COOL_SID, "Application") urn:upnp-org:serviceId:TemperatureSetpoint1_Cool SetCurrentSetpoint local coolHoldTemp = upnpToEcobee(TEMP_SETPOINT_COOL_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", lul_settings) local session = loadSession() local func = ecobee.setHoldFunction(coolHoldTemp, nil) if not setHold(session, nil, lul_device, func) then task("Failed to set cool temperature setpoint.") end urn:upnp-org:serviceId:TemperatureSetpoint1_Cool GetCurrentSetpoint return luup.variable_get(TEMP_SETPOINT_COOL_SID, "CurrentSetpoint", lul_device) urn:upnp-org:serviceId:TemperatureSetpoint1 GetApplication return ecobeeToUpnp(TEMP_SETPOINT_SID, "Application") urn:upnp-org:serviceId:TemperatureSetpoint1 SetCurrentSetpoint local holdTemp = upnpToEcobee(TEMP_SETPOINT_SID, "SetCurrentSetpoint", "NewCurrentSetpoint", lul_settings) local modeStatus = luup.variable_get(HVAC_USER_SID, "ModeStatus", lul_device) or "AutoChangeOver" local heatHoldTemp = (modeStatus ~= "CoolOn") and holdTemp or nil local coolHoldTemp = (modeStatus ~= "HeatOn") and holdTemp or nil local session = loadSession() local func = ecobee.setHoldFunction(coolHoldTemp, heatHoldTemp) if not setHold(session, nil, lul_device, func) then task("Failed to set temperature setpoint.") end urn:upnp-org:serviceId:TemperatureSetpoint1 GetCurrentSetpoint return luup.variable_get(TEMP_SETPOINT_SID, "CurrentSetpoint", lul_device) urn:upnp-org:serviceId:HVAC_UserOperatingMode1 SetModeTarget -- tell the ecobee to switch to lul_settings.NewModeTarget local hvacMode = upnpToEcobee(HVAC_USER_SID, "SetModeTarget", "NewModeTarget", lul_settings) local session = loadSession() local selection = getSelection(session, lul_device) if hvacMode and updateThermostats(session, ecobee.thermostatsUpdateOptions(selection, nil, { settings = { hvacMode = hvacMode } })) then writeVariableIfChanged(lul_device, HVAC_USER_SID, "ModeTarget", lul_settings.NewModeTarget) getStatusSoon() else task("Failed to set new hvacMode to " .. tostring(hvacMode)) end urn:upnp-org:serviceId:HVAC_UserOperatingMode1 GetModeTarget return luup.variable_get(HVAC_USER_SID, "ModeTarget", lul_device) or "AutoChangeOver" urn:upnp-org:serviceId:HVAC_UserOperatingMode1 GetModeStatus return luup.variable_get(HVAC_USER_SID, "ModeStatus", lul_device) urn:upnp-org:serviceId:HVAC_FanOperatingMode1 SetMode local session = loadSession() local func = ecobee.setHoldFunction() func.params.fan = upnpToEcobee(HVAC_FAN_SID, "SetMode", "NewMode", lul_settings) if not setHold(session, nil, lul_device, func) then task("Failed to set fan mode to " .. tostring(mode)) end urn:upnp-org:serviceId:HVAC_FanOperatingMode1 GetMode return luup.variable_get(HVAC_FAN_SID, "Mode", lul_device) or "Auto" urn:upnp-org:serviceId:HVAC_FanOperatingMode1 GetFanStatus return luup.variable_get(HVAC_FAN_SID, "FanStatus", lul_device) or "Unknown" urn:ecobee-com:serviceId:Ecobee1 ResumeProgram local session = loadSession() return resumeProgram(session, getSelection(session, lul_device), 3) urn:ecobee-com:serviceId:Ecobee1 SendMessage local session = loadSession() local selection = getSelection(session, lul_device) local functions = { ecobee.sendMessageFunction(lul_settings.MessageText) } return updateThermostats(session, ecobee.thermostatsUpdateOptions(selection, functions)) urn:ecobee-com:serviceId:Ecobee1 SetClimateHold local session = loadSession() local selection = getSelection(session, lul_device) return setClimateHold(session, selection, lul_device, lul_settings.HoldClimateRef) urn:micasaverde-com:serviceId:SecuritySensor1 SetArmed luup.variable_set(SECURITY_SENSOR_SID, "Armed", lul_settings.newArmedValue, lul_device)