Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

BACI basics

In this section we will modify our smart bulb ACS component in order to make it BACI compliant. BACI is a control interface specification we use for ACS at ALMA. For more details this document is available. There is also another useful tutorial here. BACI is fully available for C++ components only. Java and Python have partial BACI implementations. Because of this we will implement this component in C++.

...

  • AComponent is a CORBA object that corresponds to a conceptually self contained  contained entity: this can be a physical device or a business component that implements certain business logic.
  • A Characteristic is a data item that is considered static. It can change by context but not by the controller entity.
  • Each component has a number of component properties that are controlled, e.g. electric current, status, position, etc.
  • A property is a typed changeable value in a context. Properties are represented by interfaces. Depending on the nature of the property, the value that the property interface encapsulates can be either read (by using property'saccessormethods), set (by using property'smutatormethods) or both. In this regard properties can be flagged as readable, writable or read-writable.
  • An action is a command with the following properties:
    • It is issued asynchronously.
    • When the command finishes the execution, a notification is sent from the remote object to the object that issued the command; this notification must contain the description of the command execution status (successful or failure, reason for possible failure, timestamp).
    • The action has a well defined execution path, i.e. after executing the command, it must complete its execution either with an error condition or along exactly one execution path that leads to successful completion; the action must complete the execution by itself (e.g. it is illegal to define an action, which when invoked, executes as long as the component containing it exists; the action must terminate on its own).
    • The action termination state (either successful or in error condition) is reported through an universal notification (notification of which the syntax will be defined by this document).
    • An action is timed there exists a mechanism by which the remote object can obtain the timeout interval used by the caller; if the action is not before the timeout interval, the remote object must inform the caller that action is still in progress.
  • When we are defining actions, we must write its arguments and what it returns. Every asynchronous action must have "in ACS::CBvoid cb, in ACS:CBDescIn desc". Note that beside arguments there is also a word "in". Here is short explanation of all possibilities (from Advance CORBA programming with C++):

    • in – the in attribute indicates that the parameter is sent from client to the server
    • out – the out attribute indicates that the parameter is sent from the server to the client
    • inout – The inout attribute indicates a parameter that is initialized by the client and sent to the server.

...

  • Each component can access one and only one controlled device.
  • Characteristics are static variables in the component. They can be read by the controller, but can be set only by the component, depending on context. They are monitoring points.
  • Properties are variables set by the controller. They are control points.
  • It is recommended to implement asynchronous accessors to properties, but not mandatory.

Modifications needed

To make our component a BACI compliant code, we must perform the following modifications:

  1. A single component cannot control multiple physical devices. The component must control a unique physical device, so it must have a "device_id" characteristic, instead of passing it as a parameter.
  2. The "api_region", "api_key", and "api_secret" parameters must also be characteristics of the component, since they are static data as well.
  3. We will need a BACI property called "status", to check the device's current status (On/Off).

Repository

This is the repository with all the code and external libraries needed: https://bitbucket.sco.alma.cl/users/fcaro/repos/acs-baci-smart-bulb. You should be able to insert your API credentials in the component's implementation and run this code as is.

Component's template

Code Block
languageyml
titletuya_bulb_baci.yml
linenumberstrue
working_dir: /home/franciscodeveloper/workspace
prefix: ACSIOT
module: acsiot
component_name: TuyaBulbBaci
functions:
  - 'void turnOn()'
  - 'void turnOff()'
properties:
  - 'readonly attribute ACS::ROboolean status'

Component's IDL

Code Block
languagecpp
titleidlTuyaBulbBaci/idl/TuyaBulbBaci.idl
linenumberstrue
#ifndef _TUYABULBBACI_IDL_
#define _TUYABULBBACI_IDL_
 
#pragma prefix "ACSIOT"
 
#include <baci.idl>
 
module acsiot {
    interface TuyaBulbBaci : ACS::CharacteristicComponent {
        void turnOn();
        void turnOff();
        readonly attribute ACS::ROboolean status;
    };
};

#endif

Component's Makefile

Code Block
languagebash
titlecppTuyaBulbBaci/src/Makefile
...
TuyaBulbBaciImpl_OBJECTS = TuyaBulbBaciImpl hmac_sha256 sha256
TuyaBulbBaciImpl_LIBS = TuyaBulbBaciStubs baci ssl crypto
...

Component's header

Code Block
languagecpp
titlecppTuyaBulbBaci/include/TuyaBulbBaciImpl.h
linenumberstrue
#ifndef _TUYABULBBACI_IMPL_H
#define _TUYABULBBACI_IMPL_H
   
#ifndef __cplusplus
#error This is a C++ include file and cannot be used from plain C
#endif
   
