Jak zapisywać wszystkie dane SmartThings ?

Sensory w inteligentnym domu zbierają gigantyczną ilość danych. Zazwyczaj będziesz używać ich do wyzwalania różnych zadań automatyki domowej, ale po co tylko reagować na dane, skoro można je zarchiwizować. W tym wpisie przedstawię, jak rejestrować wszystkie dane z czujnika SmartThings w arkuszu kalkulacyjnym Google.

[UWAGA]: Funkcja do działania na platformie SmartThings wymagała zainstalowania niestandardowych kodów obsługi. Możliwość ta została jednak wycofana. Na chwilę obecną nie jest możliwe skonfigurowanie żadnej nowej SmartApp w aplikacji SmartThings.

Dlaczego istnieje potrzeba archiwizacji danych ?

Rejestrując dane w czasie, będziesz mieć dostęp do wszystkich informacji potrzebnych do stworzenia bardziej usprawnionego i wydajnego inteligentnego domu. Na przykład, jeśli rejestrujesz wszystkie odczyty temperatury, możesz zmierzyć, jak szybko każde pomieszczenie się nagrzewa i ile czasu zajmuje zmniejszenie temperatury. Uzbrojony w te informacje, możesz udoskonalić swój system ogrzewania, aby w każdym pomieszczeniu panowała komfortowa temperatura. Dostęp do danych historycznych pomoże w zidentyfikowaniu wszelkich problemów z domem. Nawet jeśli faktycznie nie wykorzystasz tych danych obecnie, w przyszłości mogą stanowić interesujący zbiór informacji.

Arkusz z zapisanymi wartościami temperatury z czujników SmartThings

Ten samouczek wykorzystuje SmartApp Simple Event Logger autorstwa Kevin LaFramboise, która pobiera dane z dowolnego sensora i przesyła je do arkuszy kalkulacyjnego Google.

Simple Event Logger – prosty rejestrator zdarzeń

Prosty rejestrator zdarzeń to konfigurowalna aplikacja SmartApp, która umożliwia dokładne rejestrowanie całej aktywności urządzenia w arkuszu kalkulacyjnym Arkuszy Google. Każde wydarzenie jest przechowywane w osobnym wierszu, dzięki czemu otrzymujesz dokładny czas i szczegóły zdarzenia.

Arkusze Google mają łatwą w użyciu funkcję filtrowania, która pozwala na przeglądanie tylko istotnych dla nas wartości. Pozwalają filtrować dane w zakresie dat, określonych typów zdarzeń, takich jak temperatura.

Ponieważ wszystkie dane będą przechowywane w jednym arkuszu kalkulacyjnym, zaawansowani użytkownicy mogą z łatwością generować tabele przestawne i wykresy dla dowolnych czujników. Eliminuje również potrzebę przeglądania wielu arkuszy kalkulacyjnych.

Przygotowanie arkusza Google

W pierwszej kolejności przygotowujemy dokument. Przechodzimy do tworzenia arkusza kalkulacyjnego Google. W tym celu wejdź na stronę: http://docs.google.com/spreadsheets. Zaloguj się na swoje konto Google. Utwórz nowy pusty arkusz kalkulacyjny. Jeśli klikniesz tekst „Arkusz kalkulacyjny bez tytułu” w lewym górnym rogu, możesz zmienić jego nazwę na dowolną.

Otwórz Edytor skryptów, który znajduje się w górnym menu pod zakładką „Narzędzia”.

Zalecam użycie starszego edytora skryptów, kliknij „Użyj starszej wersji edytora” po prawej stronie.

Usuń istniejący kod i wklej poniższy kod. Aktualny kod znajduje się w pliku code.gs.

/**
 *  Simple Event Logger - Google Script Code v 1.5
 *
 *  Author: 
 *    Kevin LaFramboise (krlaframboise)
 *
 *  URL to documentation:
 *    https://github.com/krlaframboise/SmartThings/tree/master/smartapps/krlaframboise/simple-event-logger.src#simple-event-logger
 *
 *  Changelog:
 *
 *    1.5 (02/19/2019)
 *      -  Replaced obsolete javascript code.
 *
 *    1.3 (02/26/2017)
 *      -  Fixed archive issue when invalid or missing date in first column.
 *      -  Added option for logging short date and hour columns.
 *
 *    1.2.1 (01/28/2017)
 *      - Fixed issue with archive process when rows are frozen.
 *
 *    1.2 (01/22/2017)
 *      - Added archive functional for out of space and event limit.
 *      - Changed Maximum rows to 500000 because the sheet becomes very slow once you reach that size.
 *
 *    1.1 (01/02/2017)
 *      - Fixed Log Size calculation and added option to delete extra columns which will drastically increase the amount of clolumns that can be stored.
 *
 *    1.0.0 (12/26/2016)
 *      - Initial Release
 *
 *  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.
 *
 */
   
var getVersion = function() { return "01.05.00"; }
 
function doGet(e) {
	var output = "Version " + getVersion()
	return ContentService.createTextOutput(output);
}

function doPost(e) {
	var result = new Object();
	result.version = getVersion();
	result.eventsLogged = 0;
	
	if (e && e.contentLength > 0) {		
		var data = JSON.parse(e.postData.contents);
		if (data) {	
			var sheet = SpreadsheetApp.getActiveSheet();
			
			if (needToArchive(sheet, data.archiveOptions, data.events.length)) {
				result = archiveSheet(sheet, result);
			}
			else {
				result = logEvents(sheet, data, result);
			}
			
			result.freeSpace = calculateAvailableLogSpace(sheet);
			sendPostback(data.postBackUrl, result);
		}
	}
	
	return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);	
}

var logEvents = function(sheet, data, result) {
	try {
		result.totalEventsLogged = sheet.getLastRow() - 1;

		initializeHeaderRow(sheet, data.logDesc, data.logReporting)
		
		for (i=0; i < data.events.length; i++) {
			logEvent(sheet, data.logDesc, data.logReporting, data.events[i]);
			result.eventsLogged++;
		}
				
		if (data.deleteExtraColumns) {
			deleteExtraColumns(sheet);
		}
				
		result.totalEventsLogged = sheet.getLastRow() - 1;
		result.success = true;
	}
	catch(e) {
		if (e.message.contains("above the limit")) {
			result.logIsFull = true
		}
		result.error = e.message;
		result.success = false;
	}
	return result;
}

var logEvent = function(sheet, logDesc, logReporting, event) {
	var newRow = [
		event.time,
		event.device,
		event.name,
		event.value
	];
	if (logDesc || logReporting) {
		newRow.push(event.desc);
	}
	if (logReporting) {
		var dateCell = "A" + (sheet.getLastRow() + 1).toString()
		newRow.push("=INT(" + dateCell + ")");
		newRow.push("=HOUR(" + dateCell + ")");
	}	
	sheet.appendRow(newRow);
}

