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);
}
}