// Base component implementation, including container services and component lifecycle infrastructure
#include <baciCharacteristicComponentImpl.h>
#include <baciROboolean.h>
#include <baciDevIO.h>

// Skeleton interface for server implementation
#include <TuyaBulbBaciS.h>

// Extras
#include "httplib.h"
#include "hmac_sha256.h"
#include "json.hpp"
   
// Error definitions for catching and raising exceptions
template <class T> class TuyaBulbBaciDevIO;

class TuyaBulbBaciImpl : public virtual baci::CharacteristicComponentImpl, public virtual POA_acsiot::TuyaBulbBaci {
    // This is for calling private function getStatus from TuyaBulbBaciDevIO object
    friend TuyaBulbBaciDevIO<bool>;

    public:
        TuyaBulbBaciImpl(const ACE_CString& name, maci::ContainerServices* containerServices);
        virtual ~TuyaBulbBaciImpl();
 
        void turnOn();
        void turnOff();
        // This is the public function to check the BACI property value
        virtual ACS::ROboolean_ptr status();
 
    private:
        const std::string api_region;
        const std::string client_id;
        const std::string api_secret;
        const std::string device_id;
        const std::string base_url;
        std::string token;
        // This is our BACI property
        baci::ROboolean* m_status_p;

        httplib::Result sendRequest(
            const std::string uri,
            const std::string action = "GET",
            const std::map<std::string, std::string> headers_map = {},
            const std::string headers_str = "",
            const std::string body = "",
            const std::string body_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            const std::string version = "v1.0"
        );
        std::string getToken();
        bool getStatus();
};

// DevIO template definition for TuyaBulbBaci
template <class T> class TuyaBulbBaciDevIO : public DevIO<T> {
    public:
        TuyaBulbBaciDevIO(TuyaBulbBaciImpl* component) :
            component(component)
        {};

        virtual ~TuyaBulbBaciDevIO() {};

        virtual bool initializeValue(){ return false; }

        virtual T read(ACS::Time& timestamp) {
            timestamp = ::getTimeStamp();
            return component->getStatus();
        }

        virtual void write(const T& value, ACS::Time& timestamp) {
            // no op, since status is a read only characteristic
        }

    protected:
        TuyaBulbBaciImpl* component;
};

#endif

Component's implementation (click on "Expand source")

Code Block
languagecpp
titlecppTuyaBulbBaci/src/TuyaBulbBaciImpl.cpp
linenumberstrue
collapsetrue
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <TuyaBulbBaciImpl.h>
 
#include <stdexcept>

// Extras
#define SHA256_HASH_SIZE 32
using json = nlohmann::json;

   
TuyaBulbBaciImpl::TuyaBulbBaciImpl(const ACE_CString& name, maci::ContainerServices* containerServices) : CharacteristicComponentImpl(name, containerServices),
    api_region("us"),
    client_id("..."),
    api_secret("..."),
    device_id("..."),
    base_url("https://openapi.tuyaus.com"),
    m_status_p(0)
{
    std::cout << "Constructing TuyaBulbBaciImpl object." << std::endl;
    token = getToken();
    TuyaBulbBaciDevIO<bool>* m_booleanDevIO = new TuyaBulbBaciDevIO<bool>(this);
    m_status_p  = new baci::ROboolean(name + ":status", getComponent(), m_booleanDevIO);
    CHARACTERISTIC_COMPONENT_PROPERTY(status, m_status_p);
    // std::cout << "API Region: " << this->api_region << std::endl;
    // std::cout << "Client ID: " << this->client_id << std::endl;
    // std::cout << "API Secret: " << this->api_secret << std::endl;
    // std::cout << "Device ID: " << this->device_id << std::endl;
    // std::cout << "API Token: " << this->token << std::endl;
}
   
TuyaBulbBaciImpl::~TuyaBulbBaciImpl() {
	if (m_status_p != 0) {
		m_status_p->destroy();
		m_status_p=0;
	}
}
 
void TuyaBulbBaciImpl::turnOn() {
    std::cout << "Turning smart bulb ON..." << std::endl;

    // Set request parameters
    const std::string uri = "/iot-03/devices/" + this->device_id + "/commands";
    const std::string action = "POST";
    const std::string headers_str = "";
    const std::string body = "{\"commands\":[{\"code\":\"switch_led\",\"value\":true}]}";
    const std::string body_sha256 = "8479c9c60cd5d531054c49333c7b361a9ce41b9b313ab8eb6bc9df4141f658ef";
    std::map<std::string, std::string> headers_map;
    headers_map = {
        {"Content-type", "application/json"},
        {"access_token", this->token}
    };

    // Send request and print response
    httplib::Result response = this->sendRequest(uri, action, headers_map, headers_str, body, body_sha256);
    // std::cout << "Response status:\n";
    // std::cout << response->status << std::endl;
    // std::cout << "Response body:\n";
    // std::cout << response->body << std::endl;

    // Check API response
    if (response->status == 200) {
        json body = json::parse(response->body);
        if (body["success"].dump() == "true") {
            std::cout << "Device turned ON successfully." << std::endl;
        } else {
            throw std::runtime_error("Error while turning smart bulb ON.\n");
        }
    }
}
 
