Manuel Alejandro Iglesias Abbatemarco
Published © GPL3+

Smart Dehydrator Cooking Appliance - Alexa Ready

A smart dehydrator that uses Home Skill API and low cost arduino to produce better dehydrated foods with less effort.

AdvancedFull instructions providedOver 8 days3,993
Smart Dehydrator Cooking Appliance - Alexa Ready

Things used in this project

Hardware components

Arduino Mini 05
Arduino Mini 05
×1
ESP8266 ESP-01
Espressif ESP8266 ESP-01
×1
Breadboard (generic)
Breadboard (generic)
×1
Relay (generic)
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

simplified schematic

The core with Arduino and ESP

Code

micropython binary for esp-01

MicroPython
Flash on a ESP-01, look for instructions in story.
No preview (download only).

lambda.py

Python
Use in AWS
# -*- coding: utf-8 -*-

# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Amazon Software License (the "License"). You may not use this file except in
# compliance with the License. A copy of the License is located at
#
#    http://aws.amazon.com/asl/
#
# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific
# language governing permissions and limitations under the License.

"""Alexa Smart Home Lambda Function Sample Code.

This file demonstrates some key concepts when migrating an existing Smart Home skill Lambda to
v3, including recommendations on how to transfer endpoint/appliance objects, how v2 and vNext
handlers can be used together, and how to validate your v3 responses using the new Validation
Schema.

Note that this example does not deal with user authentication, only uses virtual devices, omits
a lot of implementation and error handling to keep the code simple and focused.
"""

import logging
import time
import json
import uuid

import boto3

client = boto3.client('iot-data')

# Imports for v3 validation
from validation import validate_message

# Setup logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# To simplify this sample Lambda, we omit validation of access tokens and retrieval of a specific
# user's appliances. Instead, this array includes a variety of virtual appliances in v2 API syntax,
# and will be used to demonstrate transformation between v2 appliances and v3 endpoints.
SAMPLE_APPLIANCES = [
    {
        "applianceId": "smart-ronco-endpoint",
        "manufacturerName": "Hackster Contest MAIA",
        "modelName": "Smart Dehydrator",
        "version": "1",
        "friendlyName": "dehydrator",
        "friendlyDescription": "A modified ronco Dehydrator that automate the process",
        "isReachable": True,
        "actions": [
            "turnOn",
            "turnOff",
            "thermostatMode",
            "setTargetTemperature",
            "incrementTargetTemperature",
            "decrementTargetTemperature",
            "getTargetTemperature",
            "getTemperatureReading"
        ],
        "additionalApplianceDetails": {
            "detail1": "An arduino controlled modified Ronco Dehydrator"
        }
    }
]

def dehydrator_temperature(setpoint):
    # Change topic, qos and payload
    response = client.update_thing_shadow(
        thingName = 'dehydrator', 
        payload = json.dumps({
            'state': {
                'desired': {
                    'setpoint': setpoint
                }
            }
        })
    )

def dehydrator_state(state):
    # Change topic, qos and payload
    response = client.update_thing_shadow(
        thingName = 'dehydrator', 
        payload = json.dumps({
            'state': {
                'desired': {
                    'unitpower': state
                }
            }
        })
    )
    
def get_dehydrator_temperature():
    response = client.get_thing_shadow(thingName='dehydrator')
    streamingBody = response["payload"]
    jsonState = json.loads(streamingBody.read())
    return jsonState["state"]["reported"]["temperature"]

def lambda_handler(request, context):
    """Main Lambda handler.

    Since you can expect both v2 and v3 directives for a period of time during the migration
    and transition of your existing users, this main Lambda handler must be modified to support
    both v2 and v3 requests.
    """

    try:
        logger.info("Directive:")
        logger.info(json.dumps(request, indent=4, sort_keys=True))

        version = get_directive_version(request)

        if version == "3":
            logger.info("Received v3 directive!")
            if request["directive"]["header"]["name"] == "Discover":
                response = handle_discovery_v3(request)
            else:
                response = handle_non_discovery_v3(request)

        else:
            logger.info("Received v2 directive!")
            if request["header"]["namespace"] == "Alexa.ConnectedHome.Discovery":
                response = handle_discovery()
            else:
                response = handle_non_discovery(request)

        logger.info("Response:")
        logger.info(json.dumps(response, indent=4, sort_keys=True))

        if version == "3":
            logger.info("Validate v3 response")
            validate_message(request, response)

        return response
    except ValueError as error:
        logger.error(error)
        raise

