Alexa 智能音箱開發智能家居

一,前期準備材料

  1. 一個亞馬遜開發者賬戶。註冊是免費的
  2. 連接設備,如燈,恆溫器,相機或帶有云API的鎖,用於控制它
  3. 支持Alexa的設備,例如Amazon Echo
  4. 一個AWS賬戶。您在AWS Lambda函數中託管您的技能代碼(這個賬戶可以註冊,可以免費使用12個月,但需要綁定信用卡,且必須交1美元的預授權的費用,國內信用卡可以綁定,親測)
  5. 瞭解JSON
  6. Java,Node.js,C#或Python作爲Lambda函數的知識可以用任何這些語言編寫
  7. 瞭解OAuth 2.0

二,技能創建

https://developer.amazon.com/zh/alexa  登錄上述網址,並登錄您的賬號,在下圖選擇你的技能創建

進去之後,點擊create skill,技能名稱輸入您的技能名,語言默認(也可以選擇你自己喜歡的語言,然而並不支持中文,所以。。。),模型選擇 智能家居,然後點擊創建技能。

三,技能設置

 

  1. Payload version 選擇v3
  2. Smart Home service endpoint  

            2.1 AWS Lambda ARN 設置  Your Skill ID  點擊複製到文本,方便後面使用

            2.2  Default endpoint*  這個是你在創建aws lambda函數時對應的表達式,在後面會給出具體的位置。

                    

            2.3  下面三個複選框 只是爲了讓你給不同的地區選擇不同的區域,爲了使用不同的語言版本用戶時能夠得到更好的體驗

                   此處只使用默認的端點,不勾選其它的。

      3.完成以上設置,請點擊保存按鈕。