void TuyaBulbBaciImpl::turnOff() {
    std::cout << "Turning smart bulb OFF..." << std::endl;

    // Set request parameters
    const std::string uri = "/iot-03/devices/" + this->device_id + "/commands";
    const std::string action = "POST";
    const std::string headers_str = "";
    const std::string body = "{\"commands\":[{\"code\":\"switch_led\",\"value\":false}]}";
    const std::string body_sha256 = "c9df53ad98d9c9be68680613d9ece634a27f102c5e40c7b5f60c11f6b944b6a9";
    std::map<std::string, std::string> headers_map;
    headers_map = {
        {"Content-type", "application/json"},
        {"access_token", this->token}
    };

    // Send request and print response
    httplib::Result response = this->sendRequest(uri, action, headers_map, headers_str, body, body_sha256);
    // std::cout << "Response status:\n";
    // std::cout << response->status << std::endl;
    // std::cout << "Response body:\n";
    // std::cout << response->body << std::endl;

    // Check API response
    if (response->status == 200) {
        json body = json::parse(response->body);
        if (body["success"].dump() == "true") {
            std::cout << "Device turned OFF successfully." << std::endl;
        } else {
            throw std::runtime_error("Error while turning smart bulb OFF.\n");
        }
    }
}

httplib::Result TuyaBulbBaciImpl::sendRequest(
        const std::string uri,
        const std::string action,
        const std::map<std::string, std::string> headers_map,
        const std::string headers_str,
        const std::string body,
        const std::string body_sha256,
        const std::string version
    ) {
    // https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4
    // https://developer.tuya.com/en/docs/iot/singnature?id=Ka43a5mtx1gsc
    std::cout << "Sending request to Tuya API " << uri << std::endl;

    // String stringToSign = 
    // HTTPMethod + "\n" + 
    // Content-SHA256 + "\n" +
    // Headers + "\n" +
    // Url
    const std::string url = "/" + version + uri;
    const std::string string_to_sign = action + "\n" + body_sha256 + "\n" + headers_str + "\n" + url;

    // Get time in milliseconds since epoch, with 13 digits
    std::time_t t = std::time(nullptr) * 1000;

    // str = client_id + access_token + t + nonce + stringToSign 
    // sign = HMAC-SHA256(str, secret).toUpperCase()
    std::string str_data;
    if (this->token.empty()) {
        str_data = this->client_id + std::to_string(t) + string_to_sign;
    } else {
        str_data = this->client_id + this->token + std::to_string(t) + string_to_sign;
    }

    // Allocate memory for the HMAC
    std::vector<uint8_t> out(SHA256_HASH_SIZE);

    // Call hmac-sha256 function and parse result to string stream
    hmac_sha256(this->api_secret.data(), this->api_secret.size(), str_data.data(), str_data.size(),
                out.data(), out.size());
    
    std::stringstream ss_result;
    for (uint8_t i : out) {
        ss_result << std::hex << std::setfill('0') << std::setw(2) << (int)i;
    }

    // to uppercase
    std::string sign = ss_result.str();
    transform(sign.begin(), sign.end(), sign.begin(), ::toupper);

    // Create base headers
    httplib::Headers request_headers = {
        { "client_id", this->client_id },
        { "sign_method", "HMAC-SHA256" },
        { "sign", sign },
        { "t", std::to_string(t) }
    };

    // Add extra headers
    std::map<std::string, std::string>::const_iterator it;
    for (it = headers_map.begin(); it != headers_map.end(); it++) {        
        request_headers.insert({ it->first, it->second });
    }

    // Create HTTPS client
    httplib::Client cli(this->base_url);

    // Send request
    if (action.compare("GET") == 0) {
        return cli.Get(url.c_str(), request_headers);
    } else {
        return cli.Post(url.c_str(), request_headers, body.c_str(), "application/json");
    }
}

std::string TuyaBulbBaciImpl::getToken() {
    // https://openapi.tuyaus.com/v1.0/token?grant_type=1
    const std::string uri = "/token?grant_type=1";
    httplib::Result response = this->sendRequest(uri);

    // Check API response
    if (response->status == 200) {
        json body = json::parse(response->body);
        
        if (body["success"].dump() == "true") {
            const std::string token = body["result"]["access_token"];
            return token;
        }
    }

    throw std::runtime_error("Error while getting API token.\n");
}