var initializeHeaderRow = function(sheet, logDesc, logReporting) {		
	if (sheet.getLastRow() == 0) {
		var header = [
			"Date/Time",
			"Device",
			"Event Name",
			"Event Value"
		];		
		sheet.appendRow(header);
		sheet.getRange("A:A").setNumberFormat('MM/dd/yyyy HH:mm:ss');
	}	
	if (logDesc || logReporting) {
		sheet.getRange("E1").setValue("Description")
	}
	if (logReporting && sheet.getRange("F1").getValue() != "Date") {
		sheet.getRange("F1").setValue("Date")
		sheet.getRange("F:F").setNumberFormat('MM/dd/yyyy');
		sheet.getRange("G1").setValue("Hour")
		sheet.getRange("G:G").setNumberFormat('00');
	}
}

var deleteExtraColumns = function(sheet) {
	try {
		sheet.deleteColumns((sheet.getLastColumn() + 1), (sheet.getMaxColumns() - sheet.getLastColumn()))
	}
	catch (e) {
	
	}
}

var calculateAvailableLogSpace = function(sheet) {
	var cellsUsed = (sheet.getMaxRows() * sheet.getMaxColumns());
	var spaceUsed = (cellsUsed / getLogCapacity()) * 100;
	return (100 - spaceUsed).toFixed(2) + "%";
}

var sendPostback = function(url, result) {
	var options = {
			'method': 'post',
			'headers': {"Content-Type": "application/json"},
			'payload': JSON.stringify(result)
	};
		
	var response = UrlFetchApp.fetch(url, options);	
}

var getLogCapacity = function() { return 500000; }

var needToArchive = function(sheet, archiveOptions, newEvents) {
	switch (archiveOptions.type) {
		case "Out of Space":
			return (archiveOptions.logIsFull || ((sheet.getMaxRows() + newEvents) >= (getLogCapacity() / sheet.getMaxColumns())));
		case "Events":
			return (archiveOptions.logIsFull || ((sheet.getLastRow() + newEvents) >= archiveOptions.interval));
		// case "Days":
			// return (getDaysSince(sheet.getRange(2, 1).value) >= archiveOptions.interval);
		default:
			return false;
	}
}

// var getDaysSince = function(firstDT) {
	// var dayMS = 1000 * 60 * 60 * 24;
	// var currentDT = new Date();
	// var currentDate = Date.UTC(currentDT.getFullYear(), currentDT.getMonth(), currentDT.getDate());
	// var firstDate = Date.UTC(firstDT.getFullYear(), firstDT.getMonth(), firstDT.getDate());
	// var diffMS = Math.abs(currentDate - firstDate);
	// return Math.floor(diffMS / dayMS); 	
// }

var archiveSheet = function(sheet, result) {	
	try {
		var archiveSheet = createArchiveSheet(sheet);
		if (archiveSheet) {
			if (verifyArchiveSheet(sheet, archiveSheet)) {
				clearSheet(sheet);
				result.eventsArchived = true;
				result.success = true;
			}
			else {
				result.success = false;
				result.error = "The number of rows and columsn in thea archive Sheet do not match the original sheet so the original file was not cleared.";				
			}
		}
		else {
			result.success = false;
			result.error = "Unable to create archive file.";			
		}
		result.totalEventsLogged = sheet.getLastRow() - 1;		
	}
	catch(e) {
		result.error = e.message;
		result.success = false;
	}
	return result;
}

var createArchiveSheet = function(sheet) {
	var archiveSheetName = getArchiveSheetName(SpreadsheetApp.getActive().getName(), sheet);
	var archiveFile = DriveApp.getFileById(SpreadsheetApp.getActive().getId()).makeCopy(archiveSheetName);
	return SpreadsheetApp.open(archiveFile);
}


var getArchiveSheetName = function(name, sheet) {
	var firstDate = sheet.getRange("A2").getValue();
	var lastDate = sheet.getRange(sheet.getLastRow(), 1).getValue();
	return name + "_" + getFormattedDate(firstDate) + "_" + getFormattedDate(lastDate);
}

var getFormattedDate = function(dt) {
	try {
		var yyyy = dt.getFullYear().toString(); 
		var mm = (dt.getMonth()+1).toString(); 
		var dd = dt.getDate().toString(); 
		return yyyy + "-" + (mm[1] ? mm : ("0" + mm[0])) + "-" + (dd[1] ? dd : ("0" + dd[0])); 
	}
	catch (ex) {
		return "undetermined"
	}
}

var verifyArchiveSheet = function(sheet, archiveSheet) {
	return (sheet.getLastRow() == archiveSheet.getLastRow() && sheet.getLastColumn() == archiveSheet.getLastColumn());
}

var clearSheet = function(sheet) {
	if (sheet.getMaxRows() > 2) {
		sheet.deleteRows(3, (sheet.getMaxRows() - 2));
	}
	sheet.getRange(2, 1, 1, sheet.getLastColumn()).clearContent();
}  

Przejdź do menu „Opublikuj” i kliknij „Wdróż jako aplikację internetową…”.

Wpisz tytuł projektu, możesz nadać mu dowolną nazwę.

  • Zmień „Project version” na „Nowy”. Jest to wartość domyślna przy pierwszym wdrożeniu, ale należy ją zmienić ręcznie za każdym razem, gdy wdrażasz nową wersję, w przeciwnym razie zmiany nie zostaną zastosowane.
  • Zmień pole dostępu do aplikacji „Execute the app as:” na „Anyone, even anonymous”(Każdy, nawet anonimowy).
    • Dzięki temu adres URL aplikacji internetowej będzie dostępny dla każdego, kto zna ten adres, ale nikt go nie pozna, chyba że go podasz. Nawet gdyby ktoś miał adres URL, jedyną rzeczą, którą mógłby zobaczyć, jest numer wersji z interwałem.
  • Kliknij „Deploy”.

Kliknij przycisk „Sprawdź uprawnienia” w wyskakującym okienku dotyczącym wymagań autoryzacji. Kliknij przycisk „Zezwól” w każdym wyskakującym oknie i przy każdym uprawnieniu.

Skopiuj „Current web app URL” i kliknij „OK”. Adres URL aplikacji web, jest konieczny do wklejenia w ustawieniach SmartApp. To zakończy proces przygotowania arkusza Google.

Instrukcja dodawania SmartApps