# v2 handlers
def handle_discovery():
    header = {
        "namespace": "Alexa.ConnectedHome.Discovery",
        "name": "DiscoverAppliancesResponse",
        "payloadVersion": "2",
        "messageId": get_uuid()
    }
    payload = {
        "discoveredAppliances": SAMPLE_APPLIANCES
    }
    response = {
        "header": header,
        "payload": payload
    }
    return response

def handle_non_discovery(request):
    request_name = request["header"]["name"]

    if request_name == "TurnOnRequest":
        header = {
            "namespace": "Alexa.ConnectedHome.Control",
            "name": "TurnOnConfirmation",
            "payloadVersion": "2",
            "messageId": get_uuid()
        }
        payload = {}
    elif request_name == "TurnOffRequest":
        header = {
            "namespace": "Alexa.ConnectedHome.Control",
            "name": "TurnOffConfirmation",
            "payloadVersion": "2",
            "messageId": get_uuid()
        }
    # other handlers omitted in this example
    payload = {}
    response = {
        "header": header,
        "payload": payload
    }
    return response

# v2 utility functions
def get_appliance_by_appliance_id(appliance_id):
    for appliance in SAMPLE_APPLIANCES:
        if appliance["applianceId"] == appliance_id:
            return appliance
    return None

def get_utc_timestamp(seconds=None):
    return time.strftime("%Y-%m-%dT%H:%M:%S.00Z", time.gmtime(seconds))

def get_uuid():
    return str(uuid.uuid4())

# v3 handlers
def handle_discovery_v3(request):
    endpoints = []
    for appliance in SAMPLE_APPLIANCES:
        endpoints.append(get_endpoint_from_v2_appliance(appliance))

    response = {
        "event": {
            "header": {
                "namespace": "Alexa.Discovery",
                "name": "Discover.Response",
                "payloadVersion": "3",
                "messageId": get_uuid()
            },
            "payload": {
                "endpoints": endpoints
            }
        }
    }
    return response

def handle_non_discovery_v3(request):
    request_namespace = request["directive"]["header"]["namespace"]
    request_name = request["directive"]["header"]["name"]

    logger.info("Requested Namespace: %s" % request_namespace)
    logger.info("Requested Name: %s" % request_name)

    if request_namespace == "Alexa.PowerController":
        logger.info("Handle PowerController")
        if request_name == "TurnOn":
            dehydrator_state("on")
            value = "ON"
        else:
            dehydrator_state("off")
            value = "OFF"

        response = {
            "context": {
                "properties": [
                    {
                        "namespace": "Alexa.PowerController",
                        "name": "powerState",
                        "value": value,
                        "timeOfSample": get_utc_timestamp(),
                        "uncertaintyInMilliseconds": 500
                    }
                ]
            },
            "event": {
                "header": {
                    "namespace": "Alexa",
                    "name": "Response",
                    "payloadVersion": "3",
                    "messageId": get_uuid(),
                    "correlationToken": request["directive"]["header"]["correlationToken"]
                },
                "endpoint": {
                    "scope": {
                        "type": "BearerToken",
                        "token": "access-token-from-Amazon"
                    },
                    "endpointId": request["directive"]["endpoint"]["endpointId"]
                },
                "payload": {}
            }
        }
        return response

    elif request_namespace == "Alexa.Authorization":
        if request_name == "AcceptGrant":
            response = {
                "event": {
                    "header": {
                        "namespace": "Alexa.Authorization",
                        "name": "AcceptGrant.Response",
                        "payloadVersion": "3",
                        "messageId": "5f8a426e-01e4-4cc9-8b79-65f8bd0fd8a4"
                    },
                    "payload": {}
                }
            }
            return response

    elif request_namespace == "Alexa.ThermostatController":
        logger.info("Handle ThermostatController")
        if request_name == "SetTargetTemperature":
            request_value = request["directive"]["payload"]["targetSetpoint"]["value"]
            dehydrator_temperature(request_value)
            response = {
                "context": {
                    "properties": [ {
                        "namespace": "Alexa.ThermostatController",
                        "name": "targetSetpoint",
                        "value": {
                            "value": request_value,
                            "scale": "CELSIUS"
                        },
                        "timeOfSample": get_utc_timestamp(),
                        "uncertaintyInMilliseconds": 500
                    }, {
                      "namespace": "Alexa.ThermostatController",
                      "name": "thermostatMode",
                      "value": "AUTO",
                      "timeOfSample": get_utc_timestamp(),
                      "uncertaintyInMilliseconds": 500
                    } ]
                },
                "event": {
                    "header": {
                        "namespace": "Alexa",
                        "name": "Response",
                        "payloadVersion": "3",
                        "messageId": get_uuid(),
                        "correlationToken": request["directive"]["header"]["correlationToken"]
                    },
                    "endpoint": {
                        "endpointId": request["directive"]["endpoint"]["endpointId"]
                    },
                    "payload": {}
                }
            }
            return response
    elif request_namespace == "Alexa":
        logger.info("Handle TemperatureSensor")
        if request_name == "ReportState":
            logger.info("Handle ReportState")
            value = get_dehydrator_temperature()
            response = {
                "context": {
                    "properties": [{
                    "namespace": "Alexa.TemperatureSensor",
                    "name": "temperature",
                    "value": {
                        "value": value,
                        "scale": "CELSIUS"
                    },
                    "timeOfSample": get_utc_timestamp(),
                    "uncertaintyInMilliseconds": 1000
                    } ]
                },
                "event": {
                    "header": {
                    "namespace": "Alexa",
                    "name": "StateReport",
                    "payloadVersion": "3",
                    "messageId": get_uuid(),
                    "correlationToken": request["directive"]["header"]["correlationToken"]
                },
                "endpoint": {
                    "endpointId": request["directive"]["endpoint"]["endpointId"]
                },
                "payload": {}
                }
            }
            return response

