天天看點

天貓精靈對接智能裝置

why to do:

  我之前一直很喜歡智能家居,可惜的是現在市場上成品的智能家居實在是太貴了,屌絲的碼農是在背不起每月高額的房貸和裝修費用的基礎上,再買成品的智能裝置(像某米那樣一個智能開關,竟然賣那麼貴,小弟實在是承受不起啊)。

  我現在想的很簡單,就是家裡的窗簾(每個卧室一個,客廳做一個雙軌)、燈的開關、廚房的涼霸、還有幾處插座面闆做成智能的,然後在入戶門口做個按鈕就是按一下可以關閉屋内所有的燈。

  

天貓精靈對接智能裝置

  伺服器:我之前買了一個群輝,用群輝中docker做的homeassistant

  窗簾電機:我從瀚思彼岸上買的,感覺價格挺實惠*5

  主燈:我買的yeelight,感覺在燈裡面的價格還是挺靠譜的,主要他的開關可以調光,這個我很喜歡

  射燈:這塊用的也是瀚思彼岸上買的開關

  插座開關:因為我在裝修的時候把插座放到了電視後面,我這就做了個智能開關(2個),主要還得控制機頂盒還有我的高清視訊播放器

  天貓精靈(去年雙十一屯了幾個方糖)

  由于新家還沒有裝修完,我這邊就先做了個簡單試驗,把系統中基本的功能做完了,後期裝修完成了,再好好弄弄,但是目前天貓精靈和homeassistant對接,網上确實有不少,可我是一個java碼農,雖說對php了解的也還可以,但是肯定不如java啊,這個确實就比較少了,是以我在這裡簡單寫寫吧,希望java小夥伴們,可以多多提出建議哈。

how to do:

  天貓精靈有自己的開放平台:https://open.aligenie.com/

  1.添加技能  

天貓精靈對接智能裝置
天貓精靈對接智能裝置

  這個可以參考天貓精靈給的文檔,我覺得寫得挺簡單的。

  我這邊就提出幾個比較重要的點,說一下吧:

  1.需要域名和https的證書(可以在阿裡雲上購買,證書有一個免費一年的)

  2.自己搭建的中轉服務需要在外網通路

  3.homeassistant也需要在外網能通路,這裡我自己打了一套ngrok,感覺效果還是不錯的

  4.java方面,我用的spring boot這裡寫一些關鍵的,這個就是

  OAuth2SecurityConfiguration

@Order(1)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private RedisConnectionFactory redisConnection;

    @Autowired
    private BootUserDetailService userDetailService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 必須配置,不然OAuth2的http配置不生效----不明覺厲
                .requestMatchers()
                .antMatchers(  "/test/**","/auth/login","/auth/authorize","/oauth/**","/plugs/**","/gate")
                .and()
                .authorizeRequests()
                // 自定義頁面或處理url是,如果不配置全局允許,浏覽器會提示伺服器将頁面轉發多次
                .antMatchers("/test/**","/auth/login","/plugs/**","/gate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable();//跨域關閉

        // 表單登入
        http.formLogin()
                // 登入頁面
                .loginPage("/auth/login")
                // 登入處理url
                .loginProcessingUrl("/auth/authorize");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService);
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
           

AuthorizationServerConfiguration

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 儲存令牌資料棧
     */
    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private BootUserDetailService userDetailService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允許表單登入
        security.allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        String secret = passwordEncoder.encode("*******");
        clients.inMemory() // 使用in-memory存儲
                .withClient("client")
                // client_id
                .secret(secret)
                // client_secret
                .authorizedGrantTypes("refresh_token", "authorization_code")
                // 該client允許的授權類型
                .redirectUris("https://open.bot.tmall.com/oauth/callback")
                .accessTokenValiditySeconds(60*60*24)
                //token過期時間
                .refreshTokenValiditySeconds(60*60*24)
                //refresh過期時間
                .scopes("all");
        // 允許的授權範圍
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                .userDetailsService(userDetailService);
        endpoints.pathMapping("/oauth/confirm_access","/custom/confirm_access");
    }


    @Bean
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
        // return new InMemoryTokenStore(); //使用記憶體存儲令牌 tokeStore
        return new RedisTokenStore(redisConnectionFactory);
        //使用redis存儲令牌
    }

}

           

HassIntegerFaceController(和homeassitant互動)

public class HassIntegerFaceController {