Przechodzimy do instalacji SmartApps. W pierwszej kolejności należy się zalogować do IDE na https://account.smartthings.com, i kliknąć Log in. Koniecznie na to samo konto Samsung, na którym został zbudowany inteligentny dom SmartThings.

Następnie wchodzimy w My SmartApps. Tutaj posiadamy listę wszystkich niestandardowych SmartApps do tej pory zainstalowanych.

Kliknij New SmartApp i tym samym przechodzimy do instalowania.

My zdecydowaliśmy się na instalację aplikacji z posiadanego już kodu, więc na kolejnej stronie klikamy From Code. Wklejamy poniższy kod (Ctrl + V) i klikamy Create. Aktualny kod znajduje się w pliku simple-event-logger.groovy.

/**
 *  Simple Event Logger - SmartApp v 1.5
 *
 *  Author: 
 *    Kevin LaFramboise (krlaframboise)
 *
 *  URL to documentation:
 *    https://github.com/krlaframboise/SmartThings/tree/master/smartapps/krlaframboise/simple-event-logger.src#simple-event-logger
 *
 *  Changelog:
 *
 *    1.5 (02/19/2019)
 *      -  Replaced obsolete javascript code in Groovy Script.
 *
 *    1.4.1 (10/22/2017)
 *      -  The Google Script does NOT need to be updated.
 *      -  Added "Activity" attribute which will log device "online/offline" changes.
 *      -  Added setting that allows you to log the event descriptionText to the description field instead of just the value and unit.
 *      -  Fixed timeout issue with the Device Attribute Exclusions screen.
 *
 *    1.3 (02/26/2017)
 *      - Requires Google Script Update
 *      - Added maximum catch-up setting that restricts the date range it uses when catching up from missed runs.
 *      - Added option for Log Reporting so when it's enabled it creates a column for hour and short date. 
 *
 *    1.2.1 (01/28/2017)
 *      - New version of Google Script.
 *
 *    1.2 (01/22/2017)
 *      - Added archive functionality for when the sheet is full or by specific number of events.
 *
 *    1.1.3 (01/19/2017)
 *      - Retrieve events from state instead of event history.
 *      - Retrieves up to 200 events per attribute for each device instead of a total of 50 events per device.
 *      - Changed first run to previous hour instead of previous 24 hours to prevent warnings caused by the large number of events that get logged.
 *
 *    1.1.1 (01/04/2017)
 *      - Enabled submit on change for device lists so that the event list populates on initial install. 
 *
 *    1.1.0 (01/02/2017)
 *      - Moved Event Exclusions to another page to prevent timeout and added abort feature so that it stops adding exclusion fields if it runs out of time.
 *      - Fixed log space calculation and added setting to the options section that when enabled, deletes extra columns in the spreadsheet which allows you to log more.
 *
 *    1.0.3 (01/01/2017)
 *      - Disabled submit on change behavior for device selection page and made the select events field display all events instead of just the supported ones.
 *      - Added additional error handling and logging in case the other change doesn't fix the reported error.
 *
 *    1.0.2 (12/29/2016)
 *      - Added additional logging and verification of the Web App Url.
 *
 *    1.0.1 (12/28/2016)
 *      - Bug fix for devices with null attributes
 *
 *    1.0.0 (12/26/2016)
 *      - Initial Release
 *
 *  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.
 *
 */
 
include 'asynchttp_v1'
 
definition(
    name: "Simple Event Logger",
    namespace: "krlaframboise",
    author: "Kevin LaFramboise",
    description: "Allows you to choose devices and attributes and it logs the device, event name, event value, event time, and event description of all the events that have occured since the last time it ran.",
    category: "My Apps",
    iconUrl: "https://raw.githubusercontent.com/krlaframboise/Resources/master/simple-event-logger/app-SimpleEventLogger.png",
    iconX2Url: "https://raw.githubusercontent.com/krlaframboise/Resources/master/simple-event-logger/app-SimpleEventLogger@2x.png",
    iconX3Url: "https://raw.githubusercontent.com/krlaframboise/Resources/master/simple-event-logger/app-SimpleEventLogger@3x.png")
		
preferences {
	page(name: "mainPage")
	page(name: "devicesPage")
	page(name: "attributesPage")
	page(name: "attributeExclusionsPage")
	page(name: "optionsPage")
	page(name: "aboutPage")
	page(name: "createTokenPage")
}

def version() { return "01.05.00" }
def gsVersion() { return "01.05.00" }

def mainPage() {
	dynamicPage(name:"mainPage", uninstall:true, install:true) {
		if (state.allConfigured && state.loggingStatus) {
			getLoggingStatusContent()
		}
		if (state.devicesConfigured) {
			section("Selected Devices") {
				getPageLink("devicesPageLink", "Tap to change", "devicesPage", null, buildSummary(getSelectedDeviceNames()))
			}
		}
		else {			
			getDevicesPageContent()
		}
		
		if (state.attributesConfigured) {
			section("Selected Events") {
				getPageLink("attributesPageLink", "Tap to change", "attributesPage", null, buildSummary(settings?.allowedAttributes?.sort()))
			}
			section ("Event Device Exclusions") {
				getPageLink("attributeExclusionsPageLink", "Select devices to exclude for specific events.", "attributeExclusionsPage")
			}
		}
		else {
			getAttributesPageContent()
		}
				
		if (!state.optionsConfigured) {
			getOptionsPageContent()
		}
		
		section("  ") {
			if (state.optionsConfigured) {
				getPageLink("optionsPageLink", "Other Options", "optionsPage", null, "Tap to set")
			}
			label title: "Assign a name", required: false
			mode title: "Set for specific mode(s)", required: false
			if (state.installed) {		
				getPageLink("aboutPageLink", "About Simple Event Logger", "aboutPage", null, "Tap to view documentation, version and additional information.", "https://raw.githubusercontent.com/krlaframboise/Resources/master/simple-event-logger/app-SimpleEventLogger@3x.png")
			}
		}
		section("  ") {
			paragraph "  ", required: false
		}
	}
}

private getLoggingStatusContent() {
	if (state.loggingStatus?.success != null) {
		section("Logging Status") {			
			def status = getFormattedLoggingStatus()
			
			paragraph required: false,
				"Total Events Logged: ${status.totalEventsLogged}\nAvailable Log Space: ${status.freeSpace}\nLast Execution:\n - Result: ${status.result}\n - Events From: ${status.start}\n - Events To: ${status.end}\n - Logged: ${status.eventsLogged}\n - Run Time: ${status.runTime}"
		}
	}
}