四,lambda 函數的創建

       1.登錄您的lambda控制檯,如果找不到lambda函數的具體位置,請點擊aws-->計算-->選擇lambda,或者直接搜索。

       2.進入lambda控制檯,點擊創建功能,選擇從新開始,輸入名稱,因爲筆者是用java開發的,所以運行的哪一塊就選擇                       java8

      3.選擇角色,若沒有點擊創建,具體流程就不再介紹了,https://developer.amazon.com/docs/smarthome/steps-to-build-a-                smart-home-skill.html#create-an-iam-role-for-lambda

     4.進行lambda配置(在配置前請確保右上角你的地區選擇,因爲有的地區的觸發器沒有smart home 選項

      在左側的觸發器,裏面選擇 alexa smart home,然後點擊它,並進行配置,複製你的技能id,並添加。

       

     5.點擊右上角的保存

 

五,配置你的賬戶關聯

    在配置賬戶關聯之前請確保2.2 步驟的默認端點已填寫你剛纔創建的lambda函數,

    

 

六,關於如何通過lambda向本地進行測試

    如果我們在一個項目裏面寫業務邏輯,然後再進行上傳jar包到lambda函數的話,有很多的確點,一方面就意味着你要重複的上傳代碼,如果你的網絡允許你這樣做的話,當我沒說。另一方面,如果代碼上傳完你需要調試的話會很麻煩。

   所以,我們可以這麼做

// -*- coding: utf-8 -*-

// Copyright 2018 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.

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Scanner;

import org.json.JSONObject;

import com.amazonaws.services.lambda.runtime.Context;

public class AlexaHandler {

	public static void handler(InputStream inputStream, OutputStream outputStream, Context context) {

		String request;
		try {
			System.out.println("----開始請求");
			request = getRequest(inputStream);
			//System.out.println("Request:");
			System.out.println("請求參數:"+request);
			String reqURL = "https://www.******/rest/alexa/S_Alexa_Gateway/postGateway";
			// url參數轉換
			String url = UrlEncoderUntil.GetRealUrl(reqURL + "?request=" + request);
			// 響應內容
			String responseData=  GetSample(url);
			System.out.println("----結束請求");
	        System.out.println(responseData);
	        outputStream.write(responseData.getBytes(Charset.forName("UTF-8")));
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	/**
	 * 說明: 發送請求到後臺(請求轉發) 方法名: GetSample
	 * 
	 * @param url
	 * @return
	 */
	private static String GetSample(String url) {
		StringBuilder sb = new StringBuilder();
		try {
			URL iurl = new URL(url);
			HttpURLConnection c = (HttpURLConnection) iurl.openConnection();
			c.connect();
			int status = c.getResponseCode();

			switch (status) {
			case 200:
			case 201:
			case 202:
				BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream()));
				String line;
				while ((line = br.readLine()) != null) {
					sb.append(line + "\n");
				}
				br.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return sb.toString();
	}

	@SuppressWarnings("unused")
	private static JSONObject GetResponse(String json) {

		InputStream inputStream = new ByteArrayInputStream(json.getBytes(Charset.forName("UTF-8")));
		OutputStream outputStream = new OutputStream() {
			private StringBuilder sb = new StringBuilder();

			@Override
			public void write(int b) throws IOException {
				this.sb.append((char) b);
			}

			public String toString() {
				return this.sb.toString();
			}
		};
		String responseString = outputStream.toString();
		return new JSONObject(responseString);
	}

	/**
	 * 說明:判斷請求的是否有值,若沒有值返回空 方法名: getRequest
	 * 
	 * @param is
	 * @return
	 */
	static String getRequest(java.io.InputStream is) {
		Scanner s = new Scanner(is).useDelimiter("\\A");
		return s.hasNext() ? s.next() : "";
	}
}
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

public class UrlEncoderUntil {
    public static void main(String[] args)throws Exception {
      String str="http://nufm.dfcfw.com/EM_Finance2014NumericApplication/JS.aspx?type=CT&cmd=C._A&sty=FCOIATA&sortType=C&sortRule=-1&page=1&pageSize=70&js=var%20quote_123%3d{rank:[(x)],pages:(pc)}&token=7bc05d0d4c3c22ef9fca8c2a912d779c&jsName=quote_123&_g=0.5927966693718834";
      String result=GetRealUrl(str);
      System.out.println(result);
    }

    //對url中的參數進行url編碼
    public static String GetRealUrl(String str) {
        try {
            int index = str.indexOf("?");
            if (index < 0) return str;
            String query = str.substring(0, index);
            String params = str.substring(index + 1);
            Map map = GetArgs(params);
            //Map map=TransStringToMap(params);
            String encodeParams = TransMapToString(map);
            return query + "?" + encodeParams;
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
        return "";
    }

    //將url參數格式轉化爲map
    public static Map GetArgs(String params) throws Exception{
        Map map=new HashMap();
        String[] pairs=params.split("&");
        for(int i=0;i<pairs.length;i++){
            int pos=pairs[i].indexOf("=");
            if(pos==-1) continue;
            String argname=pairs[i].substring(0,pos);
            String value=pairs[i].substring(pos+1);
            value= URLEncoder.encode(value,"utf-8");
            map.put(argname,value);
        }
        return map;
    }

    //將map轉化爲指定的String類型
    public static String TransMapToString(Map map){
        java.util.Map.Entry entry;
        StringBuffer sb = new StringBuffer();
        for(Iterator iterator = map.entrySet().iterator(); iterator.hasNext();)
        {
            entry = (java.util.Map.Entry)iterator.next();
            sb.append(entry.getKey().toString()).append( "=" ).append(null==entry.getValue()?"":
                    entry.getValue().toString()).append (iterator.hasNext() ? "&" : "");
        }
        return sb.toString();
    }

    //將String類型按一定規則轉換爲Map
    public static Map TransStringToMap(String mapString){
        Map map = new HashMap();
        java.util.StringTokenizer items;
        for(StringTokenizer entrys = new StringTokenizer(mapString, "&"); entrys.hasMoreTokens();
            map.put(items.nextToken(), items.hasMoreTokens() ? ((Object) (items.nextToken())) : null))
            items = new StringTokenizer(entrys.nextToken(), "=");
        return map;
    }
}

通過以上的代碼,你就可以輕鬆的把請求轉發到你的本地去,那樣就方便多了。

後臺接收lambda請求的方法。。。

package com.cn.whirlpool.services.alexaIot;



import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;

import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.cn.whirlpool.services.DbService;
import com.cn.whirlpool.services.RedisService;

import Utils.UicJsonUtils;
import antlr.Token;



/**
 *  說明: S_DingDong_Gateway
 * @author author
 * @date 2018年9月10日
 */
@SuppressWarnings("unused")
@Transactional
@Component
@Provider
@Path("/alexa/S_Alexa_Gateway")
public class S_Alexa_Gateway {

	
	@Autowired
	private AlexaResponse alexaResponse;
	@Autowired
	private S_Alexa_Discovery s_Alexa_Discovery;
	@Autowired
	private S_Alexa_Controller s_Alexa_Controller;
	
	private static Logger log = LoggerFactory.getLogger(S_Alexa_Gateway.class);
	DbService redis=RedisService.getInstance();
	
	@SuppressWarnings("unused")
	@GET
	@Path("/postGateway/")
	public JSONObject postGateway(@Context HttpServletRequest request, @Context HttpServletResponse response) throws Exception {
		
		JSONObject jsonObject =new JSONObject();
		//打印請求的參數,這樣方便查看,後面刪掉
		showParams(request);
		// 用來接收返回信息的對象信息
		AlexaResponse alexaResponse = null;
		//處理接收的參數
		String requestParam  = request.getParameter("request");
		// 接收請求信息
		JSONObject jsonRequest = new JSONObject(requestParam);
	
	
		JSONObject directive = (JSONObject) jsonRequest.get("directive");
		JSONObject header = (JSONObject) directive.get("header");
		JSONObject payload = (JSONObject) directive.get("payload");
		String namespace = header.optString("namespace", "INVALID");
		String correlationToken = header.optString("correlationToken", "INVALID");
		//客戶的授權碼
		String code = null;
		//客戶的訪問令牌
		String token = null;
		if (payload.has("grant")) {
			JSONObject grant = (JSONObject) payload.get("grant");
			code = grant.optString("code", "INVALID");
		}
		if (payload.has("grantee")) {
			JSONObject grantee = (JSONObject) payload.get("grantee");
			token = grantee.optString("token", "INVALID");
		}
		
		switch(namespace) {
			case "Alexa":
				//狀態報告
				log.info("Found Alexa Namespace");
				alexaResponse = s_Alexa_Discovery.StateReport(jsonRequest);
				break;
				
			case "Alexa.Authorization": 
				//向alexa發送網關事件,主要是事件驗證使用
				log.info("Found Alexa.Authorization Namespace");
				alexaResponse = new AlexaResponse("Alexa.Authorization","AcceptGrant.Response");
				JSONObject payloads = new JSONObject("{}");
				alexaResponse.SetPayload(payloads.toString());
	            //alexaResponse = new AlexaResponse("Alexa.Authorization","AcceptGrant", "INVALID", "INVALID", correlationToken);
	            break;
	           
	        case "Alexa.Discovery":
	        	//發現設備的命令
	        	log.info("Found Alexa.Discovery Namespace");
				alexaResponse = s_Alexa_Discovery.Discovery(jsonRequest);
				log.info("發現設備的響應參數:"+alexaResponse.toString());
				break;
				
			case "Alexa.PowerController":
				//開關設備的指令
				System.out.println("Found Alexa.PowerController Namespace");
				alexaResponse = s_Alexa_Controller.Controller(jsonRequest,namespace);
				log.info("控制設備開關的響應參數:"+alexaResponse.toString());
				break;
			
			case "Alexa.ThermostatController":
				//溫度控制指令
				System.out.println("Found Alexa.ThermostatController Namespace");
				alexaResponse = s_Alexa_Controller.Controller(jsonRequest,namespace);
				log.info("控制設備溫度的響應參數:"+alexaResponse.toString());
				break;
			//以下功能未實現
			case "Alexa.ModeController":
				//模式設置的指令(暫時不支持,該功能只有預覽版纔有)
				System.out.println("Found Alexa.ModeController Namespace");
				alexaResponse = s_Alexa_Controller.Controller(jsonRequest,namespace);
				log.info("控制設備模式的響應參數:"+alexaResponse.toString());
				break;
			case "Alexa.RangeController":
				//溫度範圍設置的指令(暫時不支持,該功能只有預覽版纔有)
				System.out.println("Found Alexa.RangeController Namespace");
				alexaResponse = s_Alexa_Controller.Controller(jsonRequest,namespace);
				log.info("控制設備溫度範圍的響應參數:"+alexaResponse.toString());
				break;
			 default:
		        System.out.println("INVALID Namespace");
		        alexaResponse = new AlexaResponse();
		        break;	
				
		}
		jsonObject = new JSONObject(alexaResponse.toString());
		log.info(jsonObject.toString());
		return jsonObject;
	}

	
	@SuppressWarnings({ "unused", "rawtypes", "unchecked" })
	private void showParams(HttpServletRequest request) {
        Map map = new HashMap();
        Enumeration paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
 
            String[] paramValues = request.getParameterValues(paramName);
            if (paramValues.length == 1) {
                String paramValue = paramValues[0];
                if (paramValue.length() != 0) {
                    map.put(paramName, paramValue);
                }
            }
        }
 
        Set<Map.Entry<String, String>> set = map.entrySet();
        System.out.println("------------------------------");
        for (Map.Entry entry : set) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
        System.out.println("------------------------------");
    }

}

響應的方法

package com.cn.whirlpool.services.alexaIot;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.TimeZone;
import java.util.UUID;

import javax.ws.rs.ext.Provider;

import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Component
@Provider
public class AlexaResponse {

    private JSONObject response = new JSONObject("{}");
    private JSONObject event = new JSONObject("{}");
    private JSONObject header = new JSONObject("{}");
    private JSONObject endpoint = new JSONObject("{}");
    private JSONObject payload = new JSONObject("{}");

    private String CheckValue(String value, String defaultValue) {

        if (value.isEmpty())
            return defaultValue;

        return value;
    }
    
   public AlexaResponse() throws JSONException {
        this("Alexa", "Response", "INVALID", "INVALID", null);
   }

   public AlexaResponse(String namespace, String name) throws JSONException { this(namespace, name, "INVALID", "INVALID", null); }

   public AlexaResponse(String namespace, String name,String errType,String errMessage) throws JSONException {

       header.put("namespace", CheckValue(namespace, "Alexa"));
       header.put("name", CheckValue(name,"Response"));
       header.put("messageId", UUID.randomUUID().toString());
       header.put("payloadVersion", "3");
       
       event.put("header", header);
       event.put("endpoint", endpoint);
       if (StringUtils.isNotBlank(errType)) {
			payload.put("type", CheckValue(errType, "INVALID"));
			payload.put("message", CheckValue(errMessage, "INVALID"));
		}
       event.put("payload", payload);

       response.put("event", event);
   }
   
   public AlexaResponse(String namespace, String name, String endpointId, String token, String correlationToken) throws JSONException {

       header.put("namespace", CheckValue(namespace, "Alexa"));
       header.put("name", CheckValue(name,"Response"));
       header.put("messageId", UUID.randomUUID().toString());
       header.put("payloadVersion", "3");

       if (correlationToken != null) {
           header.put("correlationToken", CheckValue(correlationToken, "INVALID"));
       }

       JSONObject scope = new JSONObject("{}");
       scope.put("type", "BearerToken");
       scope.put("token", CheckValue(token, "INVALID"));

       endpoint.put("scope", scope);
       endpoint.put("endpointId", CheckValue(endpointId, "INVALID"));

       event.put("header", header);
       event.put("endpoint", endpoint);
       event.put("payload", payload);

       response.put("event", event);
   }
    
    public AlexaResponse(String namespace, String name, String endpointId, String token, String correlationToken,String errType,String errMessage) throws JSONException {

        header.put("namespace", CheckValue(namespace, "Alexa"));
        header.put("name", CheckValue(name,"Response"));
        header.put("messageId", UUID.randomUUID().toString());
        header.put("payloadVersion", "3");

        if (StringUtils.isBlank(correlationToken)) {
            header.put("correlationToken", CheckValue(correlationToken, "INVALID"));
        }

        JSONObject scope = new JSONObject("{}");
        scope.put("type", "BearerToken");
        scope.put("token", CheckValue(token, "INVALID"));

        endpoint.put("scope", scope);
        endpoint.put("endpointId", CheckValue(endpointId, "INVALID"));

        event.put("header", header);
        event.put("endpoint", endpoint);
        
        if (StringUtils.isNotBlank(errType)) {
			payload.put("type", CheckValue(errType, "INVALID"));
			payload.put("message", CheckValue(errMessage, "INVALID"));
		}
        event.put("payload", payload);

        response.put("event", event);
    }
    
    public AlexaResponse(String namespace, String name, String endpointId, String token, String correlationToken,String errType,String errMessage,String validRange) throws JSONException {

        header.put("namespace", CheckValue(namespace, "Alexa"));
        header.put("name", CheckValue(name,"Response"));
        header.put("messageId", UUID.randomUUID().toString());
        header.put("payloadVersion", "3");

        if (StringUtils.isBlank(correlationToken)) {
            header.put("correlationToken", CheckValue(correlationToken, "INVALID"));
        }

        JSONObject scope = new JSONObject("{}");
        scope.put("type", "BearerToken");
        scope.put("token", CheckValue(token, "INVALID"));

        endpoint.put("scope", scope);
        endpoint.put("endpointId", CheckValue(endpointId, "INVALID"));

        event.put("header", header);
        event.put("endpoint", endpoint);
        
        if (StringUtils.isNotBlank(errType)&&StringUtils.isNotBlank(validRange)) {
			payload.put("type", CheckValue(errType, "INVALID"));
			payload.put("message", CheckValue(errMessage, "INVALID"));
			payload.put("validRange", new JSONObject(validRange));
		}
        event.put("payload", payload);

        response.put("event", event);
    }

    public void AddCookie(String key, String value) throws JSONException {
        JSONObject endpointObject = response.getJSONObject("event").getJSONObject("endpoint");
        JSONObject cookie;
        if (endpointObject.has("cookie")) {

            cookie = endpointObject.getJSONObject("cookie");
            cookie.put(key, value);

        } else {
            cookie = new JSONObject();
            cookie.put(key, value);
            endpointObject.put("cookie", cookie);
        }

    }

    public void AddPayloadEndpoint(String friendlyName, String endpointId, String capabilities) throws JSONException {

        JSONObject payload = response.getJSONObject("event").getJSONObject("payload");

        if (payload.has("endpoints"))
        {
            JSONArray endpoints = payload.getJSONArray("endpoints");
            endpoints.put(new JSONObject(CreatePayloadEndpoint(friendlyName, endpointId, capabilities, null)));
        }
        else
        {
            JSONArray endpoints = new JSONArray();
            endpoints.put(new JSONObject(CreatePayloadEndpoint(friendlyName, endpointId, capabilities, null)));
            payload.put("endpoints", endpoints);
        }
    }

    public void AddContextProperty(String namespace, String name, String value, int uncertaintyInMilliseconds) throws JSONException 
    {
        JSONObject context;
        JSONArray properties;
        try {
            context = response.getJSONObject("context");
            properties = context.getJSONArray("properties");

        } catch (JSONException jse) {
            context = new JSONObject();
            properties = new JSONArray();
            context.put("properties", properties);
        }

        properties.put(new JSONObject(CreateContextProperty(namespace, name, value, uncertaintyInMilliseconds)));
        response.put("context", context);

    }

    public String CreateContextProperty(String namespace, String name, String value, int uncertaintyInMilliseconds) throws JSONException   {
    	 JSONObject property = new JSONObject();
    	try {
      
        property.put("namespace", namespace);
        property.put("name", name);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'");
        TimeZone tz = TimeZone.getTimeZone("UTC");
        sdf.setTimeZone(tz);
        String timeOfSample = sdf.format(new Date().getTime());

        property.put("timeOfSample", timeOfSample);
        property.put("uncertaintyInMilliseconds", uncertaintyInMilliseconds);

      /*  property.put("value", value);*/
       
            property.put("value", new JSONObject(value));
        } catch (Exception je) {
            property.put("value", value);
        }

        return property.toString();
    }

    public String CreatePayloadEndpoint(String friendlyName, String endpointId, String capabilities, String cookie) throws JSONException{
        JSONObject endpoint = new JSONObject();
        endpoint.put("capabilities", new JSONArray(capabilities));
        //設備的描述(暫時寫死)
        endpoint.put("description", "Whirlpool smart home");
        //沒有找到對應的類型,暫時全部爲other
        JSONArray displayCategories = new JSONArray("[\"OTHER\"]");
        endpoint.put("displayCategories", displayCategories);
        //設備製造商的名稱
        endpoint.put("manufacturerName", "Whirlpool Corporation");

        if (endpointId == null)
            endpointId = "endpoint_" + 100000 + new Random().nextInt(900000);
        endpoint.put("endpointId", endpointId);

        if (friendlyName == null)
            friendlyName = "Sample Endpoint";
        endpoint.put("friendlyName", friendlyName);

        if (cookie != null)
            endpoint.put("cookie", new JSONObject(cookie));

        return endpoint.toString();
    }

    public String CreatePayloadEndpointCapability(String type, String interfaceValue, String version, String properties, String configuration) throws JSONException {

        JSONObject capability = new JSONObject();
        capability.put("type", type);
        capability.put("interface", interfaceValue);
        capability.put("version", version);
        
        if (properties != null)
            capability.put("properties", new JSONObject(properties));
        if (configuration!=null) {
        	capability.put("configuration", new JSONObject(configuration));
		}
        return capability.toString();
    }

    public void SetPayload(String payload) throws JSONException {
        response.getJSONObject("event").put("payload", new JSONObject(payload));
    }

    @Override
    public String toString() {
        return response.toString();
    }
}

 

後面的話可根據請求的不同類型進行不同的方法。

關於java如何請求響應請查看亞馬遜alexa的列子

https://github.com/alexa/skill-sample-java-smarthome-switch/blob/master/instructions/setup-the-sample-resources.md

 

最後聲明,本文章純屬原創,未經允許禁止轉載。謝謝。。。