    @Value("${kyz.hassLongToken}")
    private String hassLongToken;
    @Value("${kyz.hassUrl}")
    private String hassUrl;


    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        // Do any additional configuration here
        return builder.build();
    }

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired()
    private UserService userService;

    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String add() {
        return getDeviceInfo().toString();
    }

    /**
     * 擷取裝置清單
     *
     * @return
     */
    public List<Map<String, Object>> getDeviceInfo() {
        // header填充
        RequestCallback requestCallback = getRequestCallback();
        ResponseExtractor<ResponseEntity<JSONArray>> responseExtractor = restTemplate.responseEntityExtractor(JSONArray.class);
        // 執行execute(),發送請求
        ResponseEntity<JSONArray> response = restTemplate.execute(hassUrl + "/api/states", HttpMethod.GET, requestCallback, responseExtractor);

        JSONArray jsonArray = response.getBody();

        JSONArray jsonArrayCover = new JSONArray();
        Map<String, Object> haDevice = new HashMap<>();
        List<Map<String, Object>> haDeviceList = new ArrayList<>();
        for (int i = 0; i < jsonArray.size(); i++) {
            JSONObject temp = jsonArray.getJSONObject(i);
            if (temp.getString("entity_id").startsWith("cover")) {
                jsonArrayCover.add(jsonArray.getJSONObject(i));
                haDevice = new HashMap<>();
                haDevice.put("deviceId", temp.getString("entity_id"));
                haDevice.put("friendly_name", temp.getJSONObject("attributes").getString("friendly_name"));
                haDevice.put("type", temp.getString("cover"));
                haDevice.put("state", temp.getString("state"));
                haDevice.put("deviceType", "curtain");
                redisUtil.set("ha_state_" + temp.getString("entity_id"), temp.getString("state"));
                haDeviceList.add(haDevice);
            }
            if (temp.getString("entity_id").startsWith("switch")) {
                jsonArrayCover.add(jsonArray.getJSONObject(i));
                haDevice = new HashMap<>();
                haDevice.put("deviceId", temp.getString("entity_id"));
                haDevice.put("friendly_name", temp.getJSONObject("attributes").getString("friendly_name"));
                haDevice.put("type", temp.getString("switch"));
                haDevice.put("state", temp.getString("state"));
                haDevice.put("deviceType", "switch");
                redisUtil.set("ha_state_" + temp.getString("entity_id"), temp.getString("state"));
                haDeviceList.add(haDevice);
            }
            if (temp.getString("entity_id").startsWith("light")) {
                jsonArrayCover.add(jsonArray.getJSONObject(i));
                haDevice = new HashMap<>();
                haDevice.put("deviceId", temp.getString("entity_id"));
                haDevice.put("friendly_name", temp.getJSONObject("attributes").getString("friendly_name"));
                haDevice.put("type", temp.getString("light"));
                haDevice.put("state", temp.getString("state"));
                haDevice.put("deviceType", "light");
                redisUtil.set("ha_state_" + temp.getString("entity_id"), temp.getString("state"));
                haDeviceList.add(haDevice);
            }
        }
        return haDeviceList;
    }

    /**
     * 控制控制
     *
     * @return
     */
    public Integer deviceControl(String deviceType, String entityId, String state, String postion) {
        // header填充
        RequestCallback requestCallback = getRequestCallback();
        ResponseExtractor<ResponseEntity<String>> responseExtractor = restTemplate.responseEntityExtractor(String.class);
        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
        String url = "";
        // 執行execute(),發送請求
        switch (deviceType) {
            case "cover":
                paramMap.add("entity_id", "cover." + entityId);
                if (state.equalsIgnoreCase("open") || state.equalsIgnoreCase("close") || state.equalsIgnoreCase("pause")) {
                    url = "/api/services/cover/" + state + "_cover";
                } else if (state.equalsIgnoreCase("position")) {
                    paramMap.add("position", postion);
                    url = "/api/services/cover/set_cover_position";
                }
                break;
            case "switch":
                paramMap.add("entity_id", "switch." + entityId);
                if (state.equalsIgnoreCase("open")) {
                    url = "/api/services/" + deviceType + "/turn_on";
                } else if (state.equalsIgnoreCase("close")) {
                    url = "/api/services/" + deviceType + "/turn_off";
                }
                break;
            case "light":
                paramMap.add("entity_id", "light." + entityId);
                if (state.equalsIgnoreCase("open")) {
                    url = "/api/services/" + deviceType + "/turn_on";
                } else if (state.equalsIgnoreCase("close")) {
                    url = "/api/services/" + deviceType + "/turn_off";
                }
                break;
            case "fan":
                paramMap.add("entity_id", "switch." + entityId);
                if (state.equalsIgnoreCase("open")) {
                    url = "/api/services/" + deviceType + "/turn_on";
                } else if (state.equalsIgnoreCase("close")) {
                    url = "/api/services/" + deviceType + "/turn_off";
                }
                break;
            default:
        }
        ResponseEntity<String> response = restTemplate.execute(hassUrl + url, HttpMethod.POST, requestCallback, responseExtractor, paramMap);
        return response.getStatusCodeValue();
    }

    /**
     * 定義hedader
     *
     * @return
     */
    private RequestCallback getRequestCallback() {
        LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add("Authorization", "Bearer " + hassLongToken);
        headers.add("Content-Type", "application/json");
        // 擷取單例RestTemplate
        HttpEntity request = new HttpEntity(headers);
        return restTemplate.httpEntityCallback(request, JSONArray.class);
    }
}
           

繼續閱讀