def aboutPage() {
	dynamicPage(name:"aboutPage") {
		section() {		
			def gsVerActual = state.loggingStatus?.gsVersion ?: "?"
			
			def gsVerExpectedMsg = (gsVersion() == gsVerActual) ? "" : " (expected version is ${gsVersion()})"
		
			paragraph image: "https://raw.githubusercontent.com/krlaframboise/Resources/master/simple-event-logger/app-SimpleEventLogger@3x.png",
				title: "Simple Event Logger\nBy Kevin LaFramboise (@krlaframboise)",
				required: false,
				"Allows you to choose devices and attributes and it logs the device, event name, event value, event time, and event description of all the events that have occured since the last time it ran."
				
			paragraph title: "Version",
				required: false,
				"SmartApp: ${version()}\nGoogle Script: ${gsVerActual}${gsVerExpectedMsg}"
				
			 href(name: "documentationLink",
				 title: "View Documentation",
				 required: false,
				 style: "external",
				 url: "http://htmlpreview.github.com/?https://raw.githubusercontent.com/krlaframboise/SmartThings/master/smartapps/krlaframboise/simple-event-logger.src/ReadMe.md",
				 description: "Additional information about the SmartApp and installation instructions.")
		}		
	}
}

def devicesPage() {
	dynamicPage(name:"devicesPage") {
		getDevicesPageContent()
	}
}

private getDevicesPageContent() {
	section("Choose Devices") {
		paragraph "Selecting a device from one of the fields below lets the SmartApp know that the device should be included in the logging process."
		paragraph "Each device only needs to be selected once and which field you select it from has no effect on which events will be logged for it."
		paragraph "There's a field below for every capability, but you should be able to locate most of your devices in either the 'Actuators' or 'Sensors' fields at the top."		
		
		getCapabilities().each { 
			try {
				if (it.cap) {
					input "${it.cap}Pref", "capability.${it.cap}",
						title: "${it.title}:",
						multiple: true,
						hideWhenEmpty: true,
						required: false,
						submitOnChange: true
				}
			}
			catch (e) {
				logTrace "Failed to create input for ${it}: ${e.message}"
			}
		}
			
	}
}

def attributesPage() {
	dynamicPage(name:"attributesPage") {
		getAttributesPageContent()
	}
}

private getAttributesPageContent() {
	//def supportedAttr = getAllAttributes()?.sort()
	def supportedAttr = getSupportedAttributes()?.sort()
	if (supportedAttr) {
		section("Choose Events") {
			paragraph "Select all the events that should get logged for all devices that support them."
			paragraph "If the event you want to log isn't shown, verify that you've selected a device that supports it because only supported events are included."
			input "allowedAttributes", "enum",
				title: "Which events should be logged?",
				required: true,
				multiple: true,					
				submitOnChange: true,
				options: supportedAttr
		}
	}
	else {
		section("Choose Events") {
			paragraph "You need to select devices before you can choose events."
		}
	}
}

def attributeExclusionsPage() {
	dynamicPage(name:"attributeExclusionsPage") {		
		section ("Device Exclusions (Optional)") {
			
			def startTime = new Date().time
			
			if (settings?.allowedAttributes) {
				
				paragraph "If there are some events that should't be logged for specific devices, use the corresponding event fields below to exclude them."
				paragraph "You can also use the fields below to see which devices support each event."
				
				def devices = getSelectedDevices()?.sort { it.displayName }
				
				settings?.allowedAttributes?.sort()?.each { attr ->
				
					if (startTime && (new Date().time - startTime) > 15000) {
						paragraph "The SmartApp was able to load all the fields within the allowed time.  If the event you're looking for didn't get loaded, select less devices or attributes."
						startTime = null
					}
					else if (startTime) {				
						try {
							def attrDevices = (isAllDeviceAttr("$attr") ? devices : (devices?.findAll{ device ->
								device.hasAttribute("${attr}")
							}))?.collect { it.displayName }?.unique()
							if (attrDevices) {
								input "${attr}Exclusions", "enum",
									title: "Exclude ${attr} events:",
									required: false,
									multiple: true,
									options: attrDevices
							}
						}
						catch (e) {
							logWarn "Error while getting device exclusion list for attribute ${attr}: ${e.message}"
						}
					}
				}
			}
		}
	}
}

def optionsPage() {
	dynamicPage(name:"optionsPage") {
		getOptionsPageContent()
	}
}

private getOptionsPageContent() {
	section ("Logging Options") {
		input "logFrequency", "enum",
			title: "Log Events Every:",
			required: false,
			defaultValue: "5 Minutes",
			options: ["5 Minutes", "10 Minutes", "15 Minutes", "30 Minutes", "1 Hour", "3 Hours"]
		input "logCatchUpFrequency", "enum",
			title: "Maximum Catch-Up Interval:\n(Must be greater than 'Log Events Every':",
			required: false,
			defaultValue: logCatchUpFrequencySetting,
			options: ["15 Minutes", "30 Minutes", "1 Hour", "2 Hours", "6 Hours"]
		input "maxEvents", "number",
			title: "Maximum number of events to log for each device per execution. (1 - 200)",
			range: "1..200",
			defaultValue: maxEventsSetting,
			required: false
		input "logDesc", "bool",
			title: "Log Event Descripion?",
			defaultValue: true,
			required: false
		input "useValueUnitDesc", "bool",
			title: "Use Value and Unit for Description?",
			defaultValue: true,
			required: false
		input "logReporting", "bool",
			title: "Include additional columns for short date and hour?",
			defaultValue: false,
			required: false
		input "deleteExtraColumns", "bool",
			title: "Delete Extra Columns?",
			description: "Enable this setting to increase the log size.",
			defaultValue: true,
			required: false
		input "archiveType", "enum",
			title: "Archive Type:",
			defaultValue: "None",
			submitOnChange: true,
			required: false,
			options: ["None", "Out of Space", "Events"]
		if (settings?.archiveType && !(settings?.archiveType in ["None", "Out of Space"])) {
			input "archiveInterval", "number",
				title: "Archive After How Many Events?",
				defaultValue: 50000,
				required: false,
				range: "100..100000"
		}
	}
	section("${getWebAppName()}") {		
		input "googleWebAppUrl", "text",
			title: "${getWebAppName()} Url",
			required: true
		paragraph "The url you enter into this field needs to start with: ${webAppBaseUrl} or ${webAppBaseUrl2}"
		paragraph "If your url does not start like that, go back and copy it from the Script Editor Publish screen in the Google Sheet."		
	}
	
	if (state.installed) {
		section("OAuth Token") {
			getPageLink("createTokenPageLink", "Generate New OAuth Token", "createTokenPage", null, state.endpoint ? "" : "The SmartApp was unable to generate an OAuth token which usually happens if you haven't gone into the IDE and enabled OAuth in this SmartApps settings.  Once OAuth is enabled, you can click this link to try again.")
		}
	}
	
	section("Live Logging Options") {
		input "logging", "enum",
			title: "Types of messages to write to Live Logging:",
			multiple: true,
			required: false,
			defaultValue: ["debug", "info"],
			options: ["debug", "info", "trace"]
	}
}