# v3 utility functions
def get_endpoint_from_v2_appliance(appliance):
    endpoint = {
        "endpointId": appliance["applianceId"],
        "manufacturerName": appliance["manufacturerName"],
        "friendlyName": appliance["friendlyName"],
        "description": appliance["friendlyDescription"],
        "displayCategories": [],
        "cookie": appliance["additionalApplianceDetails"],
        "capabilities": []
    }
    endpoint["displayCategories"] = get_display_categories_from_v2_appliance(appliance)
    endpoint["capabilities"] = get_capabilities_from_v2_appliance(appliance)
    return endpoint

def get_directive_version(request):
    try:
        return request["directive"]["header"]["payloadVersion"]
    except:
        try:
            return request["header"]["payloadVersion"]
        except:
            return "-1"

def get_endpoint_by_endpoint_id(endpoint_id):
    appliance = get_appliance_by_appliance_id(endpoint_id)
    if appliance:
        return get_endpoint_from_v2_appliance(appliance)
    return None

def get_display_categories_from_v2_appliance(appliance):
    model_name = appliance["modelName"]
    if model_name == "Smart Switch": displayCategories = ["SWITCH"]
    elif model_name == "Smart Light": displayCategories = ["LIGHT"]
    elif model_name == "Smart White Light": displayCategories = ["LIGHT"]
    elif model_name == "Smart Thermostat": displayCategories = ["THERMOSTAT"]
    elif model_name == "Smart Lock": displayCategories = ["SMARTLOCK"]
    elif model_name == "Smart Scene": displayCategories = ["SCENE_TRIGGER"]
    elif model_name == "Smart Activity": displayCategories = ["ACTIVITY_TRIGGER"]
    elif model_name == "Smart Camera": displayCategories = ["CAMERA"]
    elif model_name == "Smart Dehydrator": displayCategories = ["THERMOSTAT", "TEMPERATURE_SENSOR" ]
    else: displayCategories = ["OTHER"]
    return displayCategories

