diff --git a/devicetypes/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy b/devicetypes/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy new file mode 100644 index 00000000000..5cfcffef195 --- /dev/null +++ b/devicetypes/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy @@ -0,0 +1,104 @@ +/** + * EnerTalk Energy Meter + * + * Copyright 2015 hyeon seok yang + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") { + } + + simulator { + // TODO: define status and reply messages here + } + + tiles(scale:2) { + valueTile("view", "device.view", decoration: "flat") { + state "view", label:' ${currentValue} kWh' + } + valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") { + state "month", label:' ${currentValue}' + } + valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") { + state "real", label:' ${currentValue}' + } + valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") { + state "tier", label:' ${currentValue}' + } + valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") { + state "plan", label:' ${currentValue}' + } + + htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com", + "ajax.googleapis.com", + "fonts.googleapis.com", + "code.highcharts.com", + "enertalk-card.encoredtech.com", + "s3-ap-northeast-1.amazonaws.com", + "s3.amazonaws.com", + "ui-hub.encoredtech.com", + "enertalk-auth.encoredtech.com", + "api.encoredtech.com", + "cdnjs.cloudflare.com", + "encoredtech.com", + "itunes.apple.com"], width:2, height:2){} + + main (["view"]) + details (["month", "real", "tier", "plan", "deepLink"]) + } +} + +mappings { + + path("/linkApp") {action: [ GET: "getLinkedApp" ]} +} + +def getLinkedApp() { + def lang = clientLocale?.language + if ("${lang}" == "ko") { + lang = "

기기 설정

" + } else { + lang = "

Setup Device

" + } + renderHTML() { + head { + """ + + + """ + } + body { + """ +
+ + + + ${lang} +
+ + + """ + } + } +} \ No newline at end of file diff --git a/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy b/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy new file mode 100644 index 00000000000..84e6e3ae6eb --- /dev/null +++ b/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy @@ -0,0 +1,1388 @@ +/** + * ProtoType Smart Energy Service + * + * Copyright 2015 hyeon seok yang + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +definition( + name: "Smart Energy Service", + namespace: "Encored Technologies", + author: "hyeon seok yang", + description: "With visible realtime energy usage status, have good energy habits and enrich your life\r\n", + category: "SmartThings Labs", + iconUrl: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%401.png", + iconX2Url: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%402x", + iconX3Url: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%403x", + oauth: true) +{ + appSetting "clientId" + appSetting "clientSecret" + appSetting "callback" +} + + +preferences { + page(name: "checkAccessToken") +} + +cards { + card(name: "Encored Energy Service", type: "html", action: "getHtml", whitelist: whiteList()) {} +} + +/* This list contains, that url need to be allowed in Smart Energy Service.*/ +def whiteList() { + [ + "code.jquery.com", + "ajax.googleapis.com", + "fonts.googleapis.com", + "code.highcharts.com", + "enertalk-card.encoredtech.com", + "s3-ap-northeast-1.amazonaws.com", + "s3.amazonaws.com", + "ui-hub.encoredtech.com", + "enertalk-auth.encoredtech.com", + "api.encoredtech.com", + "cdnjs.cloudflare.com", + "encoredtech.com", + "itunes.apple.com" + ] +} + +/* url endpoints */ +mappings { + path("/requestCode") { action: [ GET: "requestCode" ] } + path("/receiveToken") { action: [ GET: "receiveToken"] } + path("/getHtml") { action: [GET: "getHtml"] } + path("/consoleLog") { action: [POST: "consoleLog"]} + path("/getInitialData") { action: [GET: "getInitialData"]} + path("/getEncoredPush") { action: [POST: "getEncoredPush"]} +} + + +/* This method does two things depends on the existence of Encored access token. : +* 1. If Encored access token does not exits, it starts the process of getting access token. +* 2. If Encored access token does exist, it will show a list of configurations, that user need to define values. +*/ +def checkAccessToken() { + log.debug "Staring the installation" + + /* Choose the level */ + atomicState.env_mode ="prod" + + def lang = clientLocale?.language + + /* getting language settings of user's device. */ + if ("${lang}" == "ko") { + atomicState.language = "ko" + } else { + atomicState.language = "en" + } + + /* create tanslation for descriptive and informative strings that can be seen by users. */ + if (!state.languageString) { + createLocaleStrings() + } + + if (!atomicState.encoredAccessToken) { /*check if Encored access token does exist.*/ + + log.debug "Encored Access Token does not exist." + + if (!state.accessToken) { /*if smartThings' access token does not exitst*/ + log.debug "SmartThings Access Token does not exist." + + createAccessToken() /*request and get access token from smartThings*/ + + /* re-create strings to make sure it's been initialized. */ + //createLocaleStrings() + } + + def redirectUrl = buildRedirectUrl("requestCode") /* build a redirect url with endpoint "requestCode"*/ + + /* These lines will start the OAuth process.\n*/ + log.debug "Start OAuth request." + return dynamicPage(name: "checkAccessToken", nextPage:null, uninstall: true, install:false) { + section{ + paragraph state.languageString."${atomicState.language}".desc1 + href(title: state.languageString."${atomicState.language}".main, + description: state.languageString."${atomicState.language}".desc2, + required: true, + style:"embedded", + url: redirectUrl) + } + } + } else { + /* This part will load the configuration for this application */ + return dynamicPage(name:"checkAccessToken",install:true, uninstall : true) { + section(title:state.languageString."${atomicState.language}".title6) { + + /* A push alarm for this application */ + input( + type: "boolean", + name: "notification", + title: state.languageString."${atomicState.language}".title1, + required: false, + default: true, + multiple: false + ) + + /* A plan that user need to decide */ + input( + type: "number", + name: "energyPlan", + title: state.languageString."${atomicState.language}".title2, + description : state.languageString."${atomicState.language}".subTitle1, + defaultValue: state.languageString.energyPlan, + range: "1130..*", + submitOnChange: true, + required: true, + multiple: false + ) + + /* A displaying unit that user need to decide */ + input( + type: "enum", + name: "displayUnit", + title: state.languageString."${atomicState.language}".title3, + defaultValue : state.languageString."${atomicState.language}".defaultValues.default1, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".displayUnits + ) + + /* A metering date that user should know */ + input( + type: "enum", + name: "meteringDate", + title: state.languageString."${atomicState.language}".title4, + defaultValue: state.languageString."${atomicState.language}".defaultValues.default2, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".meteringDays + ) + + /* A contract type that user should know */ + input( + type: "enum", + name: "contractType", + title: state.languageString."${atomicState.language}".title5, + defaultValue: state.languageString."${atomicState.language}".defaultValues.default3, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".contractTypes) + } + + } + } +} + +def requestCode(){ + log.debug "In state of sending a request to Encored for OAuth code.\n" + + /* Make a parameter to request Encored for a OAuth code. */ + def oauthParams = + [ + response_type: "code", + scope: "remote", + client_id: "${appSettings.clientId}", + app_version: "web", + redirect_uri: buildRedirectUrl("receiveToken") + ] + + /* Request Encored a code. */ + redirect location: "https://enertalk-auth.encoredtech.com/authorization?${toQueryString(oauthParams)}" +} + +def receiveToken(){ + log.debug "Request Encored to swap code with Encored Aceess Token" + + /* Making a parameter to swap code with a token */ + def authorization = "Basic " + "${appSettings.clientId}:${appSettings.clientSecret}".bytes.encodeBase64() + def uri = "https://enertalk-auth.encoredtech.com/token" + def header = [Authorization: authorization, contentType: "application/json"] + def body = [grant_type: "authorization_code", code: params.code] + + log.debug "Swap code with a token" + def encoredTokenParams = makePostParams(uri, header, body) + + log.debug "API call to Encored to swap code with a token" + def encoredTokens = getHttpPostJson(encoredTokenParams) + + /* make a page to show people if the REST was successful or not. */ + if (encoredTokens) { + log.debug "Got Encored OAuth token\n" + atomicState.encoredRefreshToken = encoredTokens.refresh_token + atomicState.encoredAccessToken = encoredTokens.access_token + + success() + } else { + log.debug "Could not get Encored OAuth token\n" + fail() + } + +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + /* Make sure uuid is there. */ + getUUID() + + /* Check uuid and if it does not exist then don't update.*/ + if (!atomicState.notPaired) { + def theDay = 1 + + for(def i=1; i < 28; i++) { + + /* set user choosen option to apropriate value. */ + if (atomicState.language == "en") { + if ("${i}st day of the month" == settings.meteringDate || + "${i}nd day of the month" == settings.meteringDate || + "${i}rd day of the month" == settings.meteringDate || + "${i}th day of the month" == settings.meteringDate) { + + theDay = i + i = 28 + + } else if ("Rest of the month" == settings.meteringDate) { + theDay = 27 + i = 28 + } + } else { + + if (settings.meteringDate == "매월 ${i}일") { + + theDay = i + i = 28 + + } else if ("말일" == settings.meteringDate) { + theDay = 27 + i = 28 + } + } + + } + + /* Set choosen contract to apropriate variable. */ + def contract = 1 + if (settings.contractType == "High voltage" || settings.contractType == "주택용 고압") { + contract = 2 + + if (settings.energyPlan < 460) { + settings.energyPlan = 490 + } + } else { + + if (settings.energyPlan < 1130) { + settings.energyPlan = 1130 + } + } + + + /* convert bill to milliwatts */ + def changeToUsageParam = makeGetParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/bill/expectedUsage?bill=${settings.energyPlan}", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + def energyPlanUsage = getHttpGetJson(changeToUsageParam, 'CheckEnergyPlanUsage') + def epUsage = 0 + if (energyPlanUsage) { + epUsage = energyPlanUsage.usage + } + + /* update the the information depends on the option choosen */ + def configurationParam = makePostParams("${state.domains."${atomicState.env_mode}"}/1.2/me", + [Authorization : "Bearer ${atomicState.encoredAccessToken}"], + [contractType : contract, + meteringDay : theDay, + maxLimitUsage : epUsage]) + getHttpPutJson(configurationParam) + } + +} + +def initialize() { + log.debug "Initializing Application" + + def EATValidation = checkEncoreAccessTokenValidation() + + /* if token exist get user's device id, uuid */ + if (EATValidation) { + getUUID() + if (atomicState.uuid) { + + def pushParams = makePostParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/events/push", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"], + [type: "REST", regId:"${state.accessToken}__${app.id}"]) + getHttpPostJson(pushParams) + } + + } else { + log.warning "Ecored Access Token did not get refreshed!" + } + + + /* add device Type Handler */ + atomicState.dni = "EncoredDTH01" + def d = getChildDevice(atomicState.dni) + if(!d) { + log.debug "Creating Device Type Handler." + + d = addChildDevice("Encored Technologies", "EnerTalk Energy Meter", atomicState.dni, null, [name:"EnerTalk Energy Meter", label:name]) + + } else { + log.debug "Device already created" + } + + setSummary() +} + +def setSummary() { + + log.debug "in setSummary" + def text = "Successfully installed." + sendEvent(linkText:count.toString(), descriptionText: app.label, + eventType:"SOLUTION_SUMMARY", + name: "summary", + value: text, + data: [["icon":"indicator-dot-gray","iconColor":"#878787","value":text]], + displayed: false) +} + +// TODO: implement event handlers + +/* Check the validation of Encored Access Token (EAT) +* If it's not valid try refresh Access Token. +* If the token gets refreshed, it will refresh the value of Encored Access Token +* If it doesn't get refreshed, then it returns null +*/ +private checkEncoreAccessTokenValidation() { + /* make a parameter to check the validation of Encored access token */ + def verifyParam = makeGetParams("https://enertalk-auth.encoredtech.com/verify", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + /* check the validation */ + def verified = getHttpGetJson(verifyParam, 'verifyToken') + + log.debug "verified : ${verified}" + + /* if Encored Access Token need to be renewed. */ + if (!verified) { + try { + refreshAuthToken() + + /* Recheck the renewed Encored access token. */ + verifyParam.headers = [Authorization: "Bearer ${atomicState.encoredAccessToken}"] + verified = getHttpGetJson(verifyParam, 'CheckRefresh') + + } catch (groovyx.net.http.HttpResponseException e) { + /* If refreshing token raises an error */ + log.warn "Refresh Token Error : ${e}" + } + } + + return verified +} + +/* Get device UUID, if it does not exist, return false. true otherwise.*/ +private getUUID() { + atomicState.uuid = null + atomicState.notPaired = true + /* Make a parameter to get device id (uuid)*/ + def uuidParams = makeGetParams( "https://enertalk-auth.encoredtech.com/uuid", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + def deviceUUID = getHttpGetJson(uuidParams, 'UUID') + log.debug "device uuid is : ${deviceUUID}" + if (!deviceUUID) { + return false + } + log.debug "got here even tho" + atomicState.uuid = deviceUUID.uuid + atomicState.notPaired = false + return true +} + +private createLocaleStrings() { + state.domains = [ + test : "http://api.encoredtech.com", + prod : "https://api.encoredtech.com:8082/" + ] + state.languageString = + [ + energyPlan : 30000, + en : [ + desc1 : "Tab below to sign in or sign up to Encored EnerTalk smart energy service and authorize SmartThings access.", + desc2 : "Click to proceed authorization.", + main : "EnerTalk", + defaultValues : [ + default1 : "kWh", + default2 : "1st day of the month", + default3 : "Low voltage" + ], + meteringDays : [ + "1st day of the month", + "2nd day of the month", + "3rd day of the month", + "4th day of the month", + "5th day of the month", + "6th day of the month", + "7th day of the month", + "8th day of the month", + "9th day of the month", + "10th day of the month", + "11th day of the month", + "12th day of the month", + "13th day of the month", + "14th day of the month", + "15th day of the month", + "16th day of the month", + "17th day of the month", + "18th day of the month", + "19th day of the month", + "20st day of the month", + "21st day of the month", + "22nd day of the month", + "23rd day of the month", + "24th day of the month", + "25th day of the month", + "26th day of the month", + "Rest of the month" + ], + displayUnits : ["WON(₩)", "kWh"], + contractTypes : ["Low voltage", "High voltage"], + title1 : "Send push notification", + title2 : "Energy Plan", + subTitle1 : "Setup your energy plan by won", + title3 : "Display Unit", + title4 : "Metering Date", + title5 : "Contract Type", + title6 : "User & Notifications", + message1 : """

Your Encored Account is now connected to SmartThings!

Click 'Done' to finish setup.

""", + message2 : """

The connection could not be established!

Click 'Done' to return to the menu.

""", + message3 : [ + header : "Device is not installed", + body1 : "You need to install EnerTalk device at first,", + body2 : "and proceed setup and register device.", + button1 : "Setup device", + button2 : "Not Installed" + ], + message4 : [ + header : "Device is not connected.", + body1 : "Please check the Wi-Fi network connection", + body2 : "and EnerTalk device status.", + body3 : "Select ‘Setup Device’ to reset the device." + ] + ], + ko :[ + desc1 : "스마트 에너지 서비스를 이용하시려면 EnerTalk 서비스 가입과 SmartThings 접근 권한이 필요합니다.", + desc2 : "아래 버튼을 누르면 인증을 시작합니다", + main : "EnerTalk 인증", + defaultValues : [ + default1 : "kWh", + default2 : "매월 1일", + default3 : "주택용 저압" + ], + meteringDays : [ + "매월 1일", + "매월 2일", + "매월 3일", + "매월 4일", + "매월 5일", + "매월 6일", + "매월 7일", + "매월 8일", + "매월 9일", + "매월 10일", + "매월 11일", + "매월 12일", + "매월 13일", + "매월 14일", + "매월 15일", + "매월 16일", + "매월 17일", + "매월 18일", + "매월 19일", + "매월 20일", + "매월 21일", + "매월 22일", + "매월 23일", + "매월 24일", + "매월 25일", + "매월 26일", + "말일" + ], + displayUnits : ["원(₩)", "kWh"], + contractTypes : ["주택용 저압", "주택용 고압"], + title1 : "알람 설정", + title2 : "사용 계획 (원)", + subTitle1 : "월간 계획을 금액으로 입력하세요", + title3 : "표시 단위", + title4 : "정기검침일", + title5 : "계약종별", + title6 : "사용자 & 알람 설정", + message1 : """

EnerTalk 계정이 SmartThings와 연결 되었습니다!

Done을 눌러 계속 진행해 주세요.

""", + message2 : """

계정 연결이 실패했습니다.

Done 버튼을 눌러 다시 시도해주세요.

""", + message3 : [ + header : "기기 설치가 필요합니다.", + body1 : "가정 내 분전반에 EnerTalk 기기를 먼저 설치하고,", + body2 : "아래 버튼을 눌러 기기등록 및 연결을 진행하세요.", + button1 : "기기 설정", + button2 : "설치필요" + ], + message4 : [ + header : "Device is not connected.", + body1 : "Please check the Wi-Fi network connection", + body2 : "and EnerTalk device status.", + body3 : "Select ‘Setup Device’ to reset the device." + ] + + ] + ] + +} + +/* This method makes a redirect url with a given endpoint */ +private buildRedirectUrl(mappingPath) { + log.debug "Start : Starting to making a redirect URL with endpoint : /${mappingPath}" + def url = "https://graph.api.smartthings.com/api/token/${state.accessToken}/smartapps/installations/${app.id}/${mappingPath}" + log.debug "Done : Finished to make a URL : ${url}" + url +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +/* make a success message. */ +private success() { + def lang = clientLocale?.language + + if ("${lang}" == "ko") { + log.debug "I was here at first." + atomicState.language = "ko" + } else { + + atomicState.language = "en" + } + log.debug atomicState.language + def message = atomicState.languageString."${atomicState.language}".message1 + connectionStatus(message) +} + +/* make a failure message. */ +private fail() { + def lang = clientLocale?.language + + if ("${lang}" == "ko") { + log.debug "I was here at first." + atomicState.language = "ko" + } else { + + atomicState.language = "en" + } + def message = atomicState.languageString."${atomicState.language}".message2 + connectionStatus(message) +} + +private connectionStatus(message) { + def html = """ + + + + + + + SmartThings Connection + + + + +
+ Encored icon + connected device icon + SmartThings logo +

${message}

+ +
+ + + + """ + render contentType: 'text/html', data: html +} + +private refreshAuthToken() { + /*Refreshing Encored Access Token*/ + + log.debug "Refreshing Encored Access Token" + if(!atomicState.encoredRefreshToken) { + log.error "Encored Refresh Token does not exist!" + } else { + + def authorization = "Basic " + "${appSettings.clientId}:${appSettings.clientSecret}".bytes.encodeBase64() + def refreshParam = makePostParams("https://enertalk-auth.encoredtech.com/token", + [Authorization: authorization], + [grant_type: 'refresh_token', refresh_token: "${atomicState.encoredRefreshToken}"]) + + def newAccessToken = getHttpPostJson(refreshParam) + + if (newAccessToken) { + atomicState.encoredAccessToken = newAccessToken.access_token + log.debug "Successfully got new Encored Access Token.\n" + } else { + log.error "Was unable to renew Encored Access Token.\n" + } + } +} + +private getHttpPutJson(param) { + + log.debug "Put URI : ${param.uri}" + try { + httpPut(param) { resp -> + log.debug "HTTP Put Success" + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Put Error : ${e}" + } +} + +private getHttpPostJson(param) { + log.debug "Post URI : ${param.uri}" + def jsonMap = null + try { + httpPost(param) { resp -> + jsonMap = resp.data + log.debug resp.data + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Post Error : ${e}" + } + + return jsonMap +} + +private getHttpGetJson(param, testLog) { + log.debug "Get URI : ${param.uri}" + def jsonMap = null + try { + httpGet(param) { resp -> + jsonMap = resp.data + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Get Error : ${e}" + } + + return jsonMap + +} + +private makePostParams(uri, header, body=[]) { + return [ + uri : uri, + headers : header, + body : body + ] +} + +private makeGetParams(uri, headers, path="") { + return [ + uri : uri, + path : path, + headers : headers + ] +} + +def getInitialData() { + def lang = clientLocale?.language + if ("${lang}" == "ko") { + lang = "ko" + } else { + lang = "en" + } + atomicState.solutionModuleSettings.language = lang + atomicState.solutionModuleSettings +} + +def consoleLog() { + log.debug "console log: ${request.JSON.str}" +} + +def getHtml() { + + /* initializing variables */ + def deviceStatusData = "", standbyData = "", meData = "", meteringData = "", rankingData = "", lastMonth = "", deviceId = "" + def standby = "", plan = "", start = "", end = "", meteringDay = "", meteringUsage = "", percent = "", tier = "", meteringPeriodBill = "" + def maxLimitUsageBill, maxLimitUsage = 0 + def deviceStatus = false + def displayUnit = "watt" + + def meteringPeriodBillShow = "", meteringPeriodBillFalse = "collecting data" + def standbyShow = "", standbyFalse = "collecting data" + def rankingShow = "collecting data" + def tierShow = "collecting data" + def lastMonthShow = "", lastMonthFalse = "no records" + def planShow = "", planFalse = "set up plan" + + def thisMonthUnitOne ="", thisMonthUnitTwo = "", planUnitOne = "", planUnitTwo = "", lastMonthUnit = "", standbyUnit = "" + def thisMonthTitle = "This Month", tierTitle = "Billing Tier", planTitle = "Energy Goal", + lastMonthTitle = "Last Month", rankingTitle = "Ranking", standbyTitle = "Always on", energyMonitorDeviceTitle = "EnerTalk Device" , realtimeTitle = "Realtime" + def onOff = "OFF", rankImage = "", tierImage = "" + + def htmlBody = "" + + /* Get the language setting on device. */ + def lang = clientLocale?.language + if ("${lang}" == "ko") { + atomicState.language = "ko" + } else { + atomicState.language = "en" + } + + if (atomicState.language == "ko") { + rankingShow = "데이터 수집 중" + meteringPeriodBillFalse = "데이터 수집 중" + lastMonthFalse = "정보가 없습니다" + standbyFalse = "데이터 수집 중" + planFalse = "계획을 입력하세요" + thisMonthTitle = "이번 달" + tierTitle = "누진단계" + planTitle = "사용 계획" + lastMonthTitle = "지난달" + rankingTitle = "랭킹" + standbyTitle = "대기전력" + energyMonitorDeviceTitle = "스마트미터 상태" + realtimeTitle = "실시간" + } + + /* check Encored Access Token */ + def EATValidation = checkEncoreAccessTokenValidation() + log.debug EATValidation + /* check if uuid already exist or not.*/ + if (EATValidation && atomicState.notPaired) { + getUUID() + } + + /* If token has been verified or refreshed and if uuid exist, call other apis */ + log.debug atomicState.notPaired + if (!atomicState.notPaired) { + + if(EATValidation) { + /* make a parameter to get device status */ + def deviceStatusParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/status", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* get device status. */ + deviceStatusData = getHttpGetJson(deviceStatusParam, 'CheckDeviceStatus') + + + /* make a parameter to get standby value.*/ + def standbyParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/standbyPower", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* get standby value */ + standbyData = getHttpGetJson(standbyParam, 'CheckStandbyPower') + + + + /* make a parameter to get user's info. */ + def meParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/me", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get user's info */ + meData = getHttpGetJson(meParam, 'CheckMe') + + + /* make a parameter to get energy used since metering date */ + def meteringParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/meteringUsage", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get the value of energy used since metering date. */ + meteringData = getHttpGetJson(meteringParam, 'CheckMeteringUsage') + + + /* make a parameter to get the energy usage ranking of a user. */ + def rankingParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/ranking/usages/${atomicState.uuid}?state=current&period=monthly", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get user's energy usage rank */ + rankingData = getHttpGetJson(rankingParam, 'CheckingRanking') + + /* Parse the values from the returned value of api calls. Then use these values to inform user how much they have used or will use. */ + + /* parse device status. */ + if (deviceStatusData) { + if (deviceStatusData.status == "NORMAL") { + deviceStatus = true + } + } + + log.debug "deiceStatusData : ${deviceStatus} || ${deviceStatusData}" + + /* Parse standby power. */ + if (standbyData) { + if (standbyData.standbyPower) { + standby = (standbyData.standbyPower / 1000) + } + } + + /* Parse max limit usage and it's bill from user's info. */ + if (meData) { + if (meData.maxLimitUsageBill) { + maxLimitUsageBill = meData.maxLimitUsageBill + maxLimitUsage = meData.maxLimitUsage + } + } + + /* Parse the values which have been used since metering date. + * The list is : + * meteringPeriodBill : A bill for energy usage. + * plan : The left amount of bill until it reaches limit. + * start : metering date in millisecond e.g. if the metering started on june and 1st, 2015,06,01 + * end : Today's date in millisecond + * meteringDay : The day of the metering date. e.g. if the metering date is June 1st, then it will return 1. + * meteringUSage : The amount of energy that user has used. + * tier : the level of energy use, tier exits from 1 to 6. + */ + if (meteringData) { + if (meteringData.meteringPeriodBill) { + meteringPeriodBill = meteringData.meteringPeriodBill + plan = maxLimitUsageBill - meteringData.meteringPeriodBill + start = meteringData.meteringStart + end = meteringData.meteringEnd + meteringDay = meteringData.meteringDay + meteringUsage = meteringData.meteringPeriodUsage + tier = ((int) (meteringData.meteringPeriodUsage / 100000000) + 1) + if(tier > 6) { + tier = 6 + } + + } + } + + /* Get ranking data of a user and the percent */ + if (rankingData) { + if (rankingData.user.ranking) { + percent = ((int)((rankingData.user.ranking / rankingData.user.population) * 10)) + if (percent > 10) { + percent = 10 + } + } + } + + /* if the start value exist, get last month energy usage. */ + if (start) { + def lastMonthParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/meteringUsages?period=monthly&start=${start}&end=${end}", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + lastMonth = getHttpGetJson(lastMonthParam, 'ChecklastMonth') + + } + + /* I decided to set values to device type handler, on loading solution module. + So, users may need to go back to solution module to update their device type handler. */ + def d = getChildDevice(atomicState.dni) + def kWhMonth = Math.round(meteringUsage / 10000) / 100 /* milliwatt to kilowatt*/ + def planUsed = 0 + if ( maxLimitUsage > 0 ) { + planUsed = Math.round((meteringUsage / maxLimitUsage) * 100) /* get the pecent of used amount against max usage */ + } else { + planUsed = Math.round((meteringUsage/ 1000000) * 100) /* if max was not decided let the used value be percent. e.g. 1kWh = 100% */ + } + + /* get realtime usage of user's device.*/ + def realTimeParam = makeGetParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/realtimeUsage", + [Authorization: "Bearer ${atomicState.encoredAccessToken}"]) + def realTimeInfo = getHttpGetJson(realTimeParam, 'CheckRealtimeinfo') + + if (!realTimeInfo) { + realTimeInfo = 0 + } else { + realTimeInfo = Math.round(realTimeInfo.activePower / 1000 ) + } + + + + /* inserting values to device type handler */ + + d?.sendEvent(name: "view", value : "${kWhMonth}") + if (deviceStatus) { + + d?.sendEvent(name: "month", value : "${thisMonthTitle} \n ${kWhMonth} \n kWh") + } else { + + d?.sendEvent(name: "month", value : "\n ${state.languageString."${atomicState.language}".message4.header} \n\n " + + "${state.languageString."${atomicState.language}".message4.body1} \n " + + "${state.languageString."${atomicState.language}".message4.body2} \n " + + "${state.languageString."${atomicState.language}".message4.body3}") + } + + d?.sendEvent(name: "real", value : "${realTimeInfo}w \n\n ${realtimeTitle}") + d?.sendEvent(name: "tier", value : "${tier} \n\n ${tierTitle}") + d?.sendEvent(name: "plan", value : "${planUsed}% \n\n ${planTitle}") + + deviceId = d.id + + } else { + /* If it finally couldn't get Encored access token. */ + log.error "Could not get Encored Access Token. Please try later." + } + + /* change the display uinit to bill from kWh if user want. */ + if (settings.displayUnit == "WON(₩)" || settings.displayUnit == "원(₩)") { + displayUnit = "bill" + } + + if (meteringPeriodBill) { + /* reform the value of the bill with the , separator */ + meteringPeriodBillShow = formatMoney("${meteringPeriodBill}") + meteringPeriodBillFalse = "" + thisMonthUnitOne = "₩" + + def dayPassed = getDayPassed(start, end, meteringDay) + if (atomicState.language == 'ko') { + thisMonthUnitTwo = "/ ${dayPassed}일" + } else { + if (dayPassed == 1) { + thisMonthUnitTwo = "/${dayPassed} day" + } else { + thisMonthUnitTwo = "/${dayPassed} days" + } + } + } + + if (plan) { + planShow = plan + if (plan >= 1000) {planShow = formatMoney("${plan}") } + planFalse = "" + planUnitOne = "₩" + + if (atomicState.language == 'ko') { + planUnitTwo = "남음" + } else { + planUnitTwo = "left" + } + + } + + /*set the showing units for html.*/ + log.debug lastMonth + if (lastMonth.usages) { + lastMonthShow = formatMoney("${lastMonth.usages[0].meteringPeriodBill}") + lastMonthFalse = "" + lastMonthUnit = "₩" + + } + + if (standby) { + standbyShow = standby + standbyFalse = "" + standbyUnit = "W" + } + + if (percent) { + rankImage = "" + rankingShow = "" + } + + if (tier) { + tierImage = "" + tierShow = "" + } + + if (deviceStatus) { + onOff = "ON" + } + + atomicState.solutionModuleSettings = [ + auth : atomicState.encoredAccessToken, + deviceState : deviceStatus, + percent : percent, + displayUnit : displayUnit, + language : atomicState.language, + deviceId : deviceId, + pairing : true + ] + + htmlBody = """ +
+ + +
+ + +
+

${thisMonthTitle}

+ +

${thisMonthUnitOne}

+

${meteringPeriodBillShow}

+

${meteringPeriodBillFalse}

+

${thisMonthUnitTwo}

+
+
+ + +
+

${tierTitle}

+ +
${tierImage}
+

${tierShow}

+
+
+ + +
+

${planTitle}

+ +

${planUnitOne}

+

${planShow}

+

${planFalse}

+

${planUnitTwo}

+
+
+ + +
+

${lastMonthTitle}

+ +

${lastMonthUnit}

+

${lastMonthShow}

+

${lastMonthFalse}

+
+
+ + +
+

${rankingTitle}

+ +
${rankImage}
+

${rankingShow}

+
+
+ + +
+

${standbyTitle}

+ +

${standbyShow}

+

${standbyFalse}

+

${standbyUnit}

+ +

+ + +
+

${energyMonitorDeviceTitle}

+ +
+

${onOff}

+
+
+ +
+ + + +
+
+

${thisMonthTitle}

+ +
+
+
+
+ +
+
+

${lastMonthTitle}

+ +
+
+
+ +
+
+

${tierTitle}

+ +
+
+
+ +
+
+

${rankingTitle}

+ +
+
+
+ +
+
+

${planTitle}

+ +
+
+
+ +
+
+

${standbyTitle}

+ +
+
+
+ + + + """ + } else { + log.debug "abotu to ask device connection" + def d = getChildDevice(atomicState.dni) + /* inserting values to device type handler */ + + d?.sendEvent(name: "month", value : "\n ${state.languageString."${atomicState.language}".message3.header} \n\n ${state.languageString."${atomicState.language}".message3.body1} \n ${state.languageString."${atomicState.language}".message3.body2}") + deviceId = d.id + + if (state.language == "ko") { + energyMonitorDeviceTitle = "스마트미터 상태" + } + /* need device pairing */ + atomicState.solutionModuleSettings = [ + dId : deviceId, + pairing : false + ] + + htmlBody = """ + +
+ + +
+

${state.languageString."${atomicState.language}".message3.header}

+

${state.languageString."${atomicState.language}".message3.body1}
${state.languageString."${atomicState.language}".message3.body2}

+ +
+ + + + +
+

${energyMonitorDeviceTitle}

+ +

${state.languageString."${atomicState.language}".message3.button2}

+
+
+ +
+ + + + + """ + } + + renderHTML() { + head { + """ + + + + + + + """ + } + body { + htmlBody + } + } +} + + +/* put commas for money or if there are things that need to have a comma separator.*/ +private formatMoney(money) { + def i = money.length()-1 + def ret = "" + def commas = ((int) Math.floor(i/3)) + + def j = 0 + def counter = 0 + + while (i >= 0) { + + if (counter > 0 && (counter % 3) == 0) { + ret = "${money[i]},${ret}" + j++ + } else { + ret = "${money[i]}${ret}" + } + + counter++ + i-- + } + + ret +} + +/* Count how many days have been passed since metering day: +* if metering day < today, it returns today - metering day +* else if metering day > today, it calcualtes how many days have been passed since meterin day and return calculated value. +* else return 1 (today). +*/ +private getDayPassed(start, end, meteringDay){ + + def day = 1 + def today = new Date(end) + def tzDifference = 9 * 60 + today.getTimezoneOffset() + today = new Date(today.getTime() + tzDifference * 60 * 1000).getDate(); + + if (today > meteringDay) { + day += today - meteringDay; + + } + if (today < meteringDay) { + def startDate = new Date(start); + def month = startDate.getMonth(); + def year = startDate.getYear(); + def lastDate = new Date(year, month, 31).getDate(); + + if (lastDate == 1) { + day += 30; + } else { + day += 31; + } + + day = day - meteringDay + today; + } + + day +} + +/* Get Encored push and send the notification. */ +def getEncoredPush() { + + byte[] decoded = "${params.msg}".decodeBase64() + def decodedString = new String(decoded) + + if (settings.notification == "true") { + sendNotification("${decodedString}", [method: "push"]) + } else { + sendNotificationEvent("${decodedString}") + } + +} \ No newline at end of file