def createTokenPage() {
	dynamicPage(name:"createTokenPage") {
		
		disposeAppEndpoint()
		initializeAppEndpoint()		
		
		section() {
			if (state.endpoint) {				
				paragraph "A new token has been generated."
			}
			else {
				paragraph "Unable to generate a new OAuth token.\n\n${getInitializeEndpointErrorMessage()}"				
			}
		}
	}
}

private getPageLink(linkName, linkText, pageName, args=null,desc="",image=null) {
	def map = [
		name: "$linkName", 
		title: "$linkText",
		description: "$desc",
		page: "$pageName",
		required: false
	]
	if (args) {
		map.params = args
	}
	if (image) {
		map.image = image
	}
	href(map)
}

private buildSummary(items) {
	def summary = ""
	items?.each {
		summary += summary ? "\n" : ""
		summary += "   ${it}"
	}
	return summary
}

def uninstalled() {
	logTrace "Executing uninstalled()"
	disposeAppEndpoint()
}

private disposeAppEndpoint() {
	if (state.endpoint) {
		try {
			logTrace "Revoking access token"
			revokeAccessToken()
		}
		catch (e) {
			logWarn "Unable to remove access token: $e"
		}
		state.endpoint = ""
	}
}

def installed() {	
	logTrace "Executing installed()"
	initializeAppEndpoint()
	state.installed = true
}

def updated() {
	logTrace "Executing updated()"
	state.installed = true
	
	unschedule()
	unsubscribe()
	
	initializeAppEndpoint()
	
	if (settings?.logFrequency && settings?.maxEvents && settings?.logDesc != null && verifyWebAppUrl(settings?.googleWebAppUrl)) {
		state.optionsConfigured = true
	}
	else {
		logDebug "Unconfigured - Options"
		state.optionsConfigured = false
	}
	
	if (settings?.allowedAttributes) {
		state.attributesConfigured = true
	}
	else {
		logDebug "Unconfigured - Choose Events"
	}
	
	if (getSelectedDevices()) {
		state.devicesConfigured = true
	}
	else {
		logDebug "Unconfigured - Choose Devices"
	}
	
	state.allConfigured = (state.optionsConfigured && state.attributesConfigured && state.devicesConfigured)
	
	if  (state.allConfigured) {
		def logFrequency = (settings?.logFrequency ?: "5 Minutes").replace(" ", "")
		
		"runEvery${logFrequency}"(logNewEvents)
		
		verifyGSVersion()
		runIn(10, startLogNewEvents)
	}
	else {
		logDebug "Event Logging is disabled because there are unconfigured settings."
	}
}

private verifyWebAppUrl(url) {
	if (!url) {
		logDebug "The ${getWebAppName()} Url field is required"
		return false
	}
	else if ("$url"?.toLowerCase()?.startsWith(webAppBaseUrl) || "$url"?.toLowerCase()?.startsWith(webAppBaseUrl2)) {
		return true
	}
	else {		
		logWarn "The ${webAppName} Url is not valid.  Go back and copy the url from the Google Sheets Script Editor Publish page."
		return false
	}
}

// Requests the version from the Google Script and displays a warning if it's not the expected version.
private verifyGSVersion() {
	def actualGSVersion = ""
	
	logTrace "Retrieving Google Script Code version of the ${getWebAppName()}"
	try {
		def params = [
			uri: settings?.googleWebAppUrl
		]
	
		httpGet(params) { objResponse ->
			if (objResponse?.status == 200) {
				if ("${objResponse.data}" == "Version ${gsVersion()}") {
					logTrace "The ${getWebAppName()} is using the correct version of the Google Script code."
				}
				else {
					logWarn "The ${getWebAppName()} is not using version ${gsVersion()} of the Google Script code which is required by version ${version()} of the Simple Event Logger SmartApp.\n\nPlease update to the latest version of this SmartApp and the Google Script code to ensure that everything works properly.\n\nWhen deploying a new version of the Google Script Code in the Google Sheet, make sure you change the 'Product Version' field to 'New'."
				}
			}
			else {
				logWarn "Unable to connect to the ${getWebAppName()}.  Make sure you followed the instructions for setting up and testing it."
			}
		}
	}
	catch(e) {
		logWarn "Failed to retrieve Google Script Version.  Error: ${e.message}"
	}	
}

def startLogNewEvents() {
	logNewEvents()
}

def logNewEvents() {	
	def status = state.loggingStatus ?: [:]
	
	// Move the date range to the next position unless the google script failed last time or was skipped due to the sheet being archived.
	if (!status.success || status.eventsArchived) {
		status.lastEventTime = status.firstEventTime
	}
	
	status.success = null
	status.finished = null
	status.eventsArchived = null
	status.eventsLogged = 0
	status.started = new Date().time
	
	status.firstEventTime = getFirstEventTimeMS(status.lastEventTime)
	
	status.lastEventTime = getNewLastEventTimeMS(status.started, (status.firstEventTime + 1000))
	
	def startDate = new Date(status.firstEventTime + 1000)
	def endDate = new Date(status.lastEventTime)
	
	state.loggingStatus = status

	def events = getNewEvents(startDate, endDate)
	def eventCount = events?.size ?: 0
	def actionMsg = eventCount > 0 ? ", posting them to ${getWebAppName()}" : ""
	
	logDebug "SmartThings found ${String.format('%,d', eventCount)} events between ${getFormattedLocalTime(startDate.time)} and ${getFormattedLocalTime(endDate.time)}${actionMsg}"
	
	if (events) {
		postEventsToGoogleSheets(events)
	}
	else {		
		state.loggingStatus.success = true
		state.loggingStatus.finished = new Date().time
	}
}

private getFirstEventTimeMS(lastEventTimeMS) {
	def firstRunMS = (3 * 60 * 60 * 1000) // 3 Hours 
	return safeToLong(lastEventTimeMS) ?: (new Date(new Date().time - firstRunMS)).time 
}

private getNewLastEventTimeMS(startedMS, firstEventMS) {
	if ((startedMS - firstEventMS) > logCatchUpFrequencySettingMS) {
		return (firstEventMS + logCatchUpFrequencySettingMS)
	}
	else {
		return startedMS
	}
}

private getLogCatchUpFrequencySetting() {
	return settings?.logCatchUpFrequency ?: "1 Hour"
}