def get_capabilities_from_v2_appliance(appliance):
    model_name = appliance["modelName"]
    if model_name == "Smart Dehydrator":
        capabilities = [
            {
                "type": "AlexaInterface",
                "interface": "Alexa.PowerController",
                "version": "3",
                "properties": {
                    "supported": [
                        { "name": "powerState" }
                    ],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.ThermostatController",
                "version": "3",
                "properties": {
                    "supported": [
                        { "name": "targetSetpoint" },
                        { "name": "thermostatMode" }
                    ],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            },
            {
                "type": "AlexaInterface",
                "interface": "Alexa.TemperatureSensor",
                "version": "3",
                "properties": {
                    "supported": [
                        { "name": "temperature" }
                    ],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            }
        ]
    else:
        # in this example, just return simple on/off capability
        capabilities = [
            {
                "type": "AlexaInterface",
                "interface": "Alexa.PowerController",
                "version": "3",
                "properties": {
                    "supported": [
                        { "name": "powerState" }
                    ],
                    "proactivelyReported": True,
                    "retrievable": True
                }
            }
        ]

    # additional capabilities that are required for each endpoint
    endpoint_health_capability = {
        "type": "AlexaInterface",
        "interface": "Alexa.EndpointHealth",
        "version": "3",
        "properties": {
            "supported":[
                { "name":"connectivity" }
            ],
            "proactivelyReported": True,
            "retrievable": True
        }
    }
    alexa_interface_capability = {
        "type": "AlexaInterface",
        "interface": "Alexa",
        "version": "3"
    }
    capabilities.append(endpoint_health_capability)
    capabilities.append(alexa_interface_capability)
    return capabilities

bool_rw_property_aws.js

JavaScript
Use it under freeboard node.js
// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ freeboard-bool-property-RW-plugin AWS                              │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ The Alexa and Arduino Smart Home Challenge                         │ \\
// | (URL here)                                                         | \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license.                                    │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Freeboard widget plugin.                                           │ \\
// └────────────────────────────────────────────────────────────────────┘ \\
(function () {
    //
    // DECLARATIONS
    //
    var LOADING_INDICATOR_DELAY = 1000;

    //

    freeboard.loadWidgetPlugin({
        type_name: "bool_rw_property",
        display_name: "Boolean Property Control",
        description: "Boolean property which can send a value as well as receive",
        settings: [
            {
                name: "title",
                display_name: "Title",
                type: "text"
            },
            {
                name: "config_data",
                display_name: "Configuration Data",
                type: "calculated"
            },
            {
                name: "property",
                display_name: "Property Name",
                type: "text"
            },
            {
                name: "on_value",
                display_name: "On Value",
                type: "text"
            },
            {
                name: "off_value",
                display_name: "Off Value",
                type: "text"
            },            
            {
                name: "ds_name",
                display_name: "Datasource Name",
                type: "text"
            },            
            {
                name: "topicName",
                display_name: "Topic name",
                type: "text"
            },
            {
                name: "on_text",
                display_name: "On Text",
                type: "calculated"
            },
            {
                name: "off_text",
                display_name: "Off Text",
                type: "calculated"
            }

        ],
        newInstance: function (settings, newInstanceCallback) {
            newInstanceCallback(new bool_rw_property(settings));
        }
    });

    freeboard.addStyle('.indicator-light.interactive:hover', "box-shadow: 0px 0px 15px #FF9900; cursor: pointer;");
    var bool_rw_property = function (settings) {
        var self = this;
        var titleElement = $('<h2 class="section-title"></h2>');
        var stateElement = $('<div class="indicator-text"></div>');
        var indicatorElement = $('<div class="indicator-light interactive"></div>');
        var currentSettings = settings;
        var isOn;
        var setValue = false;
        var jsonobj;
        var onText;
        var offText;
        var url;
        var myWorker;
        var updateShadow = false;

        let delay = 500;
        let timerId = setTimeout(function request() {
            var datasource = freeboard.getDatasourceInstance(currentSettings.ds_name);
            if(datasource.datasourceInstance.isConnected() === false){
                delay = 500;
                timerId = setTimeout(request, delay);
            }else{
                clearTimeout(timerId);
                updateThingShadow();
            }
        }, delay);

        if (!!window.SharedWorker) {
            console.log('Create New Shared Worker - bool property');
            myWorker = new SharedWorker("lib/js/thirdparty/worker.js");

            myWorker.port.onmessage = function(e) {
                var cmd = e.data.split(":");
                switch (cmd[0]){
                    case "debug":
                        console.log(cmd[1]);
                    break;
                    case "onconnect":
                        // do nothing
                    break;
                    case "data":
                        var data = String(e.data);
                        var response = data.substring('data:'.length);
                        console.log("bool response: "+response);
                        var json_obj = JSON.parse(response);
                        console.log(json_obj);
                        var reported = json_obj["reported"];
                        console.log(reported);
                        isOn = reported["state"];
                        updateState();
                        if(updateShadow){
                            updateThingShadow();
                            updateShadow = false
                        }
                    break;
                }
            }
        }
        
        function setOnProperty(property){
            var json_obj = new Object();
            var set_property = json_obj["direct-cmd"] = {};
            set_property.state = property;
            set_property.notify = 'on';        // property;
            console.log(json_obj);
            myWorker.port.postMessage(json_obj);
        }

        function updateState() {
            if (isOn === "on") {
                if(setValue === false){
                    setValue = true;
                    updateShadow = true;
                }
                stateElement.text((_.isUndefined(onText) ? (_.isUndefined(currentSettings.on_text) ? "" : currentSettings.on_text) : onText));
                indicatorElement.toggleClass("on", true);
            }
            else if (isOn === "off") {
                if(setValue === true){
                    setValue = false;
                    updateShadow = true;
                }
                stateElement.text((_.isUndefined(offText) ? (_.isUndefined(currentSettings.off_text) ? "" : currentSettings.off_text) : offText));
                indicatorElement.toggleClass("on", false);
            }
        }

        function updateThingShadow() {
            var json_obj = new Object();
            json_obj.state = {};
            var json_reported = json_obj.state.reported = {};
            if(setValue === true){
                json_reported[currentSettings.property] = currentSettings.on_value;
            }else if(setValue === false){
                json_reported[currentSettings.property] = currentSettings.off_value;
            }
            var jsonString= JSON.stringify(json_obj);
            console.log("json: " + jsonString);
            var datasource = freeboard.getDatasourceInstance(currentSettings.ds_name);
            datasource.datasourceInstance.publish(currentSettings.topicName, jsonString);
        }

        function updateDesiredThingShadow(){
            var json_obj = new Object();
            json_obj.state = {};
            var json_desired = json_obj.state.desired = {};
            if(setValue === true){
                // if device is On send desired new styate OFF
                console.log("Send desire to switch Unit OFF");
                json_desired[currentSettings.property] = currentSettings.off_value;
            }else if(setValue === false){
                // if device is Off send desired new styate ON
                console.log("Send desire to switch Unit ON");
                json_desired[currentSettings.property] = currentSettings.on_value;
            }
            var jsonString= JSON.stringify(json_obj);
            console.log("json: " + jsonString);
            var datasource = freeboard.getDatasourceInstance(currentSettings.ds_name);
            datasource.datasourceInstance.publish(currentSettings.topicName, jsonString);            
        }

        this.onClick = function(e) {
            updateDesiredThingShadow();
            e.preventDefault();

            // var json_obj = new Object();
            // json_obj.state = {};
            // var json_desired = json_obj.state.desired = {};
            // if(setValue){
            //     json_desired[currentSettings.property] = currentSettings.off_value;
            // }else{
            //     json_desired[currentSettings.property] = currentSettings.on_value;
            // }
            // var jsonString= JSON.stringify(json_obj);
            // console.log("json: " + jsonString);
            // // var datasources = freeboard.getDatasourceSettings("Dehydrator");
            // // var datasource = freeboard.getDatasourceInstance("Dehydrator");
            // var datasource = freeboard.getDatasourceInstance(currentSettings.ds_name);
            // // console.log("datasources settings:");
            // // console.log(datasources);
            // // console.log("datasources object:");
            // // console.log(datasource);
            // // console.log("click button received");
            // datasource.datasourceInstance.publish(currentSettings.topicName, jsonString);


            // var new_val = !isOn;
            // jsonobj[currentSettings.property] = new_val;
            // var new_jsondata = JSON.stringify(jsonobj);
            // this.onCalculatedValueChanged('config_data', jsonobj);
            // url = currentSettings.url;
            // if (_.isUndefined(url))
            //     freeboard.showDialog($("<div align='center'>url undefined</div>"), "Error!", "OK", null, function () {
            //     });
            // else {
            //     this.sendValue(url, new_jsondata);
            // }
        }


        this.render = function (element) {
            $(element).append(titleElement).append(indicatorElement).append(stateElement);
            $(indicatorElement).click(this.onClick.bind(this));
        }

        this.onSettingsChanged = function (newSettings) {
            currentSettings = newSettings;
            titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title));
            updateState();
        }

        this.onCalculatedValueChanged = function (settingName, newValue) {
            console.log(settingName);
            console.log(newValue);
            if (settingName == "config_data") {
                jsonobj = newValue;
                if (jsonobj.hasOwnProperty(currentSettings.property)) {
                    console.log("receive property");
                    var property = jsonobj[currentSettings.property];
                    setOnProperty(property);
                }
            }
            if (settingName == "on_text") {
                onText = newValue;
            }
            if (settingName == "off_text") {
                offText = newValue;
            }
        }

        var request;

        this.sendValue = function (url, json_response) {
            console.log(url, json_response);
            request = new XMLHttpRequest();
            if (!request) {
                console.log('Giving up :( Cannot create an XMLHTTP instance');
                return false;
            }
            request.onreadystatechange = this.alertContents;
            request.open('POST', url, true);
            request.setRequestHeader("Content-type", "application/json");
            freeboard.showLoadingIndicator(true);
            request.send(json_response);
        }

        this.alertContents = function () {
            if (request.readyState === XMLHttpRequest.DONE) {
                if (request.status === 200) {
                    console.log(request.responseText);
                    setTimeout(function () {
                        freeboard.showLoadingIndicator(false);
                        //freeboard.showDialog($("<div align='center'>Request response 200</div>"),"Success!","OK",null,function(){});
                    }, LOADING_INDICATOR_DELAY);
                } else {
                    console.log('There was a problem with the request.');
                    setTimeout(function () {
                        freeboard.showLoadingIndicator(false);
                        freeboard.showDialog($("<div align='center'>There was a problem with the request. Code " + request.status + request.responseText + " </div>"), "Error!", "OK", null, function () {
                        });
                    }, LOADING_INDICATOR_DELAY);
                }

            }

        }

        this.onDispose = function () {
        }

        this.getHeight = function () {
            return 1;
        }

        this.onSettingsChanged(settings);
    };

}());

gauge_rw_property_aws.js

JavaScript
use with freeboard node.js
// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ freeboard-bool-property-RW-plugin AWS                              │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ The Alexa and Arduino Smart Home Challenge                         │ \\
// | (URL here)                                                         | \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license.                                    │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Freeboard widget plugin.                                           │ \\
// └────────────────────────────────────────────────────────────────────┘ \\

(function () {

    freeboard.loadWidgetPlugin({
        type_name: "gaugeWidget",
        display_name: "AWS IoT Gauge",
        "external_scripts" : [
            "plugins/thirdparty/raphael.2.1.0.min.js",
            "plugins/thirdparty/justgage.1.0.1.js"
        ],
        settings: [
            {
                name: "title",
                display_name: "Title",
                type: "text"
            },
            {
                name: "config_data",
                display_name: "Configuration Data",
                type: "calculated"
            },
            {
                name: "property",
                display_name: "Property Name",
                type: "text"
            },
            {
                name: "var_name",
                display_name: "Variable Name",
                type: "text"
            },
            {
                name: "ds_name",
                display_name: "Datasource Name",
                type: "text"
            },            
            {
                name: "topicName",
                display_name: "Topic name",
                type: "text"
            },
            {
                name: "units",
                display_name: "Units",
                type: "text"
            },
            {
                name: "min_value",
                display_name: "Minimum",
                type: "text",
                default_value: 0
            },
            {
                name: "max_value",
                display_name: "Maximum",
                type: "text",
                default_value: 100
            }
        ],
        newInstance: function (settings, newInstanceCallback) {
            newInstanceCallback(new gaugeWidget(settings));
        }
    });


    var gaugeID = 0;
	freeboard.addStyle('.gauge-widget-wrapper', "width: 100%;text-align: center;");
	freeboard.addStyle('.gauge-widget', "width:200px;height:160px;display:inline-block;");

    var gaugeWidget = function (settings) {
        var self = this;

        var thisGaugeID = "gauge-" + gaugeID++;
        var titleElement = $('<h2 class="section-title"></h2>');
        var gaugeElement = $('<div class="gauge-widget" id="' + thisGaugeID + '"></div>');

        var gaugeObject;
        var rendered = false;

        var jsonobj;
        var currentSettings = settings;
        var update_property = true;

		if (!!window.SharedWorker) {
			console.log('Create New Shared Worker - '+thisGaugeID);
			myWorker = new SharedWorker("lib/js/thirdparty/worker.js");

			myWorker.port.onmessage = function(e) {
				var cmd = e.data.split(":");
				switch (cmd[0]){
					case "debug":
						console.log(cmd[1]);
					break;
					case "onconnect":
						//do nothing here
					break;
					case "data":
						var data = String(e.data);
						var response = data.substring('data:'.length);
						console.log("gauge response: "+response);
						var json_obj = JSON.parse(response);
						console.log(json_obj);
						var reported = json_obj["reported"];
						var temperature = reported["temperature"];
						temperature = parseFloat(temperature).toFixed(1);
						if (!_.isUndefined(gaugeObject)) {
							gaugeObject.refresh(temperature);
						}
						if(update_property === true){
							if(json_obj.hasOwnProperty(currentSettings.property)){
								var prop_val = json_obj[currentSettings.property];
								reportThingShadow(currentSettings.property, prop_val);
								update_property = false;
							}
						}
						reportThingShadow(currentSettings.var_name, temperature);
					break;	
				}
			}
		}

        function setGaugeProperty(value){
            var json_obj = new Object();
            var set_property = json_obj["direct-cmd"] = {};	
            set_property[currentSettings.property] = value;
            console.log("setGaugeProperty - send json cmd:");
            console.log(json_obj);
            myWorker.port.postMessage(json_obj);
            // this way we request the report of setpoint once is available
            update_property = true;
        }

        function reportThingShadow(param, value) {
            var json_obj = new Object();
            json_obj.state = {};
            var json_reported = json_obj.state.reported = {};
            json_reported[param] = value;
            var jsonString = JSON.stringify(json_obj);
            console.log("json: " + jsonString);
            var datasource = freeboard.getDatasourceInstance(currentSettings.ds_name);
            datasource.datasourceInstance.publish(currentSettings.topicName, jsonString);
        }

        function createGauge() {
            if (!rendered) {
                return;
            }

            gaugeElement.empty();
            gaugeObject = new JustGage({
                id: thisGaugeID,
                value: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value),
                min: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value),
                max: (_.isUndefined(currentSettings.max_value) ? 0 : currentSettings.max_value),
                label: currentSettings.units,
                showInnerShadow: false,
                valueFontColor: "#d3d4d4"
            });
        }

        this.render = function (element) {
            rendered = true;
            $(element).append(titleElement).append($('<div class="gauge-widget-wrapper"></div>').append(gaugeElement));
            createGauge();
        }

        this.onSettingsChanged = function (newSettings) {
            if (newSettings.min_value != currentSettings.min_value || newSettings.max_value != currentSettings.max_value || newSettings.units != currentSettings.units) {
                currentSettings = newSettings;
                createGauge();
            }
            else {
                currentSettings = newSettings;
            }

            titleElement.html(newSettings.title);
        }

        this.onCalculatedValueChanged = function (settingName, newValue) {
        	if (settingName == "config_data") {
        		var value;
                jsonobj = newValue;
                if (jsonobj.hasOwnProperty(currentSettings.property)) {
                    value = jsonobj[currentSettings.property];
                    setGaugeProperty(value);
                }   
	        }
        }

        this.onDispose = function () {
        }

        this.getHeight = function () {
            return 3;
        }

        this.onSettingsChanged(settings);
    };

}());

worker.js

JavaScript
This is the javascript shared worker code for freeboard node.js
var ws;
var ports = [];

function start() {
	if (typeof ws === 'undefined') {
		ws = new WebSocket("ws://192.168.1.80:80");
		ws.onopen = function () {
			for(var i=0; i<ports.length; i++){
				ports[i].postMessage("onconnect:onopen websocket");
			}
		};
		ws.onmessage = function (evt) {
			for(var i=0; i<ports.length; i++){
				ports[i].postMessage("data:"+evt.data);
			}
		};
	}
}

onconnect = function (e) {
	start();
	var port = e.ports[0];

	ports.push(port);

	port.onmessage = function (e) {
		port.postMessage("debug:received message");
		json_obj = e.data;
		if(ws.readyState === WebSocket.OPEN){
		    var json_string = JSON.stringify(json_obj);
		    ws.send(json_string+'\n');
		}
		
	};
}

dehydrator sources

Arduino and ESP-01 dehydrator sources

Arduino OTA update sources

Flash Arduino using esp8266 running micropython.

Credits

Manuel Alejandro Iglesias Abbatemarco

Manuel Alejandro Iglesias Abbatemarco

17 projects • 77 followers
Embedded Firmware Engineer

Comments