bool TuyaBulbBaciImpl::getStatus() {
    // https://openapi.tuyaus.com/v1.0/iot-03/devices/{device_id}/status
    const std::string uri = "/iot-03/devices/" + this->device_id + "/status";
    std::map<std::string, std::string> headers_map;
    headers_map = {
        {"access_token", this->token}
    };

    // Send request and print response
    httplib::Result response = this->sendRequest(uri, "GET", headers_map);
    // std::cout << "Response status:\n";
    // std::cout << response->status << std::endl;
    // std::cout << "Response body:\n";
    // std::cout << response->body << std::endl;

    // Check API response
    if (response->status == 200) {
        json body = json::parse(response->body);

        // Return status
        if (body["success"].dump() == "true") {
            if (body["result"][0]["value"].dump() == "true") {
                return true;
            } else {
                return false;
            }
        }
    }

    throw std::runtime_error("Error while getting device status.\n");
}

ACS::ROboolean_ptr TuyaBulbBaciImpl::status () {
    if(m_status_p == 0) {
		return ACS::ROboolean::_nil();
	}

    ACS::ROboolean_var prop = ACS::ROboolean::_narrow(m_status_p->getCORBAReference());
	return prop._retn();
}
   
/* --------------- [ MACI DLL support functions ] -----------------*/
#include <maciACSComponentDefines.h>
MACI_DLL_SUPPORT_FUNCTIONS(TuyaBulbBaciImpl)
/* ----------------------------------------------------------------*/

Client

Code Block
languagepy
linenumberstrue
from Acspy.Clients.SimpleClient import PySimpleClient

client = PySimpleClient()
bulb_comp = client.getComponent("TuyaBulbBaciCPP")
status = bulb_comp.status
bulb_comp.turnOn()
bulb_comp.status()
bulb_comp.turnOff()
bulb_comp.status()

Output

Code Block
languagepy
titlePython console output
>>> from Acspy.Clients.SimpleClient import PySimpleClient
>>>
>>> client = PySimpleClient()
>>> bulb_comp = client.getComponent("TuyaBulbBaciCPP")
>>> bulb_comp.status.get_sync()
(False, ACSErr.Completion(timeStamp=138772104611657330, type=0, code=0, previousError=[]))
>>> bulb_comp.turnOn()
>>> bulb_comp.status.get_sync()
(True, ACSErr.Completion(timeStamp=138772104679161260, type=0, code=0, previousError=[]))
>>> bulb_comp.turnOff()
>>> bulb_comp.status.get_sync()
(False, ACSErr.Completion(timeStamp=138772104807303050, type=0, code=0, previousError=[]))

BACI property registration in CDB

Code Block
languagexml
title/.../acsdata/config/defaultCDB/CDB/alma/TuyaBulbBaciCPP/TuyaBulbBaciCPP.xml
linenumberstrue
<?xml version='1.0' encoding='ISO-8859-1'?>
<!--
   - History:
   -   Wed May 06 09:02:21 UTC 2009 modified by jDAL
   -   Wed May 06 09:05:40 UTC 2009 modified by jDAL
-->
<TuyaBulbBaci xmlns="urn:schemas-cosylab-com:TuyaBulbBaci:1.0" xmlns:baci="urn:schemas-cosylab-com:BACI:1.0" xmlns:cdb="urn:schemas-cosylab-com:CDB:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <status
                default_timer_trig="1.0"
                description="status for smart bulb device"
                units="bit"
                archive_min_int="0"
                archive_max_int="1" />
</TuyaBulbBaci>

CDB XSD

Code Block
languagexml
title/.../acsdata/config/defaultCDB/CDB/schemas/TuyaBulbBaci.xsd
linenumberstrue
<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema 
  targetNamespace="urn:schemas-cosylab-com:TuyaBulbBaci:1.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns="urn:schemas-cosylab-com:TuyaBulbBaci:1.0"
  xmlns:cdb="urn:schemas-cosylab-com:CDB:1.0" 
  xmlns:baci="urn:schemas-cosylab-com:BACI:1.0" elementFormDefault="qualified" attributeFormDefault="unqualified">
  <xs:import namespace="urn:schemas-cosylab-com:CDB:1.0" schemaLocation="CDB.xsd"/>
  <xs:import namespace="urn:schemas-cosylab-com:BACI:1.0" schemaLocation="BACI.xsd"/>

  <xs:complexType name="TuyaBulbBaci">
    <xs:complexContent>
      <xs:extension base="baci:CharacteristicComponent">
        <xs:sequence>
          <xs:element name="status" type="baci:ROboolean" />
        </xs:sequence>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>

  <xs:element name="TuyaBulbBaci" type="TuyaBulbBaci"/>
</xs:schema>

...