private getLogCatchUpFrequencySettingMS() {
	def minutesVal
	switch (logCatchUpFrequencySetting) {
		case "15 Minutes":
			minutesVal = 15
			break
		case "30 Minutes":
			minutesVal = 30
			break
		case "1 Hour":
			minutesVal = 60
			break
		case "2 Hours":
			minutesVal = 120
			break
		case "6 Hours":
			minutesVal = 360
			break
		default:
			minutesVal = 60
	}
	return (minutesVal * 60 * 1000)
}

private postEventsToGoogleSheets(events) {
	def jsonOutput = new groovy.json.JsonOutput()
	def jsonData = jsonOutput.toJson([
		postBackUrl: "${state.endpoint}update-logging-status",
		archiveOptions: getArchiveOptions(),
		logDesc: (settings?.logDesc != false),
		logReporting: (settings?.logReporting == true),
		deleteExtraColumns: (settings?.deleteExtraColumns == true),
		events: events
	])

	def params = [
		uri: "${settings?.googleWebAppUrl}",
		contentType: "application/json",
		body: jsonData
	]	
	
	asynchttp_v1.post(processLogEventsResponse, params)
}

private getArchiveOptions() {
	return [
		logIsFull: (state.loggingStatus?.logIsFull ? true : false),
		type: (settings?.archiveType ?: ""),
		interval: safeToLong(settings?.archiveInterval, 50000)
	]
}

// Google Sheets redirects the post to a temporary url so the response is usually 302 which is page moved.
def processLogEventsResponse(response, data) {
	if (response?.status == 302) {
		logTrace "${getWebAppName()} Response: ${response.status}"
	}
	else if ( response?.errorMessage?.contains("Read timeout to script.google.com") ) {
		logTrace "Timeout while waiting for Google Logging to complete."
	}
	else {
		logWarn "Unexpected response from ${getWebAppName()}: ${response?.errorMessage}"
	}
}

private initializeAppEndpoint() {		
	try {
		if (!state.endpoint) {
			logDebug "Creating Access Token"
			def accessToken = createAccessToken()
			if (accessToken) {
				state.endpoint = apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/")
			}
		}		
	} 
	catch(e) {
		logWarn "${getInitializeEndpointErrorMessage()}"
		state.endpoint = null
	}
}

private getInitializeEndpointErrorMessage() {
	return "This SmartApp requires OAuth so please follow these steps to enable it:\n1.  Go into the My SmartApps section of the IDE\n2. Click the pencil icon next to this SmartApp to open the properties\n3.Click the 'OAuth' link\n4. Click 'Enable OAuth in Smart App'."
}

mappings {
	path("/update-logging-status") {
		action: [
			POST: "api_updateLoggingStatus"
		]
	}	
}

def api_updateLoggingStatus() {
	def status = state.loggingStatus ?: [:]
	def data = request.JSON
	if (data) {
		status.success = data.success
		status.eventsArchived = data.eventsArchived
		status.logIsFull = data.logIsFull
		status.gsVersion = data.version
		status.finished = new Date().time
		status.eventsLogged = data.eventsLogged
		status.totalEventsLogged = data.totalEventsLogged
		status.freeSpace = data.freeSpace
		
		if (data.error) {
			logDebug "${getWebAppName()} Reported: ${data.error}"
		}
	}
	else {
		status.success = false
		logDebug "The ${getWebAppName()} postback has no data."
	}	
	state.loggingStatus = status
	logLoggingStatus()
}

private logLoggingStatus() {
	def status = getFormattedLoggingStatus()
	if (status.logIsFull) {
		logWarn "The Google Sheet is Out of Space"
	}
	if (state.loggingStatus?.success) {
		if (status.eventsArchived) {
			logDebug "${getWebAppName()} archived events in ${status.runTime}."
		}
		else {
			logDebug "${getWebAppName()} logged ${status.eventsLogged} events between ${status.start} and ${status.end} in ${status.runTime}."			
		}		
	}
	else {
		logWarn "${getWebAppName()} failed to log events between ${status.start} and ${status.end}."
	}	
	
	logTrace "Google Script Version: ${state.loggingStatus?.gsVersion}, Total Events Logged: ${status.totalEventsLogged}, Remaining Space Available: ${status.freeSpace}"
}

private getFormattedLoggingStatus() {
	def status = state.loggingStatus ?: [:]
	return [
		result: status?.success ? "Successful" : "Failed",
		start:  getFormattedLocalTime(safeToLong(status.firstEventTime)),
		end:  getFormattedLocalTime(safeToLong(status.lastEventTime)),
		runTime: "${((safeToLong(status.finished) - safeToLong(status.started)) / 1000)} seconds",
		eventsLogged: "${String.format('%,d', safeToLong(status.eventsLogged))}",
		totalEventsLogged: "${String.format('%,d', safeToLong(status.totalEventsLogged))}",
		freeSpace: status.freeSpace
	]
}

private getNewEvents(startDate, endDate) {	
	def events = []
	
	logTrace "Retrieving Events from ${startDate} to ${endDate}"
	
	getSelectedDevices()?.each  { device ->
		getDeviceAllowedAttrs(device?.displayName)?.each { attr ->
			device.statesBetween("${attr}", startDate, endDate, [max: maxEventsSetting])?.each { event ->
				events << [
					time: getFormattedLocalTime(event.date?.time),
					device: device.displayName,
					name: "${attr}",
					value: event.value,
					desc: getEventDesc(event)
				]
			}
		}
	}
	return events?.unique()?.sort { it.time }
}

private getEventDesc(event) {
	if (settings?.useValueUnitDesc != false) {
		return "${event.value}" + (event.unit ? " ${event.unit}" : "")
	}
	else {
		def desc = "${event?.descriptionText}"
		if (desc.contains("{")) {
			desc = replaceToken(desc, "linkText", event.displayName)
			desc = replaceToken(desc, "displayName", event.displayName)
			desc = replaceToken(desc, "name", event.name)
			desc = replaceToken(desc, "value", event.value)
			desc = replaceToken(desc, "unit", event.unit)
		}
		return desc
	}
}

private replaceToken(desc, token, value) {
	desc = "$desc".replace("{{", "|").replace("}}", "|")
	return desc.replace("| ${token} |", "$value")
}

private getMaxEventsSetting() {
	return settings?.maxEvents ?: 200
}
	
private getFormattedLocalTime(utcTime) {
	if (utcTime) {
		try {
			def localTZ = TimeZone.getTimeZone(location.timeZone.ID)
			def localDate = new Date(utcTime + localTZ.getOffset(utcTime))	
			return localDate.format("MM/dd/yyyy HH:mm:ss")
		}
		catch (e) {
			logWarn "Unable to get formatted local time for ${utcTime}: ${e.message}"
			return "${utcTime}"
		}
	}
	else {
		return ""
	}
}

private getDeviceAllowedAttrs(deviceName) {
	def deviceAllowedAttrs = []
	try {
		settings?.allowedAttributes?.each { attr ->
			try {
				def attrExcludedDevices = settings?."${attr}Exclusions"
				
				if (!attrExcludedDevices?.find { it?.toLowerCase() == deviceName?.toLowerCase() }) {
					deviceAllowedAttrs << "${attr}"
				}
			}
			catch (e) {
				logWarn "Error while getting device allowed attributes for ${device?.displayName} and attribute ${attr}: ${e.message}"
			}
		}
	}
	catch (e) {
		logWarn "Error while getting device allowed attributes for ${device.displayName}: ${e.message}"
	}
	return deviceAllowedAttrs
}

private getSupportedAttributes() {
	def supportedAttributes = []
	def devices = getSelectedDevices()
	
	if (devices) {
	
		getAllAttributes()?.each { attr ->
			try {
				if (isAllDeviceAttr("$attr") || devices?.find { it?.hasAttribute("${attr}") }) {
					supportedAttributes << "${attr}"
				}
			}
			catch (e) {
				logWarn "Error while finding supported devices for ${attr}: ${e.message}"
			}
			
		}
	}
	
	return supportedAttributes?.unique()?.sort()
}

private isAllDeviceAttr(attr) { 
	return getCapabilities().find { it.allDevices && it.attr == attr } ? true : false
}

private getAllAttributes() {
	def attributes = []	
	
	getCapabilities().each { cap ->
		try {		
			if (cap?.attr) {
				if (cap.attr instanceof Collection) {
					cap.attr.each { attr ->
						attributes << "${attr}"
					}
				}
				else {
					attributes << "${cap?.attr}"
				}
			}
		}
		catch (e) {
			logWarn "Error while getting attributes for capability ${cap}: ${e.message}"
		}
	}	
	return attributes
}

private getSelectedDeviceNames() {
	try {
		return getSelectedDevices()?.collect { it?.displayName }?.sort()
	}
	catch (e) {
		logWarn "Error while getting selected device names: ${e.message}"
		return []
	}
}

private getSelectedDevices() {
	def devices = []
	getCapabilities()?.each {	
		try {
			if (it.cap && settings?."${it.cap}Pref") {
				devices << settings?."${it.cap}Pref"
			}
		}
		catch (e) {
			logWarn "Error while getting selected devices for capability ${it}: ${e.message}"
		}
	}	
	return devices?.flatten()?.unique { it.displayName }
}

private getCapabilities() {
	[
		[title: "Actuators", cap: "actuator"],
		[title: "Sensors", cap: "sensor"],
		[title: "Acceleration Sensors", cap: "accelerationSensor", attr: "acceleration"],
		[title: "Device Activity", attr: "activity", allDevices: true],
		[title: "Alarms", cap: "alarm", attr: "alarm"],
		[title: "Batteries", cap: "battery", attr: "battery"],
		[title: "Beacons", cap: "beacon", attr: "presence"],
		[title: "Bulbs", cap: "bulb", attr: "switch"],
		[title: "Buttons", cap: "button", attr: ["button", "numberOfButtons"]],
		[title: "Carbon Dioxide Measurement Sensors", cap: "carbonDioxideMeasurement", attr: "carbonDioxide"],
		[title: "Carbon Monoxide Detectors", cap: "carbonMonoxideDetector", attr: "carbonMonoxide"],
		[title: "Color Control Devices", cap: "colorControl", attr: ["color", "hue", "saturation"]],
		[title: "Color Temperature Devices", cap: "colorTemperature", attr: "colorTemperature"],
		[title: "Consumable Devices", cap: "consumable", attr: "consumableStatus"],
		[title: "Contact Sensors", cap: "contactSensor", attr: "contact"],
		[title: "Doors", cap: "doorControl", attr: "door"],
		[title: "Energy Meters", cap: "energyMeter", attr: "energy"],
		[title: "Garage Doors", cap: "garageDoorControl", attr: "door"],
		[title: "Illuminance Measurement Sensors", cap: "illuminanceMeasurement", attr: "illuminance"],
		[title: "Image Capture Devices", cap: "imageCapture", attr: "image"],		
		[title: "Indicators", cap: "indicator", attr: "indicatorStatus"],
		[title: "Lights", cap: "light", attr: "switch"],
		[title: "Locks", cap: "lock", attr: "lock"],
		[title: "Media Controllers", cap: "mediaController", attr: "currentActivity"],
		[title: "Motion Sensors", cap: "motionSensor", attr: "motion"],
		[title: "Music Players", cap: "musicPlayer", attr: ["level", "mute", "status", "trackDescription"]],
		[title: "Outlets", cap: "outlet", attr: "switch"],
		[title: "pH Measurement Sensors", cap: "phMeasurement", attr: "pH"],
		[title: "Power Meters", cap: "powerMeter", attr: "power"],
		[title: "Power Sources", cap: "powerSource", attr: "powerSource"],
		[title: "Presence Sensors", cap: "presenceSensor", attr: "presence"],
		[title: "Relative Humidity Measurement Sensors", cap: "relativeHumidityMeasurement", attr: "humidity"],
		[title: "Relay Switches", cap: "relaySwitch", attr: "switch"],
		[title: "Shock Sensors", cap: "shockSensor", attr: "shock"],
		[title: "Signal Strength Sensors", cap: "signalStrength", attr: ["lqi", "rssi"]],
		[title: "Sleep Sensors", cap: "sleepSensor", attr: "sleeping"],
		[title: "Smoke Detectors", cap: "smokeDetector", attr: "smoke"],
		[title: "Sound Pressure Level Sensors", cap: "soundPressureLevel", attr: "soundPressureLevel"],
		[title: "Sound Sensors", cap: "soundSensor", attr: "sound"],
		[title: "Speech Recognition Sensors", cap: "speechRecognition", attr: "phraseSpoken"],
		[title: "Switches", cap: "switch", attr: "switch"],
		[title: "Switch Level Sensors", cap: "switchLevel", attr: "level"],
		[title: "Tamper Alert Sensors", cap: "tamperAlert", attr: "tamper"],
		[title: "Temperature Measurement Sensors", cap: "temperatureMeasurement", attr: "temperature"],
		[title: "Thermostats", cap: "thermostat", attr: ["coolingSetpoint", "heatingSetpoint", "temperature", "thermostatFanMode", "thermostatMode", "thermostatOperatingState", "thermostatSetpoint"]],
		[title: "Three Axis Sensors", cap: "threeAxis", attr: "threeAxis"],
		[title: "Touch Sensors", cap: "touchSensor", attr: "touch"],
		[title: "Ultraviolet Index Sensors", cap: "ultravioletIndex", attr: "ultravioletIndex"],
		[title: "Valves", cap: "valve", attr: "valve"],
		[title: "Voltage Measurement Sensors", cap: "voltageMeasurement", attr: "voltage"],
		[title: "Water Sensors", cap: "waterSensor", attr: "water"],
		[title: "Window Shades", cap: "windowShade", attr: "windowShade"]
	]
}

// private averageSupportedAttributes() {
	// [
		// "battery",
		// "carbonDioxide",
		// "colorTemperature",
		// "coolingSetpoint",
		// "energy",
		// "heatingSetpoint",
		// "humidity",
		// "illuminance",
		// "level",
		// "lqi",
		// "pH",
		// "power",
		// "rssi",
		// "soundPressureLevel",
		// "temperature",
		// "thermostatSetpoint",
		// "ultravioletIndex",
		// "voltage"
	// ]
// }

private getArchiveTypeOptions() {
	[
		[name: "None"],
		[name: "Out of Space"],
		[name: "Weeks", defaultVal: 2, range: "1..52"],
		[name: "Events", defaultVal: 25000, range: "1000..100000"]
	]
}


private getWebAppName() {
	return "Google Sheets Web App"
}

private getWebAppBaseUrl() {
	return "https://script.google.com/macros/s/"
}

private getWebAppBaseUrl2() {
	return "https://script.google.com/macros/u/"
}

long safeToLong(val, defaultVal=0) {
	try {
		if (val && (val instanceof Long || "${val}".isLong())) {
			return "$val".toLong()
		}
		else {
			return defaultVal
		}
	}
	catch (e) {
		return defaultVal
	}
}

private logDebug(msg) {
	if (loggingTypeEnabled("debug")) {
		log.debug msg
	}
}

private logTrace(msg) {
	if (loggingTypeEnabled("trace")) {
		log.trace msg
	}
}

private logInfo(msg) {
	if (loggingTypeEnabled("info")) {
		log.info msg
	}
}

private logWarn(msg) {
	log.warn msg
}

private loggingTypeEnabled(loggingType) {
	return (!settings?.logging || settings?.logging?.contains(loggingType))
}

W kolejnym kroku po wyświetleniu komunikatu „Created SmartApp”, musimy włączyć OAuth w ustawieniach SmartApp. W tym celu klikamy App Settings. Rozwijamy „OAuth” i klikamy Enable OAuth in Smart App. Na koniec klikamy „Update”. Wracamy do kodu klikając Code po prawej na samej górze strony.

Kończąc instalację, kliknij Publish, a następnie For Me w prawym górnym rogu. Pojawi się kończący komunikat „SmartApp published successfully”.

Teraz możemy przejść do dodania SmartApps do ekosystemu SmartThings. Otwieramy aplikację na smartfonie. Na ekranie głównym aplikacji, kliknij Dodaj ( + ), następnie wybierz Smart App. Wybierz instalowaną wcześniej aplikację, czyli Simple Event Logger.

Teraz możemy przystąpić do konfiguracji. Wybierz wszystkie urządzenia, dla których chcesz rejestrować dane. Większość urządzeń powinna być widoczna w sekcji „Actuators” i „Sensors”. Wybierając urządzenie, mówisz aplikacji SmartApp, że powinna rejestrować urządzenia. To, które dane ma rejestrować, jest określone w innej sekcji ustawień, więc nie ma znaczenia, z którego pola wybierzesz urządzenie.

Po wybraniu urządzeń, dla których chcesz rejestrować dane, przewiń w dół do sekcji „Choose Events” i wybierz konkretne parametry, które mają być rejestrowane dla wszystkich urządzeń.

Zmień „Opcje rejestrowania” (w razie potrzeby):

  • Log Events Every (Rejestruj zdarzenia co): określa częstotliwość publikowania nowych danych w arkuszu Google.
  • Maximum number of events to log for each device per section (Maksymalna liczba zdarzeń do zarejestrowania dla każdego urządzenia na sekcję): Kiedy aplikacja SmartApp zapisuje dane z czujników, pobiera od (1 do 50) zdarzeń z urządzenia od ostatniego uruchomienia, w zależności od ustawienia. Ustawienie zbyt wysokiej tej liczby może spowodować, że aplikacja SmartApp osiągnie 20-sekundowy limit wykonywania, a ustawienie jej zbyt niskiego może spowodować, że niektóre zdarzenia nie zostaną zarejestrowane.
  • Log Event Description (Opis zdarzenia w dzienniku): Określa, czy opis zdarzenia jest rejestrowany. Arkusze Google są ograniczone do 2 milionów komórek, więc mogą pomieścić około 400 000 wydarzeń, ale tylko wtedy, gdy usunięto kolumny FZ i nie dodano żadnych innych arkuszy.
  • Delete Extra Columns (Usuń dodatkowe kolumny): automatycznie usuwa kolumny FZ, jeśli są puste, co umożliwia rejestrowanie większej liczby zdarzeń.

Wklej adres URL aplikacji internetowej skopiowany wcześniej z pola „URL aplikacji internetowej Google” .

  • Adres URL powinien zaczynać się od „https://script.google.com/macros/s/&#8221;
  • Jeśli Twój adres URL nie zaczyna się w ten sposób, SmartApp nie będzie działać, więc musisz wrócić do Edytora skryptów Arkuszy Google i skopiować adres URL z ekranu publikowania.

Na koniec nadaj dowolną nazwę SmartApps w sekcji „Assign a name”. Po wypełnieniu wszystkich wymaganych informacji i dotknięciu opcji Gotowe. Po upływie zaplanowanego czasu interwału czasowego na zarejestrowanie danych w arkuszu kalkulacyjnym powinny pojawić się nowe zdarzenia.

Usunięcie SmartApp

Jeśli aplikacja SmartApp nie działa zgodnie z oczekiwaniami lub nie jest już potrzebna, można ją usunąć z aplikacji SmartThings. Aby usunąć SmartApp, wykonaj następujące kroki:

  1. Na ekranie głównym wybierz Menu (☰) i dotknij SmartApps.
  2. Wybierz Więcej opcji (⋮) i dotknij Usuń.
  3. Wybierz Minus (-)  obok żądanej aplikacji SmartApp.
  4. Stuknij Usuń, aby potwierdzić.
Więcej informacji znajdziesz tutaj.

One thought on “Jak zapisywać wszystkie dane SmartThings ?

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s