├── .gitignore
├── Images
├── Donation.png
└── WeatherChina.png
├── LICENSE
├── README.md
├── airplay.py
├── dingdong.py
├── dlna.py
├── media_player
├── airplayer.py
└── dlna.py
├── sensor
├── HeWeather.md
├── HeWeather.py
├── WeatherChina.py
└── __pycache__
│ └── WeatherChina.cpython-36.pyc
├── service
└── emulated_hue_charley
│ ├── .idea
│ ├── emulated_hue_charley.iml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── misc.xml
│ ├── modules.xml
│ └── workspace.xml
│ ├── .vscode
│ └── settings.json
│ ├── README.md
│ ├── __init__.py
│ ├── hue_api.py
│ ├── upnp.py
│ └── utility.py
├── switch
└── WuKong.py
└── tts
├── __pycache__
└── baidu.cpython-36.pyc
└── baidu.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows:
2 | Thumbs.db
3 | ehthumbs.db
4 | Desktop.ini
5 |
6 | # Python:
7 | *.py[cod]
8 | *.so
9 | *.egg
10 | *.egg-info
11 | dist
12 | build
--------------------------------------------------------------------------------
/Images/Donation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/1cf9d45a7a32b15f40468a2fbbe34ef17efd9991/Images/Donation.png
--------------------------------------------------------------------------------
/Images/WeatherChina.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/1cf9d45a7a32b15f40468a2fbbe34ef17efd9991/Images/WeatherChina.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Home Assistan Components by Charley
2 | ## Description
3 |
4 | This is an Home Assistant components
5 |
6 | ## Component list
7 |
8 | - TTS from Badiu
9 | - sensor from WeatherChina
10 | - service from HueEmulated
11 |
12 |
13 | if you like what you're seeing! give me a smoke,tks. :)
14 |
15 | 
16 |
17 |
18 | ## Installation
19 | ------
20 | copy all the files into the Home Assistant location. It can now be installed either to the custom_components folder
21 | ```
22 | /home/homeassistant/.homeassistant/custom_components
23 | ```
24 | or the root folder (using virtual environment)
25 | ```
26 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
27 | ```
28 |
29 | One Gateway
30 |
31 | #### WeatherChina:
32 | Add the following line to the Configuration.yaml.
33 | ```
34 | sensor:
35 | - platform: WeatherChina
36 | CityCode:
37 | - 101010100
38 | - 101020100
39 | ```
40 |
41 | How get zhe citycode ,pls open [WeatherChina](http://www.weather.com.cn)
42 |
43 | 
44 |
45 |
46 | #### Baidu TTS
47 | Add the following line to the Configuration.yaml.
48 | ```
49 | tts:
50 | - platform: baidu
51 | language: zh
52 | api_key: 12345678
53 | secret_key: 87654321
54 | speed: 5
55 | pitch: 5
56 | volume: 5
57 | person: 1
58 | ```
59 |
60 | speed,pitch,volume,person is optional
61 | - speed default=5 value1 0-9
62 | - pitch default=5 value1 0-9
63 | - volume default=5 value1 0-9
64 | - person default=0 value1 0-1
65 | person = 0 = Woman
66 | person = 1 = Man
67 |
68 | How get ApiKey and SecretKey? Please register [Baidu developer](http://yuyin.baidu.com)
69 |
70 |
71 | #### WuKong Remote Control:
72 | Add the following line to the Configuration.yaml.
73 | ```
74 | switch:
75 | - platform: WuKong
76 | host: 172.16.1.55
77 | mode:"UDP"
78 | PrefixName: "XiaoMi"
79 | ```
80 | - install [WuKong Remote Control](http://down1.wukongtv.com/yaokong/tv/wkremoteTV-guanwang-release.apk)
81 | - host is Android TV Box Ip Address
82 | - mode is optional 'HTTP:UDP'
83 | - PrefixName is optional
84 |
85 | #### HueEmulated
86 | ##### Installation
87 | ------
88 | copy all the files into the Home Assistant location. It can now be installed either to the custom_components folder
89 | ```
90 | /home/homeassistant/.homeassistant/custom_components
91 | ```
92 | or the root folder (using virtual environment)
93 | ```
94 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
95 | ```
96 | if you use DingDong Smart SoundBox
97 | Add the following line to the Configuration.yaml.
98 | ```
99 | emulated_hue_charley:
100 | type: dingdong
101 | ```
102 |
103 | AutoLinkButtn:
104 | Add the following line to the Configuration.yaml.
105 | ```
106 | emulated_hue_charley:
107 | type: dingdong
108 | auto_link: true
109 | ```
110 |
111 | Other parameters refer to [HomeAssistant](https://home-assistant.io/components/emulated_hue/)
112 |
113 | # 中文
114 |
115 | # Home Assistant 组件
116 | ## 简介
117 | 这是本人开发的Home Assistant组件仓库
118 | ## 组件列表
119 |
120 | - 百度TTS
121 | - 中国天气网传感器
122 | - hue模拟器支持叮咚
123 |
124 | ## 如果你感觉这个组件对你有所帮助请赐我根烟 :)
125 |
126 | 
127 |
128 |
129 | ## 安装方法
130 | 在配置文件目录创建custom_components
131 | 复制tts、sensor文件夹到custom_components目录
132 | ```
133 | /home/homeassistant/.homeassistant/custom_components
134 | ```
135 | 或者复制到系统目录
136 | ```
137 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
138 | ```
139 |
140 | #### WeatherChina:
141 | 在Configuration.yaml文件中添加一下字段
142 | ```
143 | sensor:
144 | - platform: WeatherChina
145 | CityCode:
146 | - 101010100
147 | - 101020100
148 | ```
149 | 怎么获取CityCode?请打开 [WeatherChina](http://www.weather.com.cn)
150 |
151 | 按照下图获取CityCode
152 | 
153 |
154 |
155 | #### HeWeather: 和风天气
156 |
157 | 除了API_KEY以外全是可选字段,API_KEY自己去和风天气[和风天气](https://www.heweather.com)注册
158 |
159 | ```
160 | - platform: HeWeather
161 | api_key: f04c562188764488a86534222cb137bc
162 | interval: 300
163 | isShowWeatherPic: True
164 | city: beihu
165 | monitored_conditions:
166 | # 空气质量指数
167 | aqi:
168 | # 空气质量指数
169 | - aqi
170 | # 一氧化碳
171 | - co
172 | # 二氧化氮
173 | - no2
174 | # 臭氧
175 | - o3
176 | # PM10
177 | - pm10
178 | # PM2.5
179 | - pm25
180 | # 空气质量
181 | - qlty
182 | # 二氧化硫
183 | - so2
184 | # 当天预报
185 | ToDay_forecast:
186 | # 日出时间
187 | - sr
188 | # 日落时间
189 | - ss
190 | # 月升时间
191 | - mr
192 | # 月落时间
193 | - ms
194 | # 白天天气情况
195 | - Weather_d
196 | # 夜间天气情况
197 | - Weather_n
198 | # 相对湿度
199 | - hum
200 | # 降水概率
201 | - pop
202 | # 气压
203 | - pres
204 | # 最高温度
205 | - maxTmp
206 | # 最低温度
207 | - minTmp
208 | # 紫外线指数
209 | - uv
210 | # 能见度
211 | - vis
212 | # 风向(360度)
213 | - deg
214 | # 风向
215 | - dir
216 | # 风力等级
217 | - sc
218 | # 风速
219 | - spd
220 | # 明天预报
221 | Tomorrow_forecast:
222 | - spd
223 | # 后天预报
224 | OfterTomorrow_forecast:
225 | # 1小时预报
226 | 1Hour_forecast:
227 | # 天气情况
228 | - Weather
229 | # 相对湿度
230 | - hum
231 | # 降水概率
232 | - pop
233 | # 气压
234 | - pres
235 | # 温度
236 | - tmp
237 | # 风向(360度)
238 | - deg
239 | # 风向
240 | - dir
241 | # 风力等级
242 | - sc
243 | # 风速
244 | - spd
245 |
246 | # 3小时预报
247 | 3Hour_forecast:
248 | # 6小时预报
249 | 6Hour_forecast:
250 | # 9小时预报
251 | 9Hour_forecast:
252 | # 12小时预报
253 | 12Hour_forecast:
254 | # 15小时预报
255 | 15Hour_forecast:
256 | # 18小时预报
257 | 18Hour_forecast:
258 | # 21小时预报
259 | 21Hour_forecast:
260 |
261 | # 即时预报
262 | now:
263 | # 天气情况
264 | - Weather
265 | # 体感温度
266 | - fl
267 | # 相对湿度
268 | - hum
269 | # 降水量
270 | - pcpn
271 | # 气压
272 | - pres
273 | # 温度
274 | - tmp
275 | # 能见度
276 | - vis
277 | # 风向(360度)
278 | - deg
279 | # 风向
280 | - dir
281 | # 风力等级
282 | - sc
283 | # 风速
284 | - spd
285 | # 生活指数
286 | suggestion:
287 | # 空气指数
288 | air:
289 | # 简介
290 | - brf
291 | # 数据详情
292 | - txt
293 | # 舒适度指数
294 | comf:
295 | # 洗车指数
296 | cw:
297 | # 穿衣指数
298 | drsg:
299 | # 感冒指数
300 | flu:
301 | # 运动指数
302 | sport:
303 | # 旅游指数
304 | trav:
305 | # 紫外线指数
306 | uv:
307 |
308 |
309 | ```
310 |
311 |
312 | #### Baidu TTS
313 | 在Configuration.yaml文件中添加一下字段
314 | ```
315 | tts:
316 | - platform: baidu
317 | language: zh
318 | api_key: 12345678
319 | secret_key: 87654321
320 | speed: 5
321 | pitch: 5
322 | volume: 5
323 | person: 1
324 | ```
325 | speed,pitch,volume,person 字段为可选值,即可以不写入
326 | - speed 默认=5 取值范围 0-9
327 | - pitch 默认=5 取值范围 0-9
328 | - volume 默认=5 取值范围 0-9
329 | - person 默认=0 取值范围 0-1
330 | person = 0 = 男声
331 | person = 1 = 女声
332 |
333 | 怎么获取ApiKey和SecretKey? 请注册[百度开发者](http://yuyin.baidu.com)
334 |
335 |
336 | #### 悟空遥控:
337 | 在Configuration.yaml文件中添加一下字段.
338 | ```
339 | switch:
340 | - platform: WuKong
341 | host: 172.16.1.55
342 | mode:"UDP"
343 | PrefixName: "XiaoMi"
344 | ```
345 | - 安装 [悟空遥控TV端](http://down1.wukongtv.com/yaokong/tv/wkremoteTV-guanwang-release.apk)
346 | - host: 安装了悟空遥控的安卓盒子(电视)的IP地址
347 | - mode: 是一个可选项 'HTTP:UDP' 如果不输入默认HTTP
348 | - PrefixName: 是一个可选项,用与区分多个设备
349 |
350 | #### Hue模拟器
351 | ##### 安装
352 | 在配置文件目录创建custom_components
353 | 复制emulated_hue_charley文件夹到custom_components目录
354 | ```
355 | /home/homeassistant/.homeassistant/custom_components
356 | ```
357 | 或者复制到系统目录
358 | ```
359 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
360 | ```
361 |
362 | 如果是你是使用叮咚智能音箱
363 | 在Configuration.yaml文件中添加一下字段
364 | ```
365 | emulated_hue_charley:
366 | type: dingdong
367 | ```
368 | 自动按下Link按钮
369 | ```
370 | emulated_hue_charley:
371 | type: dingdong
372 | auto_link: true
373 | ```
374 |
375 | 其他设置参考官方的模拟器设置[emulated_hue](https://home-assistant.io/components/emulated_hue/)
376 |
377 | 如果模拟器有任何问题请到QQ群或者[论坛](https://bbs.hassbian.com/thread-3135-1-1.html)发布你的问题
378 |
--------------------------------------------------------------------------------
/airplay.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for AirPlay.
3 |
4 | Developed by Charley
5 | """
6 |
7 | import logging
8 |
9 | try:
10 | from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf
11 | except ImportError:
12 | pass
13 |
14 | import warnings
15 | import time
16 | import socket
17 | import asyncio
18 | from datetime import timedelta
19 |
20 | # from airplay.const import (
21 | # DOMAIN
22 | # )
23 |
24 |
25 |
26 | from homeassistant.helpers.event import async_track_point_in_utc_time
27 | from homeassistant.const import EVENT_HOMEASSISTANT_START
28 | import homeassistant.util.dt as dt_util
29 | from homeassistant.core import callback
30 | from homeassistant.helpers.discovery import load_platform
31 |
32 | _LOGGER = logging.getLogger(__name__)
33 | SCAN_INTERVAL = timedelta(seconds=10)
34 |
35 | REQUIREMENTS = ['zeroconf==0.20.0']
36 | DOMAIN = 'airplay'
37 | MEDIA_PLAYER_DOMAIN = 'airplayer'
38 |
39 | @asyncio.coroutine
40 | def async_setup(hass, config):
41 | """Set up the AirPlay component."""
42 | _LOGGER.debug('Begin setup AirPlay')
43 | ap = airplay()
44 | regDevices = []
45 |
46 | @asyncio.coroutine
47 | def scan_devices(now):
48 | devices = yield from hass.loop.run_in_executor(
49 | None, ap.discover_MediaPlayer)
50 |
51 | for device in devices:
52 | isFind = False
53 |
54 | address = device.get("address")
55 | port = device.get("port")
56 |
57 | for regDevice in regDevices:
58 | regAddress = regDevice.get("address")
59 | regPort = regDevice.get("port")
60 |
61 | if regAddress == address and regPort == port:
62 | isFind = True
63 |
64 | if isFind == False:
65 | regDevices.append(device)
66 | load_platform(hass, "media_player", MEDIA_PLAYER_DOMAIN, device)
67 | _LOGGER.debug("find device:%@", device)
68 |
69 |
70 |
71 |
72 | async_track_point_in_utc_time(hass, scan_devices,
73 | dt_util.utcnow() + SCAN_INTERVAL)
74 |
75 | @callback
76 | def schedule_first(event):
77 | """Schedule the first discovery when Home Assistant starts up."""
78 | async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
79 |
80 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first)
81 |
82 | return True
83 |
84 |
85 | class airplay:
86 |
87 | def __init__(self):
88 | pass
89 |
90 | def remove_service(self, zeroconf, type, name):
91 | print("Service %s removed" % (name,))
92 |
93 | def add_service(self, zeroconf, type, name):
94 | info = zeroconf.get_service_info(type, name)
95 | print("Service %s added, service info: %s" % (name, info))
96 |
97 | def discover_MediaPlayer(self, timeout=10, fast=False):
98 | """
99 | find airPlay devices
100 | """
101 |
102 | # this will be our list of devices
103 | devices = []
104 |
105 | # zeroconf will call this method when a device is found
106 | def on_service_state_change(zeroconf, service_type, name, state_change):
107 | if state_change is ServiceStateChange.Added:
108 | info = zeroconf.get_service_info(service_type, name)
109 | if info is None:
110 | return
111 | try:
112 | name, _ = name.split('.', 1)
113 | except ValueError:
114 | pass
115 |
116 | address = socket.inet_ntoa(info.address)
117 |
118 | devices.append(
119 | {
120 | "name":name,
121 | "address":address,
122 | "port":info.port
123 | }
124 | )
125 | elif state_change is ServiceStateChange.Removed :
126 | pass
127 |
128 | # search for AirPlay devices
129 | try:
130 | zeroconf = Zeroconf()
131 | browser = ServiceBrowser(zeroconf, "_airplay._tcp.local.", handlers=[on_service_state_change]) # NOQA
132 | except NameError:
133 | warnings.warn(
134 | 'AirPlay.find() requires the zeroconf package but it could not be imported. '
135 | 'Install it if you wish to use this method. https://pypi.python.org/pypi/zeroconf',
136 | stacklevel=2
137 | )
138 | return None
139 | time.sleep(5)
140 | zeroconf.close()
141 | return devices
142 |
--------------------------------------------------------------------------------
/dingdong.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for DingDong webhook.
3 | Ver 0.0.1
4 |
5 | For more details about this component, please refer to the documentation at
6 | https://home-assistant.io/components/DingDong/
7 | """
8 |
9 | import asyncio
10 | import copy
11 | import logging
12 |
13 | import voluptuous as vol
14 |
15 | from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
16 | from homeassistant.helpers import template, script, config_validation as cv
17 | from homeassistant.components.http import HomeAssistantView
18 |
19 |
20 | _LOGGER = logging.getLogger(__name__)
21 |
22 | INTENTS_API_ENDPOINT = '/api/dingdong'
23 |
24 | CONF_INTENTS = 'intents'
25 | CONF_SPEECH = 'speech'
26 | CONF_ACTION = 'action'
27 | CONF_ASYNC_ACTION = 'async_action'
28 |
29 | DEFAULT_CONF_ASYNC_ACTION = False
30 |
31 | DOMAIN = 'dingdong'
32 | DEPENDENCIES = ['http']
33 |
34 | CONFIG_SCHEMA = vol.Schema({
35 | DOMAIN: {
36 | CONF_INTENTS: {
37 | cv.string: {
38 | vol.Optional(CONF_SPEECH): cv.template,
39 | vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
40 | vol.Optional(CONF_ASYNC_ACTION,
41 | default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean
42 | }
43 | }
44 | }
45 | }, extra=vol.ALLOW_EXTRA)
46 |
47 |
48 | def setup(hass, config):
49 | """Activate API.AI component."""
50 | intents = config[DOMAIN].get(CONF_INTENTS, {})
51 |
52 | hass.http.register_view(DingdongIntentsView(hass, intents))
53 |
54 | return True
55 |
56 |
57 | class DingdongIntentsView(HomeAssistantView):
58 | """Handle DingDong requests."""
59 |
60 | url = INTENTS_API_ENDPOINT
61 | name = 'api:dingdong'
62 |
63 | def __init__(self,hass,intents):
64 | super().__init__()
65 |
66 | _LOGGER.debug("DingdongIntentsView init ")
67 |
68 | self.hass = hass
69 | intents = copy.deepcopy(intents)
70 | template.attach(hass, intents)
71 |
72 | for name, intent in intents.items():
73 | if CONF_ACTION in intent:
74 | intent[CONF_ACTION] = script.Script(hass,intent[CONF_ACTION],"DingDong {}".format(name))
75 |
76 | self.intents = intents
77 |
78 | @asyncio.coroutine
79 | def post(self,request):
80 | """Handle DingDong"""
81 | data = yield from request.json()
82 |
83 | _LOGGER.debug("Received DingDong request: %s", data)
84 |
85 | serverREQ = data.get('result')
86 |
87 | if serverREQ is None:
88 | _LOGGER.error("Received invalid data from Server: %s", data)
89 | return self.json_message('"Expected result value not received"',HTTP_BAD_REQUEST)
90 |
91 | # use intent to no mix HASS actions with this parameter
92 | intent = serverREQ.get('action')
93 | parameters = serverREQ.get('parameters')
94 | if parameters is None:
95 | _LOGGER.error("Received invalid parameters from Server: %s", data)
96 | return self.json_message('"Expected result value not received"', HTTP_BAD_REQUEST)
97 | # contexts = req.get('contexts')
98 | response = DingdongResponse(parameters)
99 |
100 |
101 |
102 | if intent == "":
103 | _LOGGER.warning("Received intent with empty action")
104 | response.add_speech(
105 | "You have not defined an action in your DingDong intent.")
106 | return self.json(response)
107 |
108 | config = self.intents.get(intent)
109 | if config is None:
110 | _LOGGER.warning("Received unknown intent %s", intent)
111 | response.add_speech(
112 | "请在家庭助理中配置%s意图" %
113 | intent)
114 | return self.json(response)
115 |
116 | speech = config.get(CONF_SPEECH)
117 | action = config.get(CONF_ACTION)
118 | async_action = config.get(CONF_ASYNC_ACTION)
119 |
120 | if action is not None:
121 | # DingDong expects a response in less than 5s
122 | _LOGGER.debug("DingDong response.parameters: %s", response.parameters)
123 | if async_action:
124 | # Do not wait for the action to be executed.
125 | # Needed if the action will take longer than 5s to execute
126 | self.hass.async_add_job(action.async_run(response.parameters))
127 | else:
128 | # Wait for the action to be executed so we can use results to
129 | # render the answer
130 | yield from action.async_run(response.parameters)
131 |
132 | # pylint: disable=unsubscriptable-object
133 | if speech is not None:
134 | response.add_speech(speech)
135 |
136 | return self.json(response)
137 |
138 |
139 | class DingdongResponse(object):
140 | """Help generating the response for API.AI."""
141 |
142 | def __init__(self, parameters):
143 | """Initialize the response."""
144 | self.speech = None
145 | self.parameters = {}
146 | # Parameter names replace '.' and '-' for '_'
147 | for key, value in parameters.items():
148 | underscored_key = key.replace('.', '_').replace('-', '_')
149 | self.parameters[underscored_key] = value
150 |
151 | def add_speech(self, text):
152 | """Add speech to the response."""
153 | assert self.speech is None
154 |
155 | if isinstance(text, template.Template):
156 | text = text.async_render(self.parameters)
157 |
158 | self.speech = text
159 |
160 | def as_dict(self):
161 | """Return response in an API.AI valid dict."""
162 | return {
163 | 'speech': self.speech,
164 | 'displayText': self.speech,
165 | 'source': PROJECT_NAME,
166 | }
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/dlna.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for Dlna Media Player.
3 |
4 | Developed by Charley
5 | """
6 | import logging
7 | import urllib.parse as urllibparse
8 | import socket
9 | import re
10 | import xml.etree.ElementTree as ET
11 | import requests
12 | from datetime import timedelta
13 |
14 | import asyncio
15 |
16 | from homeassistant.const import EVENT_HOMEASSISTANT_START
17 | from homeassistant.helpers.event import async_track_point_in_utc_time
18 | from homeassistant.core import callback
19 | import homeassistant.util.dt as dt_util
20 | from homeassistant.helpers.discovery import load_platform
21 |
22 |
23 | DOMAIN = 'dlna'
24 |
25 | _LOGGER = logging.getLogger(__name__)
26 |
27 | SSDP_BROADCAST_PORT = 1900
28 | SSDP_BROADCAST_ADDR = "239.255.255.250"
29 |
30 | SSDP_BROADCAST_PARAMS = ["M-SEARCH * HTTP/1.1",
31 | "HOST: {}:{}".format(SSDP_BROADCAST_ADDR,
32 | SSDP_BROADCAST_PORT),
33 | "MAN: \"ssdp:discover\"",
34 | "MX: 3",
35 | "ST: ssdp:all", "", ""]
36 | SSDP_BROADCAST_MSG = "\r\n".join(SSDP_BROADCAST_PARAMS)
37 |
38 | UPNP_DEFAULT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"
39 |
40 | SCAN_INTERVAL = timedelta(seconds=15)
41 |
42 |
43 | @asyncio.coroutine
44 | def async_setup(hass, config):
45 |
46 | dlna = Dlnadriver()
47 | regDevices = []
48 |
49 | @asyncio.coroutine
50 | def scan_devices(now):
51 | """Scan for MediaPlayer."""
52 | devices = yield from hass.loop.run_in_executor(
53 | None, dlna.discover_MediaPlayer)
54 |
55 |
56 |
57 | for device in devices:
58 | isFind = False
59 | devUUID = device.get('uuid')
60 |
61 | for regDev in regDevices:
62 | regUUID = regDev.get('uuid')
63 | if devUUID == regUUID:
64 | isFind = True
65 |
66 | if isFind == False:
67 | regDevices.append(device)
68 | load_platform(hass, 'media_player', DOMAIN, device)
69 | _LOGGER.info('RegDevice:{}'.format(device.get('friendly_name')))
70 | else:
71 | isFind = False
72 |
73 | # if device not in regDevices:
74 | # regDevices.append(device)
75 | # load_platform(hass,'media_player',DOMAIN,device)
76 | # _LOGGER.info('RegDevice:{}'.format(device.get('friendly_name')))
77 |
78 | uuid = device.get('uuid')
79 | if uuid != '':
80 | hass.data[uuid] = device
81 |
82 | async_track_point_in_utc_time(hass, scan_devices,
83 | dt_util.utcnow() + SCAN_INTERVAL)
84 |
85 | @callback
86 | def schedule_first(event):
87 | """Schedule the first discovery when Home Assistant starts up."""
88 | async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
89 |
90 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first)
91 |
92 | return True
93 |
94 | class Dlnadriver:
95 |
96 | SOCKET_BUFSIZE = 1024
97 |
98 | def __init__(self):
99 | self._broadcastsocket = None
100 | self._listening = None
101 | self._threads = None
102 |
103 | def register_device(self,location_url):
104 | try:
105 | resp = requests.get(location_url,timeout=5)
106 | except:
107 | return None
108 |
109 | resp.encoding = 'UTF-8'
110 | xml = resp.text
111 | xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1)
112 | info = ET.fromstring(xml)
113 |
114 | location = urllibparse.urlparse(location_url)
115 | hostname = location.hostname
116 |
117 | try:
118 | friendly_name = info.find("./device/friendlyName").text
119 | except:
120 | friendly_name = ''
121 |
122 | try:
123 | uuid = info.find('./device/UDN').text
124 | if uuid[0:5] == "uuid:":
125 | uuid = uuid[5:]
126 | except:
127 | uuid = ''
128 |
129 |
130 |
131 | UPNP_DEFAULT_SERVICE_TYPE = re.search("urn:schemas-upnp-org:service:AVTransport:\d+",xml).group()
132 |
133 | try:
134 | controlURLpath = info.find(
135 | "./device/serviceList/service/[serviceType='{}']/controlURL".format(
136 | UPNP_DEFAULT_SERVICE_TYPE
137 | )
138 | ).text
139 | except:
140 | controlURLpath = ''
141 |
142 | try:
143 | SCPDURLpath = info.find(
144 | "./device/serviceList/service/[serviceType='{}']/SCPDURL".format(
145 | UPNP_DEFAULT_SERVICE_TYPE
146 | )
147 | )
148 | except:
149 | SCPDURLpath = ''
150 |
151 |
152 | try:
153 | eventSubURLpath = info.find(
154 | "./device/serviceList/service/[serviceType='{}']/eventSubURLpath".format(
155 | UPNP_DEFAULT_SERVICE_TYPE
156 | )
157 | )
158 | except:
159 | eventSubURLpath = ''
160 |
161 |
162 | controlURL = urllibparse.urljoin(location_url, controlURLpath)
163 | SCPDURL = urllibparse.urljoin(location_url, SCPDURLpath)
164 | eventSubURL = urllibparse.urljoin(location_url, eventSubURLpath)
165 |
166 | device = {
167 | "location": location_url,
168 | "hostname": hostname,
169 | "uuid":uuid,
170 | "friendly_name": friendly_name,
171 | "controlURL": controlURL,
172 | "SCPDURL": SCPDURL,
173 | "eventSubURL": eventSubURL,
174 | "st": UPNP_DEFAULT_SERVICE_TYPE
175 | }
176 |
177 | return device
178 |
179 | def discover_MediaPlayer(self):
180 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
181 | s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4)
182 | s.bind(("", SSDP_BROADCAST_PORT + 20))
183 |
184 | s.sendto(SSDP_BROADCAST_MSG.encode("UTF-8"), (SSDP_BROADCAST_ADDR,
185 | SSDP_BROADCAST_PORT))
186 |
187 | s.settimeout(5.0)
188 |
189 | devices = []
190 |
191 | while True:
192 |
193 | try:
194 | data, addr = s.recvfrom(self.SOCKET_BUFSIZE)
195 | if len(data) is None:
196 | continue
197 |
198 | except socket.timeout:
199 | break
200 |
201 | try:
202 | info = [a.split(":", 1) for a in data.decode("UTF-8").split("\r\n")[1:]]
203 | device = dict([(a[0].strip().lower(), a[1].strip()) for a in info if len(a) >= 2])
204 | devices.append(device)
205 | except:
206 | pass
207 |
208 | devices_urls = [device["location"] for device in devices if "AVTransport" in device["st"]]
209 | devices_urls = list(set(devices_urls))
210 | devices = []
211 | for location_url in devices_urls:
212 | device = self.register_device(location_url)
213 | if device != None:
214 | devices.append(device)
215 |
216 | return devices
217 |
--------------------------------------------------------------------------------
/media_player/airplayer.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for AirPlay.
3 |
4 | Developed by Charley
5 | """
6 |
7 | import requests
8 |
9 | import homeassistant.util.dt as dt_util
10 | from homeassistant.components.media_player import (
11 | SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
12 | SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
13 | SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
14 | PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_VIDEO,MEDIA_TYPE_URL,
15 | MEDIA_TYPE_PLAYLIST)
16 | from homeassistant.const import (
17 | STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
18 |
19 | SUPPORT_AIRPLAY = SUPPORT_PLAY_MEDIA
20 |
21 | import logging
22 |
23 | _LOGGER = logging.getLogger(__name__)
24 |
25 | def setup_platform(hass, config, add_devices, discovery_info=None):
26 | """Setup the AirPlay media player platform."""
27 | if discovery_info is not None:
28 |
29 | add_devices([
30 | air_player(
31 | hass,
32 | discovery_info.get('name'),
33 | discovery_info.get('address'),
34 | discovery_info.get('port'),
35 | ),
36 | ])
37 |
38 | class air_player(MediaPlayerDevice):
39 | """AirPlay Device"""
40 |
41 | def __init__(self,hass,name,address,port):
42 | """Initialize AirPlay device."""
43 | self._hass = hass
44 | self._deviceUrl = "http://%s:%s" % (address, port)
45 | self._name = name
46 | self._address = address
47 | self._port = port
48 |
49 | self._state = STATE_OFF
50 |
51 | def update(self):
52 | infoResp = self.getDeviceInfo()
53 | if infoResp == None:
54 | self._state = STATE_OFF
55 | return
56 | else:
57 | if infoResp.status_code != 200:
58 | self._state = STATE_OFF
59 | return
60 | self._state = STATE_IDLE
61 |
62 | @property
63 | def name(self):
64 | """Return the name of the device."""
65 | return self._name
66 |
67 | @property
68 | def state(self):
69 | return self._state
70 |
71 | @property
72 | def supported_features(self):
73 | """Flag media player features that are supported."""
74 | return SUPPORT_AIRPLAY
75 |
76 | def media_play(self):
77 | """Send play commmand."""
78 | pass
79 |
80 | def play_media(self, media_type, media_id, **kwargs):
81 | """Send play_media commmand."""
82 | self.play(media_id)
83 |
84 | def getDeviceInfo(self):
85 | return self.getData("/server-info")
86 |
87 | def getPlayback_info(self):
88 | return self.getData("/playback-info")
89 |
90 | def play(self,url):
91 | data = """Content-Location: %s
92 | Start-Position: 0""" % (url)
93 |
94 | self.postData("/play",data=data)
95 |
96 | def getData(self,path):
97 | try:
98 | resp = requests.get("%s%s" % (self._deviceUrl,path),timeout=2)
99 | resp.encoding = 'utf-8'
100 | except Exception as e:
101 | return None
102 | return resp
103 |
104 | def postData(self,path,data=None,header=None):
105 | try:
106 | # proxies = {
107 | # # 'http': 'http://127.0.0.1:8082',
108 | # # 'https': 'http://127.0.0.1:8082',
109 | # # }
110 | resp = requests.post("%s%s" % (self._deviceUrl,path),data=data,headers=header,timeout=2)
111 | resp.encoding = 'utf-8'
112 | except Exception as e:
113 | return None
114 | return resp
115 |
--------------------------------------------------------------------------------
/media_player/dlna.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for Dlna Media Player.
3 |
4 | Developed by Charley
5 | """
6 |
7 | import requests
8 | import xml.etree.ElementTree as etree
9 | import re
10 |
11 | import homeassistant.util.dt as dt_util
12 | from homeassistant.components.media_player import (
13 | SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
14 | SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
15 | SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
16 | PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
17 | MEDIA_TYPE_PLAYLIST)
18 | from homeassistant.const import (
19 | STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
20 |
21 | SUPPORT_DLNA = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
22 | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP |\
23 | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY
24 |
25 | import logging
26 |
27 | _LOGGER = logging.getLogger(__name__)
28 |
29 | SERVER_TYPE_AV = 0
30 | SERVER_TYPE_CONTROL = 1
31 |
32 | def setup_platform(hass, config, add_devices, discovery_info=None):
33 | """Setup the DLNA media player platform."""
34 |
35 | device = DLNADevice(discovery_info.get('location'))
36 | add_devices([
37 | DLNAPlayer(hass,device),
38 | ])
39 |
40 | class DLNAPlayer(MediaPlayerDevice):
41 | """DLNA Device"""
42 |
43 | def __init__(self,hass,device):
44 | """Initialize DLNA device."""
45 | self._hass = hass
46 | self._device = device
47 | self._name = device.Uuid()
48 | self._volume = None
49 | self._muted = None
50 | self._state = STATE_OFF
51 | self._media_position_updated_at = None
52 | self._media_position = None
53 | self._media_duration = None
54 |
55 | def update(self):
56 | deviceInfo = self._hass.data[self._device.Uuid()]
57 | location = deviceInfo.get('location')
58 | if location != self._device.Location():
59 | _LOGGER.info ("Device Change!:{}".format(location))
60 | self._device = DLNADevice(location)
61 |
62 |
63 |
64 |
65 | """Get the latest details from the device."""
66 | # < allowedValue > STOPPED < / allowedValue >
67 | # < allowedValue > PAUSED_PLAYBACK < / allowedValue >
68 | # < allowedValue > PLAYING < / allowedValue >
69 | # < allowedValue > TRANSITIONING < / allowedValue >
70 | # < allowedValue > NO_MEDIA_PRESENT < / allowedValue >
71 | resp = self._device.GetTransportInfo()
72 |
73 | statusCode = resp.get("status")
74 | if statusCode != 200:
75 | self._state = STATE_OFF
76 | _LOGGER.error("update_state:{}---{}".format(self._device.Uuid(), self._state))
77 | return True
78 |
79 | respState = resp.get("CurrentTransportState")
80 | if respState == None:
81 | self._state = STATE_OFF
82 |
83 | respState = respState.decode('UTF-8')
84 |
85 | if respState == 'STOPPED':
86 | self._state = STATE_IDLE
87 | elif respState == 'PAUSED_PLAYBACK':
88 | self._state = STATE_PAUSED
89 | elif respState == 'PLAYING':
90 | self._state = STATE_PLAYING
91 | elif respState == 'NO_MEDIA_PRESENT':
92 | self._state = STATE_IDLE
93 | else:
94 | pass
95 |
96 | if self._state == STATE_PLAYING:
97 | positionInfo = self._device.GetPositionInfo()
98 |
99 | statusCode = positionInfo.get("status")
100 | if statusCode != 200:
101 | return False
102 |
103 | trackDuration = positionInfo.get('TrackDuration')
104 | if trackDuration == None:
105 | return False
106 |
107 | trackDuration = trackDuration.decode('UTF-8')
108 | trackDurationArr = trackDuration.split(':')
109 | self._media_duration = (int(trackDurationArr[0]) * 3600 + int(trackDurationArr[1]) * 60 + int(trackDurationArr[2]))
110 |
111 |
112 | relTime = positionInfo.get('RelTime')
113 | if relTime == None:
114 | return False
115 |
116 | relTime = relTime.decode('UTF-8')
117 | relTimeArr = relTime.split(':')
118 | self._media_position = (int(relTimeArr[0]) * 3600 + int(relTimeArr[1]) * 60 + int(relTimeArr[2]) )
119 |
120 | self._media_position_updated_at = dt_util.utcnow()
121 |
122 | mute = self._device.GetMute()
123 | statusCode = mute.get("status")
124 | if statusCode == 200:
125 |
126 | currentMute = mute.get("CurrentMute")
127 |
128 | if currentMute != None:
129 | currentMute = currentMute.decode('UTF-8')
130 | if currentMute == '0':
131 | self._muted = False
132 | else:
133 | self._muted = True
134 |
135 |
136 | volume = self._device.GetVolume()
137 | statusCode = volume.get("status")
138 | if statusCode == 200:
139 |
140 | currentVolume = volume.get('CurrentVolume')
141 |
142 | if currentVolume != None:
143 | currentVolumeNum = float(currentVolume) / 100
144 | self._volume = currentVolumeNum
145 |
146 |
147 | return True
148 |
149 | @property
150 | def name(self):
151 | """Return the name of the device."""
152 | return self._name
153 |
154 | @property
155 | def state(self):
156 | """Return the state of the device."""
157 | return self._state
158 |
159 | @property
160 | def volume_level(self):
161 | """Volume level of the media player (0..1)."""
162 | return self._volume
163 |
164 | @property
165 | def is_volume_muted(self):
166 | """Boolean if volume is currently muted."""
167 | return self._muted
168 |
169 | @property
170 | def supported_features(self):
171 | """Flag media player features that are supported."""
172 | return SUPPORT_DLNA
173 |
174 | @property
175 | def media_content_type(self):
176 | """Content type of current playing media."""
177 | return MEDIA_TYPE_MUSIC
178 |
179 | @property
180 | def media_duration(self):
181 | """Duration of current playing media in seconds."""
182 | return self._media_duration
183 |
184 | @property
185 | def media_position(self):
186 | """Position of current playing media in seconds."""
187 | return self._media_position
188 |
189 | @property
190 | def media_position_updated_at(self):
191 | """When was the position of the current playing media valid."""
192 | return self._media_position_updated_at
193 |
194 | def mute_volume(self, mute):
195 |
196 | """Mute the volume."""
197 | if mute:
198 | self._device.SetMute(1)
199 | else:
200 | self._device.SetMute(0)
201 |
202 | self._muted = mute
203 | pass
204 |
205 | def set_volume_level(self, volume):
206 | """Set volume level, range 0..1."""
207 |
208 | vol = int(float(volume) * 100)
209 | self._device.SetVolume(vol)
210 | self._volume = volume
211 | pass
212 |
213 | def volume_down(self):
214 | currVolume = int(float(self._volume) * 100)
215 | currVolume -= 5
216 | if currVolume < 0:
217 | currVolume = 0
218 |
219 | self._device.SetVolume(currVolume)
220 | self._volume = float(currVolume)/100
221 |
222 |
223 | def volume_up(self):
224 | currVolume = int(float(self._volume) * 100)
225 | currVolume += 5
226 | if currVolume > 100:
227 | currVolume = 100
228 |
229 | self._device.SetVolume(currVolume)
230 | self._volume = float(currVolume)/100
231 |
232 |
233 | def media_play(self):
234 | """Send play commmand."""
235 | self._device.Play()
236 | pass
237 |
238 | def media_pause(self):
239 | """Send pause command."""
240 | self._device.Pause()
241 | pass
242 |
243 | def media_stop(self):
244 | self._device.Stop()
245 |
246 | def media_next_track(self):
247 | self._device.Next()
248 |
249 | def media_previous_track(self):
250 | self._device.Previous()
251 |
252 | def play_media(self, media_type, media_id, **kwargs):
253 | """Send play_media commmand."""
254 | self._device.SetAVTransportURI(media_id,'')
255 | self._device.Play()
256 | pass
257 |
258 |
259 | class DLNADevice(object):
260 |
261 | def __init__(self,location):
262 | self.location = location
263 | resp = requests.get(location)
264 | resp.encoding = 'utf-8'
265 | xmlDesc = resp.text
266 |
267 | self.rootDevice = DLNARootDevice(xmlDesc,location)
268 | self.service = self.rootDevice.Device().Service("urn:upnp-org:serviceId:AVTransport")
269 | self.control = self.rootDevice.Device().Service("urn:upnp-org:serviceId:RenderingControl")
270 |
271 | def Location(self):
272 | return self.location
273 |
274 | def Uuid(self):
275 | return self.rootDevice.Device().Uuid()
276 |
277 | def Name(self):
278 | return self.rootDevice.Device().FriendlyName()
279 |
280 | def GetCurrentTransportActions(self):
281 | fnParams = '0'
282 | result = self.sendRequest(SERVER_TYPE_AV,"GetCurrentTransportActions",fnParams,["Actions"])
283 | return result
284 |
285 | def GetDeviceCapabilities(self):
286 | fnParams = '0'
287 | result = self.sendRequest(SERVER_TYPE_AV,"GetDeviceCapabilities",fnParams,["PlayMedia","RecMedia","RecQualityModes"])
288 | return result
289 |
290 | def GetMediaInfo(self):
291 | fnParams = '0'
292 | result = self.sendRequest(SERVER_TYPE_AV,"GetMediaInfo", fnParams, ["NrTracks", "MediaDuration", "CurrentURI","CurrentURIMetaData",
293 | "NextURI","NextURIMetaData","PlayMedium","RecordMedium","WriteStatus"])
294 | return result
295 |
296 | def GetPositionInfo(self):
297 | fnParams = '0'
298 | result = self.sendRequest(SERVER_TYPE_AV,"GetPositionInfo",fnParams,["Track","TrackDuration","TrackMetaData","TrackURI","RelTime","AbsTime","RelCount","AbsCount"])
299 | return result
300 |
301 | def GetTransportInfo(self):
302 | fnParams = '0'
303 | result = self.sendRequest(SERVER_TYPE_AV,"GetTransportInfo", fnParams,
304 | ["CurrentTransportState", "CurrentTransportStatus", "CurrentSpeed"])
305 | return result
306 |
307 | def GetTransportSettings(self):
308 | fnParams = '0'
309 | result = self.sendRequest(SERVER_TYPE_AV,"GetTransportSettings", fnParams,["PlayMode", "RecQualityMode"])
310 | return result
311 |
312 | def Next(self):
313 | fnParams = '0'
314 | result = self.sendRequest(SERVER_TYPE_AV,"Next", fnParams,[])
315 | return result
316 |
317 | def Pause(self):
318 | fnParams = '0'
319 | result = self.sendRequest(SERVER_TYPE_AV,"Pause", fnParams, [])
320 | return result
321 |
322 | def Play(self):
323 | fnParams = '01'
324 | result = self.sendRequest(SERVER_TYPE_AV,"Play", fnParams, [])
325 | return result
326 |
327 | def Stop(self):
328 | fnParams = '01'
329 | result = self.sendRequest(SERVER_TYPE_AV,"Stop", fnParams, [])
330 | return result
331 |
332 | def Previous(self):
333 | fnParams = '0'
334 | result = self.sendRequest(SERVER_TYPE_AV,"Previous", fnParams, [])
335 | return result
336 |
337 | def Seek(self,unit,target):
338 | # Unit:REL_TIME or TRACK_NR
339 | # Target: 00: 02:21 TRACK_NR Num
340 | fnParams = '0{}{}'.format(unit,target)
341 | result = self.sendRequest(SERVER_TYPE_AV,"Seek", fnParams, [])
342 | return result
343 |
344 | def SetAVTransportURI(self,uri,metaData):
345 | fnParams = '0{}{}'.format(uri,metaData)
346 | result = self.sendRequest(SERVER_TYPE_AV,"SetAVTransportURI", fnParams, [])
347 | return result
348 |
349 | def SetNextAVTransportURI(self,uri,metaData):
350 | fnParams = '0{}{}'.format(
351 | uri, metaData)
352 | result = self.sendRequest(SERVER_TYPE_AV,"SetNextAVTransportURI", fnParams, [])
353 | return result
354 |
355 | def SetPlayMode(self,playMode):
356 | # allowed NewPlayMode = "NORMAL", "REPEAT_ONE", "REPEAT_ALL", "RANDOM"
357 | fnParams = '0{}'.format(
358 | playMode)
359 | result = self.sendRequest(SERVER_TYPE_AV,"SetPlayMode", fnParams, [])
360 | return result
361 |
362 | #control
363 |
364 | def GetMute(self):
365 | fnParams = '0Master'
366 | result = self.sendRequest(SERVER_TYPE_CONTROL, "GetMute", fnParams,["CurrentMute"])
367 | return result
368 |
369 | def GetVolume(self):
370 | fnParams = '0Master'
371 | result = self.sendRequest(SERVER_TYPE_CONTROL, "GetVolume", fnParams, ["CurrentVolume"])
372 | return result
373 |
374 | def ListPresets(self):
375 | fnParams = '0'
376 | result = self.sendRequest(SERVER_TYPE_CONTROL, "ListPresets", fnParams, ["CurrentPresetNameList"])
377 | return result
378 |
379 | def SelectPreset(self):
380 | fnParams = '0'
381 | result = self.sendRequest(SERVER_TYPE_CONTROL, "SelectPreset", fnParams, ["CurrentPresetNameList"])
382 | return result
383 |
384 | def SetMute(self,mute):
385 | fnParams = '0Master{}'.format(mute)
386 | result = self.sendRequest(SERVER_TYPE_CONTROL, "SetMute", fnParams, [])
387 | return result
388 |
389 | def SetVolume(self,volume):
390 | fnParams = '0Master{}'.format(volume)
391 | result = self.sendRequest(SERVER_TYPE_CONTROL, "SetVolume", fnParams, [])
392 | return result
393 |
394 | # XiaoMi TV
395 | def SetRecordQualityMode(self,model):
396 | fnParams = '0{}'.format(model)
397 | result = self.sendRequest(SERVER_TYPE_AV,"SetRecordQualityMode", fnParams, [])
398 | return result
399 |
400 | def Record(self):
401 | fnParams = '0'
402 | result = self.sendRequest(SERVER_TYPE_AV,"SetRecordQualityMode", fnParams, [])
403 | return result
404 |
405 |
406 | def sendRequest(self,serverType,fnfunc,fnParams,arguments):
407 | if serverType == SERVER_TYPE_AV:
408 |
409 | resp = self.soapRequest(self.service.ControlUrl(), self.service.Type(), fnfunc, fnParams)
410 | else:
411 | resp = self.soapRequest(self.control.ControlUrl(), self.control.Type(), fnfunc, fnParams)
412 |
413 | if resp == None:
414 | return {"status": 999,"msg":'send Request Error'}
415 |
416 | result = {
417 | "status": resp.status_code,
418 | "msg":'ok'
419 | }
420 |
421 | respText = resp.text.encode('utf-8')
422 |
423 | respXml = etree.fromstring(respText)
424 |
425 | if resp.status_code != 200:
426 | try:
427 | et = respXml[0][0].find("detail/{urn:schemas-upnp-org:control-1-0}UPnPError")
428 | value = et.find("{urn:schemas-upnp-org:control-1-0}errorDescription").text.encode('utf-8')
429 |
430 | except:
431 | value = None
432 | result['msg'] = value
433 |
434 | try:
435 | et = respXml[0][0].find("detail/{urn:schemas-upnp-org:control-1-0}UPnPError")
436 | errorcode = et.find("{urn:schemas-upnp-org:control-1-0}errorCode").text.encode('utf-8')
437 | except:
438 | errorcode = None
439 | result['ErrorCode'] = errorcode
440 |
441 |
442 | if len(arguments) == 0:
443 |
444 | return result
445 |
446 | for argument in arguments:
447 | try:
448 | if serverType == SERVER_TYPE_AV:
449 | value = respXml[0].find("{%s}%sResponse/%s" % (self.service.Type(), fnfunc,argument)).text.encode('utf-8')
450 | else:
451 | value = respXml[0].find("{%s}%sResponse/%s" % (self.control.Type(), fnfunc, argument)).text.encode(
452 | 'utf-8')
453 | except:
454 | value = None
455 | result[argument] = value
456 |
457 | return result
458 |
459 | def soapRequest(self,location, service, fnName, fnParams):
460 | bodyString = ''
461 | bodyString += ''
462 | bodyString += ' '
463 | bodyString += ' '
464 | bodyString += ' ' + fnParams
465 | bodyString += ' '
466 | bodyString += ' '
467 | bodyString += ''
468 |
469 | headers = {
470 | 'Content-Type': 'text/xml',
471 | 'Accept': 'text/xml',
472 | 'SOAPAction': '"'+service + '#' + fnName + '"'
473 | }
474 | try:
475 | res = requests.post(location, data=bodyString, headers=headers,timeout=10)
476 | res.encoding = 'utf-8'
477 | except Exception as e:
478 | _LOGGER.error("send Request Error:{}".format(e))
479 | return None
480 |
481 | return res
482 |
483 |
484 | class DLNARootDevice:
485 |
486 | def __init__(self, devDescXml, location):
487 |
488 | self._location = location
489 | self._urlBase = None
490 | self._Device = None
491 |
492 | root = etree.fromstring(devDescXml)
493 | ns = root.tag[1:].split('}')[0]
494 |
495 | # URLBase
496 | urlBase = root.find('{%s}URLBase' % (ns))
497 | if urlBase is not None:
498 | self._urlBase = urlBase.text
499 | else:
500 | m = re.match('http://([^/]*)(.*$)', location)
501 | self._urlBase = 'http://' + m.group(1)
502 |
503 | # Strip off any trailing '/'
504 | m = re.match('(.*?)(/*$)', self._urlBase)
505 | if m:
506 | self._urlBase = m.group(1)
507 |
508 | # create the root device
509 | device = root.find('{%s}device' % (ns))
510 | self._Device = DLNAUPNPDevice(device, ns, self)
511 |
512 |
513 | def Location(self):
514 | return self._location
515 |
516 | def SetLocation(self, aLocation):
517 | self._location = aLocation
518 |
519 | def UrlBase(self):
520 | return self._urlBase
521 |
522 | def Device(self):
523 | return self._Device
524 |
525 | class DLNAUPNPDevice:
526 |
527 | def __init__(self, devElem, devNs, rootDevice):
528 | self._RootDevice = rootDevice
529 | self._ServiceList = []
530 | self._DeviceList = []
531 |
532 | # deviceType
533 | try:
534 | self._Type = devElem.find('{%s}deviceType' % (devNs)).text
535 | except:
536 | self._Type = ''
537 |
538 | # UDN
539 | try:
540 | self._Uuid = devElem.find('{%s}UDN' % (devNs)).text
541 | if self._Uuid[0:5] == "uuid:":
542 | self._Uuid = self._Uuid[5:]
543 | except:
544 | self._Uuid = ''
545 |
546 | # friendlyName
547 | try:
548 | self._FriendlyName = devElem.find('{%s}friendlyName' % (devNs)).text
549 | except:
550 | self._FriendlyName = ''
551 |
552 | # serviceList
553 | serviceList = devElem.find('{%s}serviceList' % (devNs))
554 | if serviceList is not None:
555 | for service in serviceList.getchildren():
556 | newServ = DLNAService(self, service, devNs)
557 | self._ServiceList.append(newServ)
558 |
559 | # deviceList
560 | deviceList = devElem.find('{%s}deviceList' % (devNs))
561 | if deviceList:
562 | for device in deviceList.getchildren():
563 | newDev = DLNAUPNPDevice(device, devNs, rootDevice)
564 | self._DeviceList.append(newDev)
565 |
566 | # presentationUrl
567 | try:
568 | self._PresentationUrl = devElem.find('{%s}presentationURL' % (devNs)).text
569 | except:
570 | self._PresentationUrl = ''
571 |
572 | def __str__(self):
573 | devStr = 'DEVICE:\r\n'
574 | devStr += 'UUID : ' + self.Uuid() + '\r\n'
575 | devStr += 'DEV DESC URL: ' + self.Location() + '\r\n'
576 | devStr += 'URL BASE : ' + self.UrlBase() + '\r\n'
577 | for serv in self._ServiceList:
578 | devStr += str(serv)
579 | devStr += '\r\n'
580 | return devStr
581 |
582 | def Uuid(self):
583 | return self._Uuid
584 |
585 | def Location(self):
586 | return self._RootDevice.Location()
587 |
588 | def UrlBase(self):
589 | return self._RootDevice.UrlBase()
590 |
591 | def Type(self):
592 | return self._Type
593 |
594 | def FriendlyName(self):
595 | return self._FriendlyName
596 |
597 | def PresentationUrl(self):
598 | return self._PresentationUrl
599 |
600 | def SetLocation(self, location):
601 | self._RootDevice.SetLocation(location)
602 |
603 | def Service(self, servId):
604 | for serv in self._ServiceList:
605 | if re.match(servId,serv.Id()):
606 | return serv
607 | return None
608 |
609 | def ServiceList(self):
610 | return self._ServiceList
611 |
612 | def DeviceList(self):
613 | return self._DeviceList
614 |
615 | def FindDevice(self, uuid):
616 | if uuid == self._Uuid:
617 | return self
618 | for dev in self._DeviceList:
619 | found = dev.FindDevice(uuid)
620 | if found != None:
621 | return found
622 | return None
623 |
624 | class DLNAService:
625 |
626 | def __init__(self, parentDevice, servElem, servNs):
627 | self._ParentDevice = parentDevice
628 |
629 | self._Type = servElem.find('{%s}serviceType' % (servNs)).text
630 | self._Id = servElem.find('{%s}serviceId' % (servNs)).text
631 | self._ScpdUrl = servElem.find('{%s}SCPDURL' % (servNs)).text
632 | self._ControlUrl = servElem.find('{%s}controlURL' % (servNs)).text
633 |
634 | try:
635 | self._EventSubUrl = servElem.find('{%s}eventSubURL' % (servNs)).text
636 | except:
637 | self._EventSubUrl = None
638 |
639 | self._ActionList = []
640 | self._StateVarList = []
641 |
642 | # Make sure the relative URLs have a '/' at the front
643 | if self._ScpdUrl[0:7] != "http://":
644 | if self._ScpdUrl[0] != '/':
645 | self._ScpdUrl = self._ParentDevice.UrlBase() + '/' + self._ScpdUrl
646 | else:
647 | self._ScpdUrl = self._ParentDevice.UrlBase() + self._ScpdUrl
648 |
649 | if self._ControlUrl[0:7] != "http://":
650 | if self._ControlUrl[0] != '/':
651 | self._ControlUrl = self._ParentDevice.UrlBase() + '/' + self._ControlUrl
652 | else:
653 | self._ControlUrl = self._ParentDevice.UrlBase() + self._ControlUrl
654 |
655 | if self._EventSubUrl and self._EventSubUrl[0:7] != "http://":
656 | if self._EventSubUrl[0] != '/':
657 | self._EventSubUrl = self._ParentDevice.UrlBase() + '/' + self._EventSubUrl
658 | else:
659 | self._EventSubUrl = self._ParentDevice.UrlBase() + self._EventSubUrl
660 |
661 | # scpdXml = requests.get(self._ScpdUrl).text.encode('utf-8')
662 | # self.ParseXmlDesc(scpdXml)
663 |
664 |
665 |
666 | def __str__(self):
667 | servStr = '\t' + 'SERVICE:\r\n'
668 | servStr += '\t' + 'TYPE : ' + self._Type + '\r\n'
669 | servStr += '\t' + 'ID : ' + self._Id + '\r\n'
670 | servStr += '\t' + 'SCPD URL : ' + self.ScpdUrl() + '\r\n'
671 | servStr += '\t' + 'CONTROL URL: ' + self.ControlUrl() + '\r\n'
672 | if self._EventSubUrl:
673 | servStr += '\t' + 'EVENT URL : ' + self.EventSubUrl() + '\r\n'
674 | return servStr
675 |
676 | def Type(self):
677 | return self._Type
678 |
679 | def Id(self):
680 | return self._Id
681 |
682 | def ScpdUrl(self):
683 | return self._ScpdUrl
684 |
685 | def ControlUrl(self):
686 | return self._ControlUrl
687 |
688 | def EventSubUrl(self):
689 | return self._EventSubUrl
690 |
691 | def StateVarList(self):
692 | return self._StateVarList
693 |
694 | def ActionList(self):
695 | return self._ActionList
696 |
697 | def ParseXmlDesc(self, xmlDesc):
698 | "Parse the service description XML file."""
699 |
700 | scpd = etree.fromstring(xmlDesc)
701 | ns = scpd.tag[1:].split('}')[0]
702 |
703 | # State Variables
704 | stateVars = scpd.find('{%s}serviceStateTable' % (ns))
705 | if stateVars is not None:
706 | for stateVar in stateVars.getchildren():
707 | name = stateVar.find('{%s}name' % (ns)).text
708 | type = stateVar.find('{%s}dataType' % (ns)).text
709 | sendEvs = stateVar.attrib['sendEvents']
710 | try:
711 | default = stateVar.find('{%s}defaultValue' % (ns)).text
712 | except:
713 | default = None
714 |
715 | sv = DLNAService_StateVariable(self, name, type)
716 | if default:
717 | sv.SetDefaultValue(default)
718 |
719 | if sendEvs == 'yes':
720 | sv.SetEvented(1)
721 | else:
722 | sv.SetEvented(0)
723 |
724 | allowedVals = stateVar.find('{%s}allowedValueList' % (ns))
725 | if allowedVals is not None:
726 | for allowedVal in allowedVals.getchildren():
727 | sv.AddAllowedValue(allowedVal.text)
728 |
729 | allowedValRange = stateVar.find('{%s}allowedValueRange' % (ns))
730 | if allowedValRange is not None:
731 | min = allowedValRange.find('{%s}minimum' % (ns)).text
732 | max = allowedValRange.find('{%s}maximum' % (ns)).text
733 | try:
734 | step = allowedValRange.find('{%s}step' % (ns)).text
735 | except:
736 | step = '1'
737 | sv.SetAllowedValueRange(min, max, step)
738 |
739 | self._StateVarList.append(sv)
740 |
741 | # Actions
742 | actions = scpd.find('{%s}actionList' % (ns))
743 | if actions is not None:
744 | for act in actions.getchildren():
745 | name = act.find('{%s}name' % (ns)).text
746 | action = DLNAService_Action(self, name)
747 |
748 | arguments = act.find('{%s}argumentList' % (ns))
749 | if arguments is not None:
750 | for argument in arguments.getchildren():
751 | name = argument.find('{%s}name' % (ns)).text
752 | dir = argument.find('{%s}direction' % (ns)).text
753 | arg = DLNAService_Argument(action, name, dir)
754 |
755 | rsv = argument.find('{%s}relatedStateVariable' % (ns)).text
756 | for sv in self._StateVarList:
757 | if rsv == sv.Name():
758 | arg.SetRelatedStateVar(sv)
759 | break
760 |
761 | self._ActionList.append(action)
762 |
763 | class DLNAService_StateVariable:
764 | """A service state variable."""
765 |
766 | def __init__(self, parent, name, type):
767 | self._Parent = parent
768 | self._Name = name
769 | self._Type = type
770 | self._DefaultValue = None
771 | self._AllowedValueList = None
772 | self._AllowedRangeMin = None
773 | self._AllowedRangeMax = None
774 | self._AllowedRangeStep = None
775 | self._IsEvented = 0
776 |
777 | def SetEvented(self, isEvented):
778 | self._IsEvented = isEvented
779 |
780 | def SetDefaultValue(self, defVal):
781 | self._DefaultValue = defVal
782 |
783 | def AddAllowedValue(self, val):
784 | if self._AllowedValueList == None:
785 | self._AllowedValueList = []
786 | self._AllowedValueList.append(val)
787 |
788 | def SetAllowedValueRange(self, min, max, step):
789 | self._AllowedRangeMin = min
790 | self._AllowedRangeMax = max
791 | self._AllowedRangeStep = step
792 |
793 | def IsEvented(self):
794 | return self._IsEvented
795 |
796 | def Name(self):
797 | return self._Name
798 |
799 | def Type(self):
800 | return self._Type
801 |
802 | def DefaultValue(self):
803 | return self._DefaultValue
804 |
805 | def AllowedValueList(self):
806 | return self._AllowedValueList
807 |
808 | def AllowedValueRange(self):
809 | return (self._AllowedRangeMin, self._AllowedRangeMax, self._AllowedRangeStep)
810 |
811 | class DLNAService_Action:
812 | """An action belonging to a service."""
813 |
814 | def __init__(self, parent, name):
815 | self._Parent = parent
816 | self._Name = name
817 | self._ArgList = []
818 |
819 | def AddArg(self, arg):
820 | self._ArgList.append(arg)
821 |
822 | def Name(self):
823 | return self._Name
824 |
825 | def ArgList(self):
826 | return self._ArgList
827 |
828 | class DLNAService_Argument:
829 | """An argument of an action."""
830 |
831 | def __init__(self, parent, name, dir):
832 | self.iParent = parent
833 | self.iName = name
834 | self.iDir = dir
835 | self.iRsv = None
836 | self.iParent.AddArg(self)
837 |
838 | def Name(self):
839 | return self._Name
840 |
841 | def Direction(self):
842 | return self._Dir
843 |
844 | def RelatedStateVar(self):
845 | return self._Rsv
846 |
847 | def Type(self):
848 | if self._Rsv:
849 | return self._Rsv.Type()
850 | else:
851 | return None
852 |
853 | def SetRelatedStateVar(self, rsv):
854 | self._Rsv = rsv
855 |
856 |
--------------------------------------------------------------------------------
/sensor/HeWeather.md:
--------------------------------------------------------------------------------
1 | ```
2 | sensor:
3 | - platform: HeWeather
4 | monitored_conditions:
5 | # 空气质量指数
6 | aqi:
7 | # 空气质量指数
8 | - aqi
9 | # 一氧化碳
10 | - co
11 | # 二氧化氮
12 | - no2
13 | # 臭氧
14 | - o3
15 | # PM10
16 | - pm10
17 | # PM2.5
18 | - pm25
19 | # 空气质量
20 | - qlty
21 | # 二氧化硫
22 | - so2
23 | # 当天预报
24 | ToDay_forecast:
25 | # 日出时间
26 | - sr
27 | # 日落时间
28 | - ss
29 | # 月升时间
30 | - mr
31 | # 月落时间
32 | - ms
33 | # 白天天气情况
34 | - Weather_d
35 | # 夜间天气情况
36 | - Weather_n
37 | # 相对湿度
38 | - hum
39 | # 降水概率
40 | - pop
41 | # 气压
42 | - pres
43 | # 最高温度
44 | - maxTmp
45 | # 最低温度
46 | - minTmp
47 | # 紫外线指数
48 | - uv
49 | # 能见度
50 | - vis
51 | # 风向(360度)
52 | - deg
53 | # 风向
54 | - dir
55 | # 风力等级
56 | - sc
57 | # 风速
58 | - spd
59 | # 明天预报
60 | Tomorrow_forecast:
61 | # 后天预报
62 | OfterTomorrow_forecast:
63 |
64 | # 1小时预报
65 | 1Hour_forecast:
66 | # 天气情况
67 | - Weather
68 | # 相对湿度
69 | - hum
70 | # 降水概率
71 | - pop
72 | # 气压
73 | - pres
74 | # 温度
75 | - tmp
76 | # 风向(360度)
77 | - deg
78 | # 风向
79 | - dir
80 | # 风力等级
81 | - sc
82 | # 风速
83 | - spd
84 |
85 | # 3小时预报
86 | 3Hour_forecast:
87 | # 6小时预报
88 | 6Hour_forecast:
89 | # 9小时预报
90 | 9Hour_forecast:
91 | # 12小时预报
92 | 12Hour_forecast:
93 | # 15小时预报
94 | 15Hour_forecast:
95 | # 18小时预报
96 | 18Hour_forecast:
97 | # 21小时预报
98 | 21Hour_forecast:
99 |
100 | # 即时预报
101 | now:
102 | # 天气情况
103 | - Weather
104 | # 体感温度
105 | - fl
106 | # 相对湿度
107 | - hum
108 | # 降水量
109 | - pcpn
110 | # 气压
111 | - pres
112 | # 温度
113 | - tmp
114 | # 能见度
115 | - vis
116 | # 风向(360度)
117 | - deg
118 | # 风向
119 | - dir
120 | # 风力等级
121 | - sc
122 | # 风速
123 | - spd
124 | # 生活指数
125 | suggestion:
126 | # 舒适度指数
127 | air:
128 | - brf
129 | - txt
130 | # 洗车指数
131 | comf:
132 | # 穿衣指数
133 | cw:
134 | # 穿衣指数
135 | drsg:
136 | # 感冒指数
137 | flu:
138 | # 运动指数
139 | sport:
140 | # 旅游指数
141 | trav:
142 | # 紫外线指数
143 | uv:
144 | ```
--------------------------------------------------------------------------------
/sensor/HeWeather.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import voluptuous as vol
3 | from datetime import timedelta
4 |
5 | from homeassistant.const import TEMP_CELSIUS ,CONF_LATITUDE, CONF_LONGITUDE,CONF_API_KEY
6 | from homeassistant.helpers.entity import Entity
7 | from homeassistant.components.sensor import PLATFORM_SCHEMA
8 | import homeassistant.helpers.config_validation as cv
9 | from homeassistant.util import Throttle
10 | from homeassistant.const import (
11 | CONF_MONITORED_CONDITIONS)
12 | import requests
13 |
14 | CONF_AQI = 'aqi'
15 | CONF_TODAY_FORECAST = 'ToDay_forecast'
16 | CONF_TOMORROW_FORECAST = 'Tomorrow_forecast'
17 | CONF_OFTERTOMORROW_FORECAST = 'OfterTomorrow_forecast'
18 | CONF_1HOUR_FORECAST = '1Hour_forecast'
19 | CONF_3HOUR_FORECAST = '3Hour_forecast'
20 | CONF_6HOUR_FORECAST = '6Hour_forecast'
21 | CONF_9HOUR_FORECAST = '9Hour_forecast'
22 | CONF_12HOUR_FORECAST = '12Hour_forecast'
23 | CONF_15HOUR_FORECAST = '15Hour_forecast'
24 | CONF_18HOUR_FORECAST = '18Hour_forecast'
25 | CONF_21HOUR_FORECAST = '21Hour_forecast'
26 | CONF_NOW = 'now'
27 | CONF_SUGGESTION = 'suggestion'
28 | CONF_UPDATE_INTERVAL = 'interval'
29 | CONF_CITY = 'city'
30 | CONF_ISSHOWWEATHERPIC = 'isShowWeatherPic'
31 | CONF_ISDEBUG = 'isDebug'
32 |
33 | AQI_TYPES = {
34 | 'aqi': ['AQI', None],
35 | "co": ['AQI_CO', None],
36 | "no2": ['AQI_NO2', None],
37 | "o3": ['AQI_O3', None],
38 | "pm10": ['AQI_PM10', 'mg/m3'],
39 | "pm25": ['AQI_PM25', 'μg/m3'],
40 | "qlty": ['AQI_QLTY', None],
41 | "so2": ['AQI_SO2', None],
42 | }
43 |
44 | DAY_FORECAST_TYPES = {
45 | 'sr': ['Day_SR', None],
46 | 'ss': ['Day_SS', None],
47 | 'mr': ['Day_MR', None],
48 | 'ms': ['Day_MS', None],
49 | 'Weather_d': ['Day_Weather_Day', None],
50 | 'Weather_n': ['Day_Weather_Night', None],
51 | 'hum': ['Day_HUM', '%'],
52 | 'pcpn': ['Day_PCPN', 'mm'],
53 | 'pop': ['Day_POP', '%'],
54 | 'pres': ['Day_PRES', None],
55 | 'maxTmp': ['Day_MaxTmp', '°C'],
56 | 'minTmp': ['Day_MinTmp', '°C'],
57 | 'uv': ['Day_UV', None],
58 | 'vis': ['Day_VIS', 'Km'],
59 | 'deg': ['Day_DEG', '°'],
60 | 'dir': ['Day_DIR', None],
61 | 'sc': ['Day_SC', None],
62 | 'spd': ['Day_SPD', 'Km/h'],
63 | }
64 |
65 | HOUR_FORECAST_TYPE = {
66 | 'Weather': ['Hour_Weather', None],
67 | 'hum': ['Hour_HUM', None],
68 | 'pop': ['Hour_POP', '%'],
69 | 'pres': ['Hour_PRES', 'hPa'],
70 | 'tmp': ['Hour_Tmp', '°C'],
71 | 'deg': ['Hour_DEG', '°'],
72 | 'dir': ['Hour_DIR', None],
73 | 'sc': ['Hour_SC', None],
74 | 'spd': ['Hour_SPD', 'Km/h'],
75 | }
76 |
77 | NOW_FORECAST_TYPE = {
78 | 'Weather': ['Now_Weather', None],
79 | 'fl': ['Now_Fl', None],
80 | 'hum': ['Now_HUM', None],
81 | 'pcpn': ['Now_PCPN', 'mm'],
82 | 'pres': ['Now_PRES', 'hPa'],
83 | 'tmp': ['Now_Tmp', '°C'],
84 | 'vis': ['Now_VIS', 'Km'],
85 | 'deg': ['Now_DEG', '°'],
86 | 'dir': ['Now_DIR', None],
87 | 'sc': ['Now_SC', None],
88 | 'spd': ['Now_SPD', 'Km/h'],
89 | }
90 |
91 | Weather_images = {
92 | '100':'http://www.z4a.net/images/2017/03/29/100.png',
93 | '101':'http://www.z4a.net/images/2017/03/29/101.png',
94 | '102':'http://www.z4a.net/images/2017/03/29/102.png',
95 | '103':'http://www.z4a.net/images/2017/03/29/103.png',
96 | '104':'http://www.z4a.net/images/2017/03/29/104.png',
97 |
98 | '200':'http://www.z4a.net/images/2017/03/29/200.png',
99 | '201':'http://www.z4a.net/images/2017/03/29/201.png',
100 | '202':'http://www.z4a.net/images/2017/03/29/202.png',
101 | '203':'http://www.z4a.net/images/2017/03/29/202.png',
102 | '204':'http://www.z4a.net/images/2017/03/29/202.png',
103 | '205':'http://www.z4a.net/images/2017/03/29/205.png',
104 | '206':'http://www.z4a.net/images/2017/03/29/205.png',
105 | '207':'http://www.z4a.net/images/2017/03/29/205.png',
106 | '208':'http://www.z4a.net/images/2017/03/29/208.png',
107 | '209':'http://www.z4a.net/images/2017/03/29/208.png',
108 | '210':'http://www.z4a.net/images/2017/03/29/208.png',
109 | '211':'http://www.z4a.net/images/2017/03/29/208.png',
110 | '212':'http://www.z4a.net/images/2017/03/29/208.png',
111 | '213':'http://www.z4a.net/images/2017/03/29/208.png',
112 |
113 | '300':'http://www.z4a.net/images/2017/03/29/300.png',
114 | '301':'http://www.z4a.net/images/2017/03/29/301.png',
115 | '302':'http://www.z4a.net/images/2017/03/29/302.png',
116 | '303':'http://www.z4a.net/images/2017/03/29/303.png',
117 | '304':'http://www.z4a.net/images/2017/03/29/304.png',
118 | '305':'http://www.z4a.net/images/2017/03/29/305.png',
119 | '306':'http://www.z4a.net/images/2017/03/29/306.png',
120 | '307':'http://www.z4a.net/images/2017/03/29/307.png',
121 | '308':'http://www.z4a.net/images/2017/03/29/308.png',
122 | '309':'http://www.z4a.net/images/2017/03/29/309.png',
123 | '310':'http://www.z4a.net/images/2017/03/29/310.png',
124 | '311':'http://www.z4a.net/images/2017/03/29/311.png',
125 | '312':'http://www.z4a.net/images/2017/03/29/311.png',
126 | '313':'http://www.z4a.net/images/2017/03/29/313.png',
127 |
128 |
129 | '400':'http://www.z4a.net/images/2017/03/29/400.png',
130 | '401':'http://www.z4a.net/images/2017/03/29/401.png',
131 | '402':'http://www.z4a.net/images/2017/03/29/402.png',
132 | '403':'http://www.z4a.net/images/2017/03/29/403.png',
133 | '404':'http://www.z4a.net/images/2017/03/29/404.png',
134 | '405':'http://www.z4a.net/images/2017/03/29/405.png',
135 | '406':'http://www.z4a.net/images/2017/03/29/406.png',
136 | '407':'http://www.z4a.net/images/2017/03/29/407.png',
137 |
138 | '500':'http://www.z4a.net/images/2017/03/29/500.png',
139 | '501':'http://www.z4a.net/images/2017/03/29/501.png',
140 | '502':'http://www.z4a.net/images/2017/03/29/502.png',
141 | '503':'http://www.z4a.net/images/2017/03/29/503.png',
142 | '504':'http://www.z4a.net/images/2017/03/29/504.png',
143 | '505':'http://www.z4a.net/images/2017/03/29/504.png',
144 | '506':'http://www.z4a.net/images/2017/03/29/504.png',
145 | '507':'http://www.z4a.net/images/2017/03/29/507.png',
146 | '508':'http://www.z4a.net/images/2017/03/29/508.png',
147 |
148 | '900':'http://www.z4a.net/images/2017/03/29/900.png',
149 | '901':'http://www.z4a.net/images/2017/03/29/901.png',
150 | '999':'http://www.z4a.net/images/2017/03/29/999.png',
151 | }
152 |
153 | SUGGESTION_FORECAST_TYPE = {
154 | 'brf': ['Suggestion_BRF', None],
155 | 'txt': ['Suggestion_TXT', None],
156 | }
157 | MODULE_SUGGESTION = vol.Schema({
158 | vol.Required(cv.string, default=[]):
159 | vol.All(cv.ensure_list, [vol.In(SUGGESTION_FORECAST_TYPE)]),
160 | })
161 |
162 | MODULE_SCHEMA = vol.Schema({
163 | vol.Required(CONF_AQI,default=[]):vol.All(cv.ensure_list,[vol.In(AQI_TYPES)]),
164 | vol.Required(CONF_TODAY_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(DAY_FORECAST_TYPES)]),
165 | vol.Required(CONF_TOMORROW_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(DAY_FORECAST_TYPES)]),
166 | vol.Required(CONF_OFTERTOMORROW_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(DAY_FORECAST_TYPES)]),
167 | vol.Required(CONF_1HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
168 | vol.Required(CONF_3HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
169 | vol.Required(CONF_6HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
170 | vol.Required(CONF_9HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
171 | vol.Required(CONF_12HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
172 | vol.Required(CONF_15HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
173 | vol.Required(CONF_18HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
174 | vol.Required(CONF_21HOUR_FORECAST,default=None):vol.All(cv.ensure_list, [vol.In(HOUR_FORECAST_TYPE)]),
175 | vol.Required(CONF_NOW,default=None):vol.All(cv.ensure_list, [vol.In(NOW_FORECAST_TYPE)]),
176 | vol.Required(CONF_SUGGESTION,default=None): MODULE_SUGGESTION,
177 | })
178 |
179 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
180 | vol.Optional(CONF_MONITORED_CONDITIONS): MODULE_SCHEMA,
181 | vol.Optional(CONF_LATITUDE): cv.latitude,
182 | vol.Optional(CONF_LONGITUDE): cv.longitude,
183 | vol.Optional(CONF_API_KEY): cv.string,
184 | vol.Optional(CONF_CITY,default=None):cv.string,
185 | vol.Optional(CONF_ISSHOWWEATHERPIC,default=False):cv.boolean,
186 | vol.Optional(CONF_ISDEBUG,default=False):cv.boolean,
187 | vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (vol.All(cv.time_period, cv.positive_timedelta)),
188 |
189 | })
190 |
191 | _Log=logging.getLogger(__name__)
192 |
193 |
194 | def setup_platform(hass, config, add_devices, discovery_info=None):
195 | latitude = config.get(CONF_LATITUDE, hass.config.latitude)
196 | longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
197 | api_key = config.get(CONF_API_KEY,None)
198 | interval = config.get(CONF_UPDATE_INTERVAL)
199 | city = config.get(CONF_CITY)
200 | isShowWeatherPic = config.get(CONF_ISSHOWWEATHERPIC)
201 | isDebug = config.get(CONF_ISDEBUG)
202 | monitored_conditions = config[CONF_MONITORED_CONDITIONS]
203 |
204 | if None in (latitude, longitude) and None == city :
205 | _Log.error("Latitude or longitude or city not set in Home Assistant config")
206 | return False
207 |
208 | if api_key == None:
209 | _Log.error('Pls enter api_key!')
210 | return False
211 |
212 | weatherData = HeWeatherData(
213 | api_key = api_key,
214 | latitude = latitude,
215 | longitude = longitude,
216 | city = city,
217 | isDebug= isDebug,
218 | interval = interval
219 | )
220 | weatherData.update()
221 | weatherData.GetDataBySensor_Type(CONF_AQI)
222 | if weatherData.data == None:
223 | _Log.error('weatherData is nil')
224 | return False
225 |
226 | dev = []
227 | if CONF_AQI in monitored_conditions:
228 | aqiSensor = monitored_conditions['aqi']
229 | if isinstance(aqiSensor, list):
230 | if len(aqiSensor) == 0:
231 | sensor_Name = AQI_TYPES['aqi'][0]
232 | measurement = AQI_TYPES['aqi'][1]
233 | dev.append(HeWeatherSensor(weatherData, CONF_AQI, 'aqi', sensor_Name, isShowWeatherPic,measurement))
234 | for sensor in aqiSensor:
235 | sensor_Name = AQI_TYPES[sensor][0]
236 | measurement = AQI_TYPES[sensor][1]
237 | dev.append(HeWeatherSensor(weatherData,CONF_AQI,sensor,sensor_Name,measurement))
238 |
239 | if CONF_TODAY_FORECAST in monitored_conditions:
240 | DaySensor = monitored_conditions[CONF_TODAY_FORECAST]
241 | if isinstance(DaySensor,list):
242 | if len(DaySensor) == 0:
243 | sensor_Name = DAY_FORECAST_TYPES['Weather_d'][0]
244 | measurement = DAY_FORECAST_TYPES['Weather_d'][1]
245 | dev.append(HeWeatherSensor(weatherData, CONF_TODAY_FORECAST, 'Weather_d', sensor_Name, isShowWeatherPic,measurement))
246 | for sensor in DaySensor:
247 | sensor_Name = DAY_FORECAST_TYPES[sensor][0]
248 | measurement = DAY_FORECAST_TYPES[sensor][1]
249 | dev.append(HeWeatherSensor(weatherData, CONF_TODAY_FORECAST, sensor, sensor_Name, isShowWeatherPic,measurement))
250 |
251 | if CONF_TOMORROW_FORECAST in monitored_conditions:
252 | DaySensor = monitored_conditions[CONF_TOMORROW_FORECAST]
253 | if isinstance(DaySensor, list):
254 | if isinstance(DaySensor, list):
255 | if len(DaySensor) == 0:
256 | sensor_Name = DAY_FORECAST_TYPES['Weather_d'][0]
257 | measurement = DAY_FORECAST_TYPES['Weather_d'][1]
258 | dev.append(HeWeatherSensor(weatherData, CONF_TOMORROW_FORECAST, 'Weather_d',sensor_Name,measurement))
259 | for sensor in DaySensor:
260 | sensor_Name = DAY_FORECAST_TYPES[sensor][0]
261 | measurement = DAY_FORECAST_TYPES[sensor][1]
262 | dev.append(HeWeatherSensor(weatherData, CONF_TOMORROW_FORECAST, sensor, sensor_Name, isShowWeatherPic,measurement))
263 |
264 | if CONF_OFTERTOMORROW_FORECAST in monitored_conditions:
265 | DaySensor = monitored_conditions[CONF_OFTERTOMORROW_FORECAST]
266 | if isinstance(DaySensor, list):
267 | if len(DaySensor) == 0:
268 | sensor_Name = DAY_FORECAST_TYPES['Weather_d'][0]
269 | measurement = DAY_FORECAST_TYPES['Weather_d'][1]
270 | dev.append(HeWeatherSensor(weatherData, CONF_OFTERTOMORROW_FORECAST, 'Weather_d', sensor_Name, isShowWeatherPic,measurement))
271 | for sensor in DaySensor:
272 | sensor_Name = DAY_FORECAST_TYPES[sensor][0]
273 | measurement = DAY_FORECAST_TYPES[sensor][1]
274 | dev.append(HeWeatherSensor(weatherData, CONF_OFTERTOMORROW_FORECAST, sensor, sensor_Name, isShowWeatherPic,measurement))
275 |
276 | if CONF_1HOUR_FORECAST in monitored_conditions:
277 | HourSensor = monitored_conditions[CONF_1HOUR_FORECAST]
278 | if isinstance(HourSensor, list):
279 | if len(HourSensor) == 0:
280 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
281 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
282 | dev.append(HeWeatherSensor(weatherData, CONF_1HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic,measurement))
283 | for sensor in HourSensor:
284 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
285 | measurement = HOUR_FORECAST_TYPE[sensor][1]
286 | dev.append(HeWeatherSensor(weatherData, CONF_1HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic,measurement))
287 |
288 | if CONF_3HOUR_FORECAST in monitored_conditions:
289 | HourSensor = monitored_conditions[CONF_3HOUR_FORECAST]
290 | if isinstance(HourSensor, list):
291 | if len(HourSensor) == 0:
292 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
293 | measurement = HOUR_FORECAST_TYPE[sensor][1]
294 | dev.append(HeWeatherSensor(weatherData, CONF_3HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
295 | for sensor in HourSensor:
296 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
297 | measurement = HOUR_FORECAST_TYPE[sensor][1]
298 | dev.append(HeWeatherSensor(weatherData, CONF_3HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
299 |
300 | if CONF_6HOUR_FORECAST in monitored_conditions:
301 | HourSensor = monitored_conditions[CONF_6HOUR_FORECAST]
302 | if isinstance(HourSensor, list):
303 | if len(HourSensor) == 0:
304 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
305 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
306 | dev.append(HeWeatherSensor(weatherData, CONF_6HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
307 | for sensor in HourSensor:
308 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
309 | measurement = HOUR_FORECAST_TYPE[sensor][1]
310 | dev.append(HeWeatherSensor(weatherData, CONF_6HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
311 |
312 | if CONF_9HOUR_FORECAST in monitored_conditions:
313 | HourSensor = monitored_conditions[CONF_9HOUR_FORECAST]
314 | if isinstance(HourSensor, list):
315 | if len(HourSensor) == 0:
316 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
317 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
318 | dev.append(HeWeatherSensor(weatherData, CONF_9HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
319 | for sensor in HourSensor:
320 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
321 | measurement = HOUR_FORECAST_TYPE[sensor][1]
322 | dev.append(HeWeatherSensor(weatherData, CONF_9HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
323 |
324 | if CONF_12HOUR_FORECAST in monitored_conditions:
325 | HourSensor = monitored_conditions[CONF_12HOUR_FORECAST]
326 | if isinstance(HourSensor, list):
327 | if len(HourSensor) == 0:
328 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
329 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
330 | dev.append(HeWeatherSensor(weatherData, CONF_12HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
331 | for sensor in HourSensor:
332 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
333 | measurement = HOUR_FORECAST_TYPE[sensor][1]
334 | dev.append(HeWeatherSensor(weatherData, CONF_12HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
335 |
336 | if CONF_15HOUR_FORECAST in monitored_conditions:
337 | HourSensor = monitored_conditions[CONF_15HOUR_FORECAST]
338 | if isinstance(HourSensor, list):
339 | if len(HourSensor) == 0:
340 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
341 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
342 | dev.append(HeWeatherSensor(weatherData, CONF_15HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
343 | for sensor in HourSensor:
344 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
345 | measurement = HOUR_FORECAST_TYPE[sensor][1]
346 | dev.append(HeWeatherSensor(weatherData, CONF_15HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
347 |
348 | if CONF_18HOUR_FORECAST in monitored_conditions:
349 | HourSensor = monitored_conditions[CONF_18HOUR_FORECAST]
350 | if isinstance(HourSensor, list):
351 | if len(HourSensor) == 0:
352 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
353 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
354 | dev.append(HeWeatherSensor(weatherData, CONF_18HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
355 | for sensor in HourSensor:
356 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
357 | measurement = HOUR_FORECAST_TYPE[sensor][1]
358 | dev.append(HeWeatherSensor(weatherData, CONF_18HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
359 |
360 | if CONF_21HOUR_FORECAST in monitored_conditions:
361 | HourSensor = monitored_conditions[CONF_21HOUR_FORECAST]
362 | if isinstance(HourSensor, list):
363 | if len(HourSensor) == 0:
364 | sensor_Name = HOUR_FORECAST_TYPE['Weather'][0]
365 | measurement = HOUR_FORECAST_TYPE['Weather'][1]
366 | dev.append(HeWeatherSensor(weatherData, CONF_21HOUR_FORECAST, 'Weather', sensor_Name, isShowWeatherPic, measurement))
367 | for sensor in HourSensor:
368 | sensor_Name = HOUR_FORECAST_TYPE[sensor][0]
369 | measurement = HOUR_FORECAST_TYPE[sensor][1]
370 | dev.append(HeWeatherSensor(weatherData, CONF_21HOUR_FORECAST, sensor, sensor_Name, isShowWeatherPic, measurement))
371 |
372 | if CONF_NOW in monitored_conditions:
373 | NowSensor = monitored_conditions[CONF_NOW]
374 | if isinstance(NowSensor, list):
375 | if len(NowSensor) == 0:
376 | sensor_Name = NOW_FORECAST_TYPE['Weather'][0]
377 | measurement = NOW_FORECAST_TYPE['Weather'][1]
378 | dev.append(HeWeatherSensor(weatherData, CONF_NOW, 'Weather', sensor_Name, isShowWeatherPic, measurement))
379 | for sensor in NowSensor:
380 | sensor_Name = NOW_FORECAST_TYPE[sensor][0]
381 | measurement = NOW_FORECAST_TYPE[sensor][1]
382 | dev.append(HeWeatherSensor(weatherData, CONF_NOW, sensor, sensor_Name, isShowWeatherPic, measurement))
383 |
384 | if CONF_SUGGESTION in monitored_conditions:
385 | SuggestionSensor = monitored_conditions[CONF_SUGGESTION]
386 | if isinstance(SuggestionSensor, dict):
387 | for variable in SuggestionSensor:
388 | sensors = SuggestionSensor[variable]
389 | if len(sensors) == 0:
390 | sensor_Name = SUGGESTION_FORECAST_TYPE['brf'][0]
391 | measurement = SUGGESTION_FORECAST_TYPE['brf'][1]
392 | dev.append(HeWeatherSensor(weatherData, CONF_SUGGESTION, 'brf', sensor_Name, isShowWeatherPic, measurement,variable))
393 | for sensor in sensors:
394 | sensor_Name = SUGGESTION_FORECAST_TYPE[sensor][0]
395 | measurement = SUGGESTION_FORECAST_TYPE[sensor][1]
396 | dev.append(HeWeatherSensor(weatherData, CONF_SUGGESTION, sensor, sensor_Name, isShowWeatherPic, measurement,variable))
397 |
398 | add_devices(dev, True)
399 |
400 | class HeWeatherSensor(Entity):
401 | def __init__(self,weatherData,sensor_Type,sensor,sensor_Name,isShowWeatherPic,measurement = None,suggestionType = None):
402 | """Initialize the sensor."""
403 | self.weatherData = weatherData
404 | self._sensor_Type = sensor_Type
405 | self._sensor = sensor
406 | self._name = sensor_Name
407 | self._isShowWeatherPic = isShowWeatherPic
408 | self._unit_of_measurement = measurement
409 | self._suggestionType = suggestionType
410 |
411 | self._state = None
412 | self._weatherCode = None
413 |
414 | @property
415 | def name(self):
416 | """Return the name of the sensor."""
417 | if self._sensor_Type == CONF_AQI:
418 | return self._name
419 | if self._sensor_Type == CONF_TODAY_FORECAST:
420 | return 'To'+ self._name
421 | if self._sensor_Type == CONF_TOMORROW_FORECAST:
422 | return 'Tomorrow'+ self._name
423 | if self._sensor_Type == CONF_OFTERTOMORROW_FORECAST:
424 | return 'OfterTomorrow'+ self._name
425 | if self._sensor_Type == CONF_1HOUR_FORECAST:
426 | return '1'+ self._name
427 | if self._sensor_Type == CONF_3HOUR_FORECAST:
428 | return '3'+ self._name
429 | if self._sensor_Type == CONF_6HOUR_FORECAST:
430 | return '6'+ self._name
431 | if self._sensor_Type == CONF_9HOUR_FORECAST:
432 | return '9'+ self._name
433 | if self._sensor_Type == CONF_12HOUR_FORECAST:
434 | return '12'+ self._name
435 | if self._sensor_Type == CONF_15HOUR_FORECAST:
436 | return '15'+ self._name
437 | if self._sensor_Type == CONF_18HOUR_FORECAST:
438 | return '18'+ self._name
439 | if self._sensor_Type == CONF_21HOUR_FORECAST:
440 | return '21'+ self._name
441 | if self._sensor_Type == CONF_NOW:
442 | return self._name
443 | if self._sensor_Type == CONF_SUGGESTION:
444 | return self._suggestionType + '_'+ self._name
445 |
446 | @property
447 | def entity_picture(self):
448 | """Weather symbol if type is symbol."""
449 | if not self._isShowWeatherPic:
450 | return
451 | if self._sensor != 'Weather_d' and self._sensor != 'Weather_n' and self._sensor != 'Weather':
452 | return None
453 |
454 | if self._weatherCode == None:
455 | return
456 | return Weather_images[self._weatherCode]
457 |
458 |
459 |
460 | @property
461 | def state(self):
462 | """Return the state of the sensor."""
463 | return self._state
464 | @property
465 | def unit_of_measurement(self):
466 | """Return the unit of measurement."""
467 | return self._unit_of_measurement
468 |
469 | def update(self):
470 | self.weatherData.update()
471 | # _Log.error('_sensor_Type ==%s' % self._sensor)
472 | if self._sensor_Type == CONF_AQI:
473 | data = self.weatherData.GetDataBySensor_Type(self._sensor_Type)
474 | if data == None:
475 | return
476 | if 'city' in data:
477 | cityData = data['city']
478 | if self._sensor in cityData:
479 | statusData = cityData[self._sensor]
480 | self._state = statusData
481 |
482 | elif self._sensor_Type == CONF_TODAY_FORECAST:
483 | data = self.weatherData.GetDataBySensor_Type('daily_forecast')
484 | if data == None:
485 | return
486 | if len(data) > 0 :
487 | dayData = data[0]
488 | self._SetDay_Forecast_Status(dayData)
489 | elif self._sensor_Type == CONF_TOMORROW_FORECAST:
490 | data = self.weatherData.GetDataBySensor_Type('daily_forecast')
491 | if data == None:
492 | return
493 | if len(data) > 1:
494 | dayData = data[1]
495 | self._SetDay_Forecast_Status(dayData)
496 | elif self._sensor_Type == CONF_OFTERTOMORROW_FORECAST:
497 | data = self.weatherData.GetDataBySensor_Type('daily_forecast')
498 | if data == None:
499 | return
500 | if len(data) > 2:
501 | dayData = data[2]
502 | self._SetDay_Forecast_Status(dayData)
503 |
504 | elif self._sensor_Type == CONF_1HOUR_FORECAST:
505 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
506 | if data == None:
507 | return
508 | if len(data) > 0:
509 | HourData = data[0]
510 | self._SetHourly_Forecast_Status(HourData)
511 | elif self._sensor_Type == CONF_3HOUR_FORECAST:
512 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
513 | if data == None:
514 | return
515 | if len(data) > 2:
516 | HourData = data[2]
517 | self._SetHourly_Forecast_Status(HourData)
518 | elif self._sensor_Type == CONF_6HOUR_FORECAST:
519 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
520 | if data == None:
521 | return
522 | if len(data) > 3:
523 | HourData = data[3]
524 | self._SetHourly_Forecast_Status(HourData)
525 | elif self._sensor_Type == CONF_9HOUR_FORECAST:
526 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
527 | if data == None:
528 | return
529 | if len(data) > 4:
530 | HourData = data[4]
531 | self._SetHourly_Forecast_Status(HourData)
532 | elif self._sensor_Type == CONF_12HOUR_FORECAST:
533 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
534 | if data == None:
535 | return
536 | if len(data) > 5:
537 | HourData = data[5]
538 | self._SetHourly_Forecast_Status(HourData)
539 | elif self._sensor_Type == CONF_15HOUR_FORECAST:
540 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
541 | if data == None:
542 | return
543 | if len(data) > 6:
544 | HourData = data[6]
545 | self._SetHourly_Forecast_Status(HourData)
546 | elif self._sensor_Type == CONF_18HOUR_FORECAST:
547 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
548 | if data == None:
549 | return
550 | if len(data) > 7:
551 | HourData = data[7]
552 | self._SetHourly_Forecast_Status(HourData)
553 | elif self._sensor_Type == CONF_21HOUR_FORECAST:
554 | data = self.weatherData.GetDataBySensor_Type('hourly_forecast')
555 | if data == None:
556 | return
557 | if len(data) > 8:
558 | HourData = data[8]
559 | self._SetHourly_Forecast_Status(HourData)
560 | elif self._sensor_Type == CONF_NOW:
561 | NowData = self.weatherData.GetDataBySensor_Type('now')
562 | if NowData == None:
563 | return
564 | self._SetHourly_Forecast_Status(NowData)
565 | elif self._sensor_Type == CONF_SUGGESTION:
566 | suggestionData = self.weatherData.GetDataBySensor_Type('suggestion')
567 | if suggestionData == None:
568 | return
569 | if self._suggestionType == None:
570 | return
571 |
572 | if self._suggestionType in suggestionData:
573 | sData = suggestionData[self._suggestionType]
574 | if self._sensor in sData:
575 | statusData = sData[self._sensor]
576 | self._state = statusData
577 |
578 | # SetDaily_forecast Data
579 | def _SetDay_Forecast_Status(self,dayData):
580 |
581 | if self._sensor in ('mr', 'ms', 'sr', 'ss'):
582 | if 'astro' in dayData:
583 | astroData = dayData['astro']
584 | if self._sensor in astroData:
585 | statusData = astroData[self._sensor]
586 | self._state = statusData
587 |
588 | elif self._sensor in ('Weather_d', 'Weather_n'):
589 | if 'cond' in dayData:
590 | condData = dayData['cond']
591 | sensor = ''
592 | code = ''
593 | if self._sensor == 'Weather_d':
594 | sensor = 'txt_d'
595 | code = 'code_d'
596 | elif self._sensor == 'Weather_n':
597 | sensor = 'txt_n'
598 | code = 'code_n'
599 | if sensor in condData:
600 | statusData = condData[sensor]
601 | codeData = condData[code]
602 | self._state = statusData
603 | self._weatherCode = codeData
604 |
605 | elif self._sensor in ('maxTmp', 'minTmp'):
606 | if 'tmp' in dayData:
607 | tmpData = dayData['tmp']
608 | sensor = ''
609 | if self._sensor == 'maxTmp':
610 | sensor = 'max'
611 | elif self._sensor == 'minTmp':
612 | sensor = 'min'
613 |
614 | if sensor in tmpData:
615 | statusData = tmpData[sensor]
616 | self._state = statusData
617 |
618 | elif self._sensor in ('deg', 'dir', 'sc', 'spd'):
619 | if 'wind' in dayData:
620 | windData = dayData['wind']
621 | if self._sensor in windData:
622 | statusData = windData[self._sensor]
623 | self._state = statusData
624 |
625 | if self._sensor in dayData:
626 | statusData = dayData[self._sensor]
627 | self._state = statusData
628 | def _SetHourly_Forecast_Status(self,hourlyData):
629 | if self._sensor == 'Weather':
630 | if 'cond' in hourlyData:
631 | condData = hourlyData['cond']
632 | statusData = condData['txt']
633 | codeData = condData['code']
634 | self._state = statusData
635 | self._weatherCode = codeData
636 |
637 | elif self._sensor in ('deg', 'dir', 'sc', 'spd'):
638 | if 'wind' in hourlyData:
639 | windData = hourlyData['wind']
640 | if self._sensor in windData:
641 | statusData = windData[self._sensor]
642 | self._state = statusData
643 |
644 | if self._sensor in hourlyData:
645 | statusData = hourlyData[self._sensor]
646 | self._state = statusData
647 |
648 | class HeWeatherData(object):
649 | def __init__(self,api_key,latitude ,longitude,city,interval,isDebug):
650 | self._api_key = api_key
651 | self.latitude = latitude
652 | self.longitude = longitude
653 | self.city = city
654 | self.isDebug = isDebug
655 |
656 | self.data = None
657 |
658 | self.update = Throttle(interval)(self._update)
659 |
660 |
661 | def _update(self):
662 | city = '%s,%s' % (self.longitude,self.latitude)
663 |
664 | if not None == self.city:
665 | city = self.city
666 | interface = 'https://api.heweather.com/v5/weather?city=%s&key=%s' % (city ,self._api_key)
667 |
668 | resp = requests.get(interface)
669 |
670 | if resp.status_code != 200:
671 | _Log.error('http get data Error StatusCode:%s' % resp.status_code)
672 | return
673 |
674 | self.data = resp.json()
675 | if not 'HeWeather5' in self.data:
676 | _Log.error('Json Status Error1!')
677 | return
678 |
679 | HeWeather5 = self.data['HeWeather5']
680 | HeWeather5Dic = HeWeather5[0]
681 |
682 | if not 'status' in HeWeather5Dic:
683 | _Log.error('Json Status Error2!')
684 | return
685 |
686 | status = HeWeather5Dic['status']
687 | if status != 'ok':
688 | _Log.error('Json Status Not Good!')
689 | return
690 | self.data =HeWeather5Dic
691 | if self.isDebug:
692 | _Log.info('HeWeather5DicData:%s' % self.data)
693 |
694 | def GetDataBySensor_Type(self,sensor_Type):
695 | if self.data == None:
696 | return None
697 | if sensor_Type in self.data:
698 | return self.data[sensor_Type]
--------------------------------------------------------------------------------
/sensor/WeatherChina.py:
--------------------------------------------------------------------------------
1 | '''
2 | WeatherChina Developer by Charley
3 | '''
4 | import logging
5 | import voluptuous as vol
6 |
7 | from homeassistant.const import TEMP_CELSIUS
8 | from homeassistant.helpers.entity import Entity
9 | from homeassistant.components.sensor import PLATFORM_SCHEMA
10 | import homeassistant.helpers.config_validation as cv
11 |
12 | import requests
13 | import json
14 |
15 |
16 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
17 | vol.Required('CityCode',default=[]):
18 | vol.All(cv.ensure_list(cv.string))
19 | })
20 |
21 | _Logger=logging.getLogger(__name__)
22 |
23 |
24 | def setup_platform(hass, config, add_devices, discovery_info=None):
25 | """Setup the sensor platform."""
26 |
27 | # global _CityCodes
28 | cityCodes = config.get('CityCode')
29 | dev = []
30 | for code in cityCodes:
31 | # _Logger.info('[WeatherChina] init CityCode======>'+ code)
32 | dev.append(
33 | WeatherChina(code)
34 | )
35 |
36 | add_devices(dev)
37 |
38 |
39 | class WeatherChina(Entity):
40 | """Representation of a Sensor."""
41 |
42 | def __init__(self,cityCode):
43 | """Initialize the sensor."""
44 | self._state = None
45 | self._cityCode = cityCode
46 | self._weatherData=None
47 |
48 | @property
49 | def name(self):
50 | """Return the name of the sensor."""
51 |
52 | city = self.getWeatherData("city")
53 | weather = self.getWeatherData("weather")
54 | return city + ' Temperature'
55 |
56 | @property
57 | def state(self):
58 | """Return the state of the sensor."""
59 | tmp = '%s'% self.getWeatherData("temp2")
60 | tmp = tmp[:-1]
61 | return tmp
62 |
63 | @property
64 | def unit_of_measurement(self):
65 | """Return the unit of measurement."""
66 | return TEMP_CELSIUS
67 |
68 |
69 | def update(self):
70 | """Fetch new state data for the sensor.
71 |
72 | This is the only method that should fetch new data for Home Assistant.
73 | """
74 |
75 | # _Logger.info("[WeatherChina] update =====>"+ self._cityCode)
76 | if self._cityCode == '':
77 | _Logger.error("[WeatherChina]: CityCode is nil")
78 | return
79 |
80 | urlStr = 'http://www.weather.com.cn/data/cityinfo/' + self._cityCode + '.html'
81 | # resp = None
82 | # try:
83 |
84 | # _Logger.info("[WeatherChina] updateUrl =====>" +urlStr)
85 |
86 | resp = requests.get(urlStr)
87 | if resp.status_code != 200:
88 | _Logger.error("http get Error code:" + resp.status_code)
89 | return
90 | resp.encoding = "utf-8"
91 | weatherDataStr=resp.text
92 | self._weatherData = json.loads(weatherDataStr)
93 |
94 |
95 | def getWeatherData(self,key):
96 | if self._weatherData == None:
97 | return ""
98 |
99 |
100 | if not "weatherinfo" in self._weatherData:
101 | return ""
102 |
103 | weatherinfo = self._weatherData["weatherinfo"]
104 |
105 | if not key in weatherinfo:
106 | return ""
107 |
108 | return weatherinfo[key]
109 |
--------------------------------------------------------------------------------
/sensor/__pycache__/WeatherChina.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/1cf9d45a7a32b15f40468a2fbbe34ef17efd9991/sensor/__pycache__/WeatherChina.cpython-36.pyc
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.idea/emulated_hue_charley.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 | typd
58 | TYPE_
59 | CONF_TYPE
60 | TYPE_GOOGLE
61 | DescriptionXmlView
62 | tt
63 | _LOg
64 | whitelist
65 | is_entity_exposed
66 | local
67 | util
68 | timezone
69 | get_
70 | get
71 | HomeAssistantHTTP
72 | HomeAssistantView
73 | config
74 |
75 |
76 | $PROJECT_DIR$
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | true
93 | DEFINITION_ORDER
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | 1521255310596
168 |
169 |
170 | 1521255310596
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/Users/charley/Downloads/ha/venv/bin"
3 | }
--------------------------------------------------------------------------------
/service/emulated_hue_charley/README.md:
--------------------------------------------------------------------------------
1 | # hue emulated
2 | ## Description
3 | Support DingDong Smart SoundBox
4 | if you like what you're seeing! give me a smoke,tks. :)
5 |
6 | 
7 |
8 |
9 | ## Installation
10 | ------
11 | copy all the files into the Home Assistant location. It can now be installed either to the custom_components folder
12 | ```
13 | /home/homeassistant/.homeassistant/custom_components
14 | ```
15 | or the root folder (using virtual environment)
16 | ```
17 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
18 | ```
19 | if you use DingDong Smart SoundBox
20 | Add the following line to the Configuration.yaml.
21 | ```
22 | emulated_hue_charley:
23 | type: dingdong
24 | ```
25 |
26 | AutoLinkButtn:
27 | Add the following line to the Configuration.yaml.
28 | ```
29 | emulated_hue_charley:
30 | type: dingdong
31 | auto_link: true
32 | ```
33 |
34 | Other parameters refer to [HomeAssistant](https://home-assistant.io/components/emulated_hue/)
35 |
36 |
37 | #中文
38 |
39 | # Hue模拟器
40 | ## 描述
41 | 在原官方模拟器上添加叮咚智能音箱的支持
42 |
43 | ## 安装
44 | 在配置文件目录创建custom_components
45 | 复制emulated_hue_charley文件夹到custom_components目录
46 | ```
47 | /home/homeassistant/.homeassistant/custom_components
48 | ```
49 | 或者复制到系统目录
50 | ```
51 | /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/homeassistant/components
52 | ```
53 |
54 | 如果是你是使用叮咚智能音箱
55 | 在Configuration.yaml文件中添加一下字段
56 | ```
57 | emulated_hue_charley:
58 | type: dingdong
59 | ```
60 | 自动按下Link按钮
61 | ```
62 | emulated_hue_charley:
63 | type: dingdong
64 | auto_link: true
65 | ```
66 |
67 | 其他设置参考官方的模拟器设置[emulated_hue](https://home-assistant.io/components/emulated_hue/)
68 |
69 | 如果模拟器有任何问题请到QQ群或者[论坛](https://bbs.hassbian.com/thread-3135-1-1.html)发布你的问题
70 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for local control of entities by emulating the Phillips Hue bridge.
3 |
4 | For more details about this component, please refer to the documentation at
5 | https://home-assistant.io/components/emulated_hue/
6 | """
7 | import logging
8 |
9 | from aiohttp import web
10 | import voluptuous as vol
11 |
12 | from homeassistant import util
13 | from homeassistant.const import (
14 | EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
15 | )
16 | from homeassistant.components.http import REQUIREMENTS # NOQA
17 | from homeassistant.exceptions import HomeAssistantError
18 | from homeassistant.helpers.deprecation import get_deprecated
19 | import homeassistant.helpers.config_validation as cv
20 | from homeassistant.util.json import load_json, save_json
21 | from .hue_api import (
22 | HueDingDongConfigView, HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
23 | HueOneLightChangeView)
24 | from .upnp import DescriptionXmlView, UPNPResponderThread
25 |
26 | DOMAIN = 'emulated_hue_charley'
27 |
28 | _LOGGER = logging.getLogger(__name__)
29 |
30 | NUMBERS_FILE = 'emulated_hue_ids.json'
31 |
32 | CONF_HOST_IP = 'host_ip'
33 | CONF_LISTEN_PORT = 'listen_port'
34 | CONF_ADVERTISE_IP = 'advertise_ip'
35 | CONF_ADVERTISE_PORT = 'advertise_port'
36 | CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast'
37 | CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
38 | CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
39 | CONF_EXPOSED_DOMAINS = 'exposed_domains'
40 | CONF_TYPE = 'type'
41 | CONF_AUTOLINK = "auto_link"
42 | CONF_ENTITIES = 'entities'
43 | CONF_ENTITY_NAME = 'name'
44 | CONF_ENTITY_HIDDEN = 'hidden'
45 | CONF_NETMASK = 'netmask'
46 |
47 | TYPE_ALEXA = 'alexa'
48 | TYPE_GOOGLE = 'google_home'
49 | TYPE_DINGDONG = 'dingdong'
50 |
51 |
52 | DEFAULT_LISTEN_PORT = 8300
53 | DEFAULT_UPNP_BIND_MULTICAST = True
54 | DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
55 | DEFAULT_EXPOSE_BY_DEFAULT = True
56 | DEFAULT_EXPOSED_DOMAINS = [
57 | 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
58 | ]
59 | DEFAULT_TYPE = TYPE_GOOGLE
60 |
61 | CONFIG_ENTITY_SCHEMA = vol.Schema({
62 | vol.Optional(CONF_ENTITY_NAME): cv.string,
63 | vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean
64 | })
65 |
66 | CONFIG_SCHEMA = vol.Schema({
67 | DOMAIN: vol.Schema({
68 | vol.Optional(CONF_HOST_IP): cv.string,
69 | vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port,
70 | vol.Optional(CONF_ADVERTISE_IP): cv.string,
71 | vol.Optional(CONF_ADVERTISE_PORT): cv.port,
72 | vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean,
73 | vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
74 | vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
75 | vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
76 | vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
77 | vol.Any(TYPE_ALEXA, TYPE_GOOGLE,TYPE_DINGDONG),
78 | vol.Optional(CONF_AUTOLINK): cv.boolean,
79 | vol.Optional(CONF_ENTITIES):
80 | vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA}),
81 | vol.Optional(CONF_NETMASK,default='255.255.255.0'): cv.string
82 | })
83 | }, extra=vol.ALLOW_EXTRA)
84 |
85 | ATTR_EMULATED_HUE = 'emulated_hue'
86 | ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
87 | ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
88 |
89 |
90 | def setup(hass, yaml_config):
91 | """Activate the emulated_hue component."""
92 | timezone = yaml_config.get("homeassistant").get("time_zone")
93 | config = Config(hass, yaml_config.get(DOMAIN, {}), timezone)
94 |
95 | app = web.Application()
96 | app['hass'] = hass
97 | handler = None
98 | server = None
99 |
100 | DescriptionXmlView(config).register(app, app.router)
101 | HueDingDongConfigView(config).register(app, app.router)
102 | HueUsernameView().register(app, app.router)
103 | HueAllLightsStateView(config).register(app, app.router)
104 | HueOneLightStateView(config).register(app, app.router)
105 | HueOneLightChangeView(config).register(app, app.router)
106 |
107 | upnp_listener = UPNPResponderThread(
108 | config.host_ip_addr, config.listen_port,
109 | config.upnp_bind_multicast, config.advertise_ip,
110 | config.advertise_port)
111 |
112 | async def stop_emulated_hue_bridge(event):
113 | """Stop the emulated hue bridge."""
114 | upnp_listener.stop()
115 | if server:
116 | server.close()
117 | await server.wait_closed()
118 | await app.shutdown()
119 | if handler:
120 | await handler.shutdown(10)
121 | await app.cleanup()
122 |
123 | async def start_emulated_hue_bridge(event):
124 | """Start the emulated hue bridge."""
125 | upnp_listener.start()
126 | nonlocal handler
127 | nonlocal server
128 |
129 | handler = app.make_handler(loop=hass.loop)
130 |
131 | try:
132 | server = await hass.loop.create_server(
133 | handler, config.host_ip_addr, config.listen_port)
134 | except OSError as error:
135 | _LOGGER.error("Failed to create HTTP server at port %d: %s",
136 | config.listen_port, error)
137 | else:
138 | hass.bus.async_listen_once(
139 | EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
140 |
141 | hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
142 |
143 | return True
144 |
145 |
146 | class Config(object):
147 | """Hold configuration variables for the emulated hue bridge."""
148 |
149 | def __init__(self, hass, conf,timezone):
150 | """Initialize the instance."""
151 | self.hass = hass
152 | self.type = conf.get(CONF_TYPE)
153 | self.numbers = None
154 | self.cached_states = {}
155 | self.auto_link = conf.get(CONF_AUTOLINK)
156 | self.netmask = conf.get(CONF_NETMASK)
157 | self.timezone = timezone
158 |
159 | if self.type == TYPE_ALEXA:
160 | _LOGGER.warning(
161 | 'Emulated Hue running in legacy mode because type has been '
162 | 'specified. More info at https://goo.gl/M6tgz8')
163 |
164 | # Get the IP address that will be passed to the Echo during discovery
165 | self.host_ip_addr = conf.get(CONF_HOST_IP)
166 | if self.host_ip_addr is None:
167 | self.host_ip_addr = util.get_local_ip()
168 | _LOGGER.info(
169 | "Listen IP address not specified, auto-detected address is %s",
170 | self.host_ip_addr)
171 |
172 | # Get the port that the Hue bridge will listen on
173 | self.listen_port = conf.get(CONF_LISTEN_PORT)
174 | if not isinstance(self.listen_port, int):
175 | self.listen_port = DEFAULT_LISTEN_PORT
176 | _LOGGER.info(
177 | "Listen port not specified, defaulting to %s",
178 | self.listen_port)
179 |
180 | if self.type == TYPE_GOOGLE and self.listen_port != 80:
181 | _LOGGER.warning("When targeting Google Home, listening port has "
182 | "to be port 80")
183 |
184 | # Get whether or not UPNP binds to multicast address (239.255.255.250)
185 | # or to the unicast address (host_ip_addr)
186 | self.upnp_bind_multicast = conf.get(
187 | CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST)
188 |
189 | # Get domains that cause both "on" and "off" commands to map to "on"
190 | # This is primarily useful for things like scenes or scripts, which
191 | # don't really have a concept of being off
192 | self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
193 | if not isinstance(self.off_maps_to_on_domains, list):
194 | self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
195 |
196 | # Get whether or not entities should be exposed by default, or if only
197 | # explicitly marked ones will be exposed
198 | self.expose_by_default = conf.get(
199 | CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
200 |
201 | # Get domains that are exposed by default when expose_by_default is
202 | # True
203 | self.exposed_domains = conf.get(
204 | CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
205 |
206 | # Calculated effective advertised IP and port for network isolation
207 | self.advertise_ip = conf.get(
208 | CONF_ADVERTISE_IP) or self.host_ip_addr
209 |
210 | self.advertise_port = conf.get(
211 | CONF_ADVERTISE_PORT) or self.listen_port
212 |
213 | self.entities = conf.get(CONF_ENTITIES, {})
214 |
215 | def entity_id_to_number(self, entity_id):
216 | """Get a unique number for the entity id."""
217 | if self.type == TYPE_ALEXA:
218 | return entity_id
219 |
220 | if self.numbers is None:
221 | self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
222 |
223 | # Google Home
224 | for number, ent_id in self.numbers.items():
225 | if entity_id == ent_id:
226 | return number
227 |
228 | number = '1'
229 | if self.numbers:
230 | number = str(max(int(k) for k in self.numbers) + 1)
231 | self.numbers[number] = entity_id
232 | save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
233 | return number
234 |
235 | def number_to_entity_id(self, number):
236 | """Convert unique number to entity id."""
237 | if self.type == TYPE_ALEXA:
238 | return number
239 |
240 | if self.numbers is None:
241 | self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
242 |
243 | # Google Home
244 | assert isinstance(number, str)
245 | return self.numbers.get(number)
246 |
247 | def get_entity_name(self, entity):
248 | """Get the name of an entity."""
249 | if entity.entity_id in self.entities and \
250 | CONF_ENTITY_NAME in self.entities[entity.entity_id]:
251 | return self.entities[entity.entity_id][CONF_ENTITY_NAME]
252 |
253 | return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
254 |
255 | def is_entity_exposed(self, entity):
256 | """Determine if an entity should be exposed on the emulated bridge.
257 |
258 | Async friendly.
259 | """
260 | if entity.attributes.get('view') is not None:
261 | # Ignore entities that are views
262 | return False
263 |
264 | domain = entity.domain.lower()
265 | explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
266 | explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None)
267 |
268 | if entity.entity_id in self.entities and \
269 | CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]:
270 | explicit_hidden = \
271 | self.entities[entity.entity_id][CONF_ENTITY_HIDDEN]
272 |
273 | if explicit_expose is True or explicit_hidden is False:
274 | expose = True
275 | elif explicit_expose is False or explicit_hidden is True:
276 | expose = False
277 | else:
278 | expose = None
279 | get_deprecated(entity.attributes, ATTR_EMULATED_HUE_HIDDEN,
280 | ATTR_EMULATED_HUE, None)
281 | domain_exposed_by_default = \
282 | self.expose_by_default and domain in self.exposed_domains
283 |
284 | # Expose an entity if the entity's domain is exposed by default and
285 | # the configuration doesn't explicitly exclude it from being
286 | # exposed, or if the entity is explicitly exposed
287 | is_default_exposed = \
288 | domain_exposed_by_default and expose is not False
289 |
290 | return is_default_exposed or expose
291 |
292 |
293 | def _load_json(filename):
294 | """Wrapper, because we actually want to handle invalid json."""
295 | try:
296 | return load_json(filename)
297 | except HomeAssistantError:
298 | pass
299 | return {}
300 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/hue_api.py:
--------------------------------------------------------------------------------
1 | """Provides a Hue API to control Home Assistant."""
2 | import asyncio
3 | import logging
4 | import json
5 |
6 |
7 | from aiohttp import web
8 |
9 | from homeassistant import core
10 | from homeassistant.const import (
11 | ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
12 | SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF,
13 | HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ATTR_SUPPORTED_FEATURES,
14 | )
15 | from homeassistant.components.light import (
16 | ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS
17 | )
18 | from homeassistant.components.media_player import (
19 | ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET,
20 | )
21 | from homeassistant.components.fan import (
22 | ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW,
23 | SPEED_MEDIUM, SPEED_HIGH
24 | )
25 | from homeassistant.components.http import HomeAssistantView
26 | from homeassistant import util
27 |
28 | from .utility import *
29 |
30 | _LOGGER = logging.getLogger(__name__)
31 |
32 | HUE_API_STATE_ON = 'on'
33 | HUE_API_STATE_BRI = 'bri'
34 |
35 | class HueDingDongConfigView(HomeAssistantView):
36 | """Handle Support DingDong Smart SoundBox"""
37 |
38 | url = '/api/{username}'
39 | name = 'emulated_hue:Config:state'
40 | requires_auth = False
41 |
42 | def __init__(self,config):
43 | self.config = config
44 |
45 | @core.callback
46 | def get(self ,request,username):
47 | if username == "null":
48 | return self.json([{"error":{"type":"1","address":"null","description":"unauthorized user"}}])
49 | else:
50 | hass = request.app['hass']
51 | lights = {}
52 |
53 | for entity in hass.states.async_all():
54 |
55 | if self.config.is_entity_exposed(entity):
56 | state, brightness = get_entity_state(self.config, entity)
57 |
58 | number = self.config.entity_id_to_number(entity.entity_id)
59 | light = entity_to_json(self.config, entity, state, brightness)
60 | light["manufacturername"] = "philips"
61 | light["state"]["alert"] = "none"
62 | lights[number] = light
63 |
64 | json_response = {}
65 | json_response["lights"] = lights
66 | json_response["scenes"] = {}
67 | json_response["groups"] = {}
68 | json_response["schedules"] = {}
69 | json_response["sensors"] = {}
70 | json_response["rules"] = {}
71 |
72 | swupdate = {}
73 | swupdate["updatestate"] = 0
74 | swupdate["checkforupdate"] = False
75 | swupdate["devicetypes"] = {}
76 | swupdate["text"] = ""
77 | swupdate["notify"] = False
78 | swupdate["url"] = ""
79 |
80 | config = {}
81 | config["portalservices"] = False
82 | config["gateway"] = util.get_local_ip()
83 | config["mac"] = get_mac_address()
84 | config["swversion"] = "9999999999"
85 | config["apiversion"] = "1.19.0"
86 | config["linkbutton"] = True
87 | config["ipaddress"] = util.get_local_ip()
88 | config["proxyport"] = 0
89 | config["portalservices"] = False
90 | config["swupdate"] = swupdate
91 | config["netmask"] = self.config.netmask
92 | config["dhcp"] = True
93 | config["utc"] = get_utc_time()
94 | config["proxyaddress"] = "none"
95 | config["localtime"] = get_local_time()
96 | config["timezone"] = self.config.timezone
97 | config["zigbeechannel"] = "6"
98 | config["modelid"] = "HomeAssistant"
99 | config["bridgeid"] = get_bridgeid()
100 | config["factorynew"] = False
101 |
102 | whitelist = {}
103 | whitelist_dingdong = {}
104 | whitelist_dingdong["createdate"] = get_local_time()
105 | whitelist_dingdong["lastusedate"] = get_local_time()
106 | whitelist_dingdong["name"] = "dingdong#allwinner-tablet#{0}".format(request._state.get("ha_real_ip"))
107 |
108 | whitelist["ab2633c3ed104604afc5709d4d9cac9e"] = whitelist_dingdong
109 | config["whitelist"] = whitelist
110 | json_response["config"] = config
111 |
112 | return self.json(json_response)
113 |
114 |
115 |
116 | class HueUsernameView(HomeAssistantView):
117 | """Handle requests to create a username for the emulated hue bridge."""
118 |
119 | url = '/api'
120 | name = 'emulated_hue:api:create_username'
121 | extra_urls = ['/api/']
122 | requires_auth = False
123 |
124 | @asyncio.coroutine
125 | def post(self, request):
126 | """Handle a POST request."""
127 | try:
128 | data = yield from request.json()
129 | except ValueError:
130 | return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
131 |
132 | if 'devicetype' not in data:
133 | return self.json_message('devicetype not specified',
134 | HTTP_BAD_REQUEST)
135 | return self.json([{'success': {'username': '12345678901234567890'}}])
136 |
137 | class HueAllLightsStateView(HomeAssistantView):
138 | """Handle requests for getting and setting info about entities."""
139 |
140 | url = '/api/{username}/lights'
141 | name = 'emulated_hue:lights:state'
142 | requires_auth = False
143 |
144 | def __init__(self, config):
145 | """Initialize the instance of the view."""
146 | self.config = config
147 |
148 | @core.callback
149 | def get(self, request, username):
150 | """Process a request to get the list of available lights."""
151 | hass = request.app['hass']
152 | json_response = {}
153 |
154 | for entity in hass.states.async_all():
155 | if self.config.is_entity_exposed(entity):
156 | state, brightness = get_entity_state(self.config, entity)
157 |
158 | number = self.config.entity_id_to_number(entity.entity_id)
159 | json_response[number] = entity_to_json(
160 | self.config, entity, state, brightness)
161 |
162 | return self.json(json_response)
163 |
164 |
165 | class HueOneLightStateView(HomeAssistantView):
166 | """Handle requests for getting and setting info about entities."""
167 |
168 | url = '/api/{username}/lights/{entity_id}'
169 | name = 'emulated_hue:light:state'
170 | requires_auth = False
171 |
172 | def __init__(self, config):
173 | """Initialize the instance of the view."""
174 | self.config = config
175 |
176 | @core.callback
177 | def get(self, request, username, entity_id):
178 | """Process a request to get the state of an individual light."""
179 | hass = request.app['hass']
180 | entity_id = self.config.number_to_entity_id(entity_id)
181 | entity = hass.states.get(entity_id)
182 |
183 | if entity is None:
184 | _LOGGER.error('Entity not found: %s', entity_id)
185 | return web.Response(text="Entity not found", status=404)
186 |
187 | if not self.config.is_entity_exposed(entity):
188 | _LOGGER.error('Entity not exposed: %s', entity_id)
189 | return web.Response(text="Entity not exposed", status=404)
190 |
191 | state, brightness = get_entity_state(self.config, entity)
192 |
193 | json_response = entity_to_json(self.config, entity, state, brightness)
194 |
195 | return self.json(json_response)
196 |
197 |
198 | class HueOneLightChangeView(HomeAssistantView):
199 | """Handle requests for getting and setting info about entities."""
200 |
201 | url = '/api/{username}/lights/{entity_number}/state'
202 | name = 'emulated_hue:light:state'
203 | requires_auth = False
204 |
205 | def __init__(self, config):
206 | """Initialize the instance of the view."""
207 | self.config = config
208 |
209 | @asyncio.coroutine
210 | def put(self, request, username, entity_number):
211 | """Process a request to set the state of an individual light."""
212 | config = self.config
213 | hass = request.app['hass']
214 | entity_id = config.number_to_entity_id(entity_number)
215 |
216 | if entity_id is None:
217 | _LOGGER.error('Unknown entity number: %s', entity_number)
218 | return self.json_message('Entity not found', HTTP_NOT_FOUND)
219 |
220 | entity = hass.states.get(entity_id)
221 |
222 | if entity is None:
223 | _LOGGER.error('Entity not found: %s', entity_id)
224 | return self.json_message('Entity not found', HTTP_NOT_FOUND)
225 |
226 | if not config.is_entity_exposed(entity):
227 | _LOGGER.error('Entity not exposed: %s', entity_id)
228 | return web.Response(text="Entity not exposed", status=404)
229 |
230 | try:
231 | request_json = yield from request.json()
232 | except ValueError:
233 | _LOGGER.error('Received invalid json')
234 | return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
235 |
236 | # Parse the request into requested "on" status and brightness
237 | parsed = parse_hue_api_put_light_body(request_json, entity)
238 |
239 | if parsed is None:
240 | _LOGGER.error('Unable to parse data: %s', request_json)
241 | return web.Response(text="Bad request", status=400)
242 |
243 | result, brightness = parsed
244 |
245 | # Choose general HA domain
246 | domain = core.DOMAIN
247 |
248 | # Entity needs separate call to turn on
249 | turn_on_needed = False
250 |
251 | # Convert the resulting "on" status into the service we need to call
252 | service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
253 |
254 | # Construct what we need to send to the service
255 | data = {ATTR_ENTITY_ID: entity_id}
256 |
257 | # Make sure the entity actually supports brightness
258 | entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
259 |
260 | if entity.domain == "light":
261 | if entity_features & SUPPORT_BRIGHTNESS:
262 | if brightness is not None:
263 | data[ATTR_BRIGHTNESS] = brightness
264 |
265 | # If the requested entity is a script add some variables
266 | elif entity.domain == "script":
267 | data['variables'] = {
268 | 'requested_state': STATE_ON if result else STATE_OFF
269 | }
270 |
271 | if brightness is not None:
272 | data['variables']['requested_level'] = brightness
273 |
274 | # If the requested entity is a media player, convert to volume
275 | elif entity.domain == "media_player":
276 | if entity_features & SUPPORT_VOLUME_SET:
277 | if brightness is not None:
278 | turn_on_needed = True
279 | domain = entity.domain
280 | service = SERVICE_VOLUME_SET
281 | # Convert 0-100 to 0.0-1.0
282 | data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0
283 |
284 | # If the requested entity is a cover, convert to open_cover/close_cover
285 | elif entity.domain == "cover":
286 | domain = entity.domain
287 | if service == SERVICE_TURN_ON:
288 | service = SERVICE_OPEN_COVER
289 | else:
290 | service = SERVICE_CLOSE_COVER
291 |
292 | # If the requested entity is a fan, convert to speed
293 | elif entity.domain == "fan":
294 | if entity_features & SUPPORT_SET_SPEED:
295 | if brightness is not None:
296 | domain = entity.domain
297 | # Convert 0-100 to a fan speed
298 | if brightness == 0:
299 | data[ATTR_SPEED] = SPEED_OFF
300 | elif 0 < brightness <= 33.3:
301 | data[ATTR_SPEED] = SPEED_LOW
302 | elif 33.3 < brightness <= 66.6:
303 | data[ATTR_SPEED] = SPEED_MEDIUM
304 | elif 66.6 < brightness <= 100:
305 | data[ATTR_SPEED] = SPEED_HIGH
306 |
307 | if entity.domain in config.off_maps_to_on_domains:
308 | # Map the off command to on
309 | service = SERVICE_TURN_ON
310 |
311 | # Caching is required because things like scripts and scenes won't
312 | # report as "off" to Alexa if an "off" command is received, because
313 | # they'll map to "on". Thus, instead of reporting its actual
314 | # status, we report what Alexa will want to see, which is the same
315 | # as the actual requested command.
316 | config.cached_states[entity_id] = (result, brightness)
317 |
318 | # Separate call to turn on needed
319 | if turn_on_needed:
320 | hass.async_add_job(hass.services.async_call(
321 | core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
322 | blocking=True))
323 |
324 | hass.async_add_job(hass.services.async_call(
325 | domain, service, data, blocking=True))
326 |
327 | json_response = \
328 | [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
329 |
330 | if brightness is not None:
331 | json_response.append(create_hue_success_response(
332 | entity_id, HUE_API_STATE_BRI, brightness))
333 |
334 | return self.json(json_response)
335 |
336 |
337 | def parse_hue_api_put_light_body(request_json, entity):
338 | """Parse the body of a request to change the state of a light."""
339 | if HUE_API_STATE_ON in request_json:
340 | if not isinstance(request_json[HUE_API_STATE_ON], bool):
341 | return None
342 |
343 | if request_json['on']:
344 | # Echo requested device be turned on
345 | brightness = None
346 | report_brightness = False
347 | result = True
348 | else:
349 | # Echo requested device be turned off
350 | brightness = None
351 | report_brightness = False
352 | result = False
353 |
354 | if HUE_API_STATE_BRI in request_json:
355 | try:
356 | # Clamp brightness from 0 to 255
357 | brightness = \
358 | max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
359 | except ValueError:
360 | return None
361 |
362 | # Make sure the entity actually supports brightness
363 | entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
364 |
365 | if entity.domain == "light":
366 | if entity_features & SUPPORT_BRIGHTNESS:
367 | report_brightness = True
368 | result = (brightness > 0)
369 |
370 | elif entity.domain == "scene":
371 | brightness = None
372 | report_brightness = False
373 | result = True
374 |
375 | elif (entity.domain == "script" or
376 | entity.domain == "media_player" or
377 | entity.domain == "fan"):
378 | # Convert 0-255 to 0-100
379 | level = brightness / 255 * 100
380 | brightness = round(level)
381 | report_brightness = True
382 | result = True
383 |
384 | return (result, brightness) if report_brightness else (result, None)
385 |
386 |
387 | def get_entity_state(config, entity):
388 | """Retrieve and convert state and brightness values for an entity."""
389 | cached_state = config.cached_states.get(entity.entity_id, None)
390 |
391 | if cached_state is None:
392 | final_state = entity.state != STATE_OFF
393 | final_brightness = entity.attributes.get(
394 | ATTR_BRIGHTNESS, 255 if final_state else 0)
395 |
396 | # Make sure the entity actually supports brightness
397 | entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
398 |
399 | if entity.domain == "light":
400 | if entity_features & SUPPORT_BRIGHTNESS:
401 | pass
402 |
403 | elif entity.domain == "media_player":
404 | level = entity.attributes.get(
405 | ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
406 | # Convert 0.0-1.0 to 0-255
407 | final_brightness = round(min(1.0, level) * 255)
408 | elif entity.domain == "fan":
409 | speed = entity.attributes.get(ATTR_SPEED, 0)
410 | # Convert 0.0-1.0 to 0-255
411 | final_brightness = 0
412 | if speed == SPEED_LOW:
413 | final_brightness = 85
414 | elif speed == SPEED_MEDIUM:
415 | final_brightness = 170
416 | elif speed == SPEED_HIGH:
417 | final_brightness = 255
418 | else:
419 | final_state, final_brightness = cached_state
420 | # Make sure brightness is valid
421 | if final_brightness is None:
422 | final_brightness = 255 if final_state else 0
423 |
424 | return (final_state, final_brightness)
425 |
426 |
427 | def entity_to_json(config, entity, is_on=None, brightness=None):
428 | """Convert an entity to its Hue bridge JSON representation."""
429 | return {
430 | 'state':
431 | {
432 | HUE_API_STATE_ON: is_on,
433 | HUE_API_STATE_BRI: brightness,
434 | 'reachable': True
435 | },
436 | 'type': 'Dimmable light',
437 | 'name': config.get_entity_name(entity),
438 | 'modelid': 'HASS123',
439 | 'uniqueid': entity.entity_id,
440 | 'swversion': '123'
441 | }
442 |
443 |
444 | def create_hue_success_response(entity_id, attr, value):
445 | """Create a success response for an attribute set on a light."""
446 | success_key = '/lights/{}/state/{}'.format(entity_id, attr)
447 | return {'success': {success_key: value}}
448 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/upnp.py:
--------------------------------------------------------------------------------
1 | """Provides a UPNP discovery method that mimics Hue hubs."""
2 | import threading
3 | import socket
4 | import logging
5 | import select
6 |
7 | from aiohttp import web
8 |
9 | from homeassistant import core
10 | from homeassistant.components.http import HomeAssistantView
11 | from .utility import *
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 |
15 |
16 | class DescriptionXmlView(HomeAssistantView):
17 | """Handles requests for the description.xml file."""
18 |
19 | url = '/description.xml'
20 | name = 'description:xml'
21 | requires_auth = False
22 |
23 | def __init__(self, config):
24 | """Initialize the instance of the view."""
25 | self.config = config
26 |
27 | @core.callback
28 | def get(self, request):
29 | """Handle a GET request."""
30 | xml_template = """
31 |
32 |
33 | 1
34 | 0
35 |
36 | http://{0}:{1}/
37 |
38 | urn:schemas-upnp-org:device:Basic:1
39 | HASS Bridge ({0})
40 | Royal Philips Electronics
41 | http://www.philips.com
42 | Philips hue Personal Wireless Lighting
43 | Philips hue bridge 2015
44 | BSB002
45 | http://www.meethue.com
46 | {2}
47 | uuid:01234567-89ab-cdef-0123-{2}
48 |
49 |
50 | """
51 |
52 | resp_text = xml_template.format(
53 | self.config.advertise_ip, self.config.advertise_port,get_mac_address_noformat())
54 |
55 | return web.Response(text=resp_text, content_type='text/xml')
56 |
57 |
58 | class UPNPResponderThread(threading.Thread):
59 | """Handle responding to UPNP/SSDP discovery requests."""
60 |
61 | _interrupted = False
62 |
63 | def __init__(self, host_ip_addr, listen_port, upnp_bind_multicast,
64 | advertise_ip, advertise_port):
65 | """Initialize the class."""
66 | threading.Thread.__init__(self)
67 |
68 | self.host_ip_addr = host_ip_addr
69 | self.listen_port = listen_port
70 | self.upnp_bind_multicast = upnp_bind_multicast
71 |
72 | # Note that the double newline at the end of
73 | # this string is required per the SSDP spec
74 | resp_template = """HTTP/1.1 200 OK
75 | CACHE-CONTROL: max-age=60
76 | EXT:
77 | LOCATION: http://{0}:{1}/description.xml
78 | SERVER: HomeAssistant/0.6.5, UPnP/1.0, IpBridge/0.1
79 | hue-bridgeid: {2}
80 | ST: upnp:rootdevice
81 | USN: uuid:01234567-89ab-cdef-0123-{3}::upnp:rootdevice
82 |
83 | """
84 |
85 | self.upnp_response = resp_template.format(
86 | advertise_ip, advertise_port, get_bridgeid(),get_mac_address_noformat()).replace("\n", "\r\n") \
87 | .encode('utf-8')
88 |
89 | def run(self):
90 | """Run the server."""
91 | # Listen for UDP port 1900 packets sent to SSDP multicast address
92 | ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
93 | ssdp_socket.setblocking(False)
94 |
95 | # Required for receiving multicast
96 | ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
97 |
98 | ssdp_socket.setsockopt(
99 | socket.SOL_IP,
100 | socket.IP_MULTICAST_IF,
101 | socket.inet_aton(self.host_ip_addr))
102 |
103 | ssdp_socket.setsockopt(
104 | socket.SOL_IP,
105 | socket.IP_ADD_MEMBERSHIP,
106 | socket.inet_aton("239.255.255.250") +
107 | socket.inet_aton(self.host_ip_addr))
108 |
109 | if self.upnp_bind_multicast:
110 | ssdp_socket.bind(("", 1900))
111 | else:
112 | ssdp_socket.bind((self.host_ip_addr, 1900))
113 |
114 | while True:
115 | if self._interrupted:
116 | clean_socket_close(ssdp_socket)
117 | return
118 |
119 | try:
120 | read, _, _ = select.select(
121 | [ssdp_socket], [],
122 | [ssdp_socket], 2)
123 |
124 | if ssdp_socket in read:
125 | data, addr = ssdp_socket.recvfrom(1024)
126 | else:
127 | # most likely the timeout, so check for interrupt
128 | continue
129 | except socket.error as ex:
130 | if self._interrupted:
131 | clean_socket_close(ssdp_socket)
132 | return
133 |
134 | _LOGGER.error("UPNP Responder socket exception occurred: %s",
135 | ex.__str__)
136 | # without the following continue, a second exception occurs
137 | # because the data object has not been initialized
138 | continue
139 |
140 | if "M-SEARCH" in data.decode('utf-8', errors='ignore'):
141 | # SSDP M-SEARCH method received, respond to it with our info
142 | resp_socket = socket.socket(
143 | socket.AF_INET, socket.SOCK_DGRAM)
144 |
145 | resp_socket.sendto(self.upnp_response, addr)
146 | resp_socket.close()
147 |
148 | def stop(self):
149 | """Stop the server."""
150 | # Request for server
151 | self._interrupted = True
152 | self.join()
153 |
154 |
155 | def clean_socket_close(sock):
156 | """Close a socket connection and logs its closure."""
157 | _LOGGER.info("UPNP responder shutting down.")
158 |
159 | sock.close()
160 |
--------------------------------------------------------------------------------
/service/emulated_hue_charley/utility.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import time
3 | import datetime
4 |
5 | def get_mac_address():
6 | mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
7 | return ":".join([mac[e:e + 2] for e in range(0, 11, 2)])
8 |
9 | def get_mac_address_noformat():
10 | return uuid.UUID(int=uuid.getnode()).hex[-12:]
11 |
12 | def get_bridgeid():
13 | mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
14 | return mac[:6] + "fffe" + mac[6:]
15 |
16 |
17 | def get_local_time():
18 | return time.strftime('%Y-%m-%dt%H:%M:%S', time.localtime(time.time()))
19 |
20 |
21 | def get_utc_time():
22 | return datetime.datetime.utcnow().strftime("%Y-%m-%dt%H:%M:%S")
23 |
--------------------------------------------------------------------------------
/switch/WuKong.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo platform that has two fake switches.
3 |
4 | For more details about this platform, please refer to the documentation
5 | https://home-assistant.io/components/demo/
6 | """
7 | from homeassistant.components.switch import SwitchDevice
8 | from homeassistant.const import DEVICE_DEFAULT_NAME
9 |
10 | from homeassistant.components.switch import PLATFORM_SCHEMA
11 | import homeassistant.helpers.config_validation as cv
12 | from homeassistant.const import CONF_HOST
13 | import voluptuous as vol
14 |
15 | import time
16 | import logging
17 | import requests
18 | import socket,base64
19 |
20 | DOMAIN = 'Wu_Kong_Control'
21 | CONF_MODE = 'mode'
22 | CONF_PREFIX = 'PrefixName'
23 | _Log=logging.getLogger(__name__)
24 |
25 | PACKAGES = {
26 | 'connect' :'AAC4EwEzAp4AAAgmAAABNAAAAAAAAABEeyJuYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjaGFubmVsIjoiaUFwcFN0b3JlIiwiZGV2IjoiaU9TIn0=',
27 | 'tv_ctl_up' :'AAC4EwEzAp4AAAghAAAAEwAAAAAAAAAA',
28 | 'tv_ctl_down' :'AAC4EwEzAp4AAAghAAAAFAAAAAAAAAAA',
29 | 'tv_ctl_left' :'AAC4EwEzAp4AAAghAAAAFQAAAAAAAAAA',
30 | 'tv_ctl_right' :'AAC4EwEzAp4AAAghAAAAFgAAAAAAAAAA',
31 | 'tv_ctl_home' :'AAC4EwEzAp4AAAghAAAAAwAAAAAAAAAA',
32 | 'tv_ctl_ok' :'AAC4EwEzAp4AAAghAAAAFwAAAAAAAAAA',
33 | 'tv_ctl_back' :'AAC4EwEzAp4AAAghAAAABAAAAAAAAAAA',
34 | 'tv_ctl_volup' :'AAC4EwEzAp4AAAghAAAAGAAAAAAAAAAA',
35 | 'tv_ctl_voldown':'AAC4EwEzAp4AAAghAAAAGQAAAAAAAAAA',
36 | 'tv_ctl_power' :'AAC4EwEzAp4AAAghAAAAGgAAAAAAAAAA',
37 | 'tv_ctl_menu' :'AAC4EwEzAp4AAAghAAAAUgAAAAAAAAAA',
38 | 'tv_ctl_1' :'AAC4EwEzAp4AAAghAAAACAAAAAAAAAAA',
39 | 'tv_ctl_2' :'AAC4EwEzAp4AAAghAAAACQAAAAAAAAAA',
40 | 'tv_ctl_3' :'AAC4EwEzAp4AAAghAAAACgAAAAAAAAAA',
41 | 'tv_ctl_4' :'AAC4EwEzAp4AAAghAAAACwAAAAAAAAAA',
42 | 'tv_ctl_5' :'AAC4EwEzAp4AAAghAAAADAAAAAAAAAAA',
43 | 'tv_ctl_6' :'AAC4EwEzAp4AAAghAAAADQAAAAAAAAAA',
44 | 'tv_ctl_7' :'AAC4EwEzAp4AAAghAAAADgAAAAAAAAAA',
45 | 'tv_ctl_8' :'AAC4EwEzAp4AAAghAAAADwAAAAAAAAAA',
46 | 'tv_ctl_9' :'AAC4EwEzAp4AAAghAAAAEAAAAAAAAAAA',
47 | 'tv_ctl_0' :'AAC4EwEzAp4AAAghAAAABwAAAAAAAAAA',
48 |
49 | }
50 |
51 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
52 | vol.Required(CONF_HOST,default=None):cv.string,
53 | vol.Required(CONF_MODE,default='http'):cv.string,
54 | vol.Required(CONF_PREFIX,default=None):cv.string,
55 | })
56 |
57 | # pylint: disable=unused-argument
58 | def setup_platform(hass, config, add_devices_callback, discovery_info=None):
59 | """Setup the demo switches."""
60 | host = config.get(CONF_HOST)
61 | mode = config.get(CONF_MODE)
62 | prefix = config.get(CONF_PREFIX)
63 | if host == None:
64 | _Log.error('pls enter host ip address!')
65 | return False
66 |
67 | # def SendControlCommand(call):
68 | # code = call.data.get('code')
69 | # _Log.info(code)
70 | # hass.states.set('WuKong.Send_Control_Command', code)
71 |
72 | service = WuKongService(hass,host,mode)
73 |
74 |
75 | hass.services.register(DOMAIN, 'Send_Control_Command', service.SendControlCommand)
76 | hass.services.register(DOMAIN, 'Send_Open_Command', service.SendOpenCommand)
77 | hass.services.register(DOMAIN, 'Send_Install_Command', service.SendInstallCommand)
78 | hass.services.register(DOMAIN, 'Send_Clean_Command', service.SendCleanCommand)
79 | hass.services.register(DOMAIN, 'Send_Command_Queue', service.SendCommandQueue)
80 | hass.services.register(DOMAIN, 'Send_Connect_Command', service.SendConnectCommand)
81 |
82 |
83 | add_devices_callback([
84 | WuKongSwitch(hass, host, prefix, 'tv_ctl_up', False, 'mdi:arrow-up-bold-circle', True, mode,19),
85 | WuKongSwitch(hass, host, prefix, 'tv_ctl_down', False, 'mdi:arrow-down-bold-circle', True, mode,20),
86 | WuKongSwitch(hass, host, prefix, 'tv_ctl_left', False, 'mdi:arrow-left-bold-circle', True, mode,21),
87 | WuKongSwitch(hass, host, prefix, 'tv_ctl_right', False, 'mdi:arrow-right-bold-circle', True, mode,22),
88 | WuKongSwitch(hass, host, prefix, 'tv_ctl_home', False, 'mdi:home', True, mode,3),
89 | WuKongSwitch(hass, host, prefix, 'tv_ctl_back', False, 'mdi:backup-restore', True, mode,4),
90 | WuKongSwitch(hass, host, prefix, 'tv_ctl_ok', False, 'mdi:adjust', True, mode,23),
91 | WuKongSwitch(hass, host, prefix, 'tv_ctl_volup', False, 'mdi:volume-high', True, mode,24),
92 | WuKongSwitch(hass, host, prefix, 'tv_ctl_voldown', False, 'mdi:volume-medium', True, mode,25),
93 | WuKongSwitch(hass, host, prefix, 'tv_ctl_power', False, 'mdi:power', True, mode, 26),
94 | WuKongSwitch(hass, host, prefix, 'tv_ctl_menu', False, 'mdi:menu', True, mode, 82),
95 | WuKongSwitch(hass, host, prefix, 'tv_ctl_1', False, 'mdi:numeric-1-box', True, mode,8),
96 | WuKongSwitch(hass, host, prefix, 'tv_ctl_2', False, 'mdi:numeric-2-box', True, mode,9),
97 | WuKongSwitch(hass, host, prefix, 'tv_ctl_3', False, 'mdi:numeric-3-box', True, mode,10),
98 | WuKongSwitch(hass, host, prefix, 'tv_ctl_4', False, 'mdi:numeric-4-box', True, mode,11),
99 | WuKongSwitch(hass, host, prefix, 'tv_ctl_5', False, 'mdi:numeric-5-box', True, mode,12),
100 | WuKongSwitch(hass, host, prefix, 'tv_ctl_6', False, 'mdi:numeric-6-box', True, mode,13),
101 | WuKongSwitch(hass, host, prefix, 'tv_ctl_7', False, 'mdi:numeric-7-box', True, mode,14),
102 | WuKongSwitch(hass, host, prefix, 'tv_ctl_8', False, 'mdi:numeric-8-box', True, mode,15),
103 | WuKongSwitch(hass, host, prefix, 'tv_ctl_9', False, 'mdi:numeric-9-box', True, mode,16),
104 | WuKongSwitch(hass, host, prefix, 'tv_ctl_0', False, 'mdi:numeric-0-box', True, mode,7),
105 |
106 | WuKongSwitch(hass, host, prefix, 'tv_ctl_clean', False, 'mdi:notification-clear-all', True, mode,999),
107 | ])
108 |
109 |
110 | class WuKongSwitch(SwitchDevice):
111 | """Representation of a demo switch."""
112 |
113 | def __init__(self,hass,host,prefix, name, state, icon, assumed,mode,code):
114 | """Initialize the WuKongSwitch switch."""
115 | self._name = name or DEVICE_DEFAULT_NAME
116 | self._state = state
117 | self._icon = icon
118 | self._assumed = assumed
119 | self._code = code
120 | self._hass = hass
121 | self._host = host
122 | self._mode = mode
123 | self._prefix = prefix
124 |
125 | @property
126 | def should_poll(self):
127 |
128 | return False
129 |
130 | @property
131 | def name(self):
132 | """Return the name of the device if any."""
133 | if self._prefix != None:
134 | return self._prefix + '_' + self._name
135 | return self._name
136 |
137 | @property
138 | def icon(self):
139 | """Return the icon to use for device if any."""
140 | return self._icon
141 |
142 | @property
143 | def assumed_state(self):
144 | """Return if the state is based on assumptions."""
145 | return self._assumed
146 |
147 | @property
148 | def is_on(self):
149 | """Return true if switch is on."""
150 | return self._state
151 |
152 | def turn_on(self, **kwargs):
153 | """Turn the switch on."""
154 | self._state = self.sendCode()
155 | self.schedule_update_ha_state()
156 |
157 | def turn_off(self, **kwargs):
158 | """Turn the device off."""
159 | self._state = self.sendCode()
160 | self.schedule_update_ha_state()
161 |
162 | def sendCode(self):
163 | s = WuKongService(self._hass, self._host,self._mode)
164 | if self._code == 999:
165 | return s.SendCleanCommand(None)
166 | if self._mode == 'UDP':
167 | return s.sendUDPPackage(PACKAGES[self._name])
168 | else:
169 |
170 | return s.SendControlCommand(None,self._code)
171 |
172 | class WuKongService(object):
173 |
174 | def __init__(self,hass,host,mode):
175 | self._host = host
176 | self._hass = hass
177 | self._mode = mode
178 |
179 | def SendControlCommand(self,call,selfcode=None):
180 |
181 | if self._mode == 'UDP':
182 | code = call.data.get('code')
183 | if code == None:
184 | _Log.error('Command Code is nil!')
185 | return
186 | if code in PACKAGES.keys():
187 | package = PACKAGES[code]
188 | return self.sendUDPPackage(package)
189 | else:
190 | _Log.error('Code Error!')
191 | return
192 |
193 |
194 | code = ''
195 | if selfcode == None:
196 | code = call.data.get('code')
197 | if code == None:
198 | _Log.error('Command Code is nil!')
199 | return
200 | else:
201 | code = selfcode
202 | url = 'http://{host}:8899/send?key={code}'.format(host=self._host, code=code)
203 | return self.sendHttpRequest(url)
204 |
205 | def SendOpenCommand(self,call,selfappid=None):
206 | appid = ''
207 | if selfappid == None:
208 | appid = call.data.get('appid')
209 | if appid == None:
210 | _Log.error('Appid is nil!')
211 | return
212 | else:
213 | appid = selfappid
214 | url = 'http://{host}:12104/?action=open&pkg={appid}'.format(host=self._host, appid=appid)
215 | return self.sendHttpRequest(url)
216 |
217 | def SendInstallCommand(self,call,selfappUrl=None):
218 | appUrl = ''
219 | if selfappUrl ==None:
220 | appUrl = call.data.get('appUrl')
221 | if appUrl == None:
222 | _Log.error('appUrl is nil!')
223 | return
224 | else:
225 | appUrl = selfappUrl
226 | url = 'http://{host}:12104/?action=install&url={appUrl}'.format(host=self._host,appUrl=appUrl)
227 | _Log.error('url:%s' % url)
228 | return self.sendHttpRequest(url)
229 |
230 | def SendCleanCommand(self,call):
231 | url = 'http://{host}:12104/?action=clean'.format(host=self._host)
232 | return self.sendHttpRequest(url)
233 |
234 | def SendConnectCommand(self,call):
235 | host = call.data.get('host')
236 | if host == None:
237 | _Log.error('host is nil!')
238 | return
239 | package=PACKAGES["connect"]
240 | self.sendUDPPackage(package,host)
241 |
242 | def SendCommandQueue(self,call):
243 | cmdQueue = call.data.get('cmdQueue')
244 | for cmd in cmdQueue:
245 |
246 | code = cmd.get('code')
247 | delay = cmd.get('delay')
248 |
249 | if code == None:
250 | return
251 | if delay == None:
252 | delay = 1000
253 | if self._mode == 'UDP':
254 | if code in PACKAGES.keys():
255 | package = PACKAGES[code]
256 | self.sendUDPPackage(package)
257 | time.sleep(delay / 1000)
258 | else:
259 |
260 | _Log.error('Code Error! code:{cd}'.format(cd=code))
261 | return
262 | else:
263 | self.SendControlCommand(None,code)
264 | time.sleep(delay / 1000)
265 |
266 | def sendHttpRequest(self,url):
267 | url +'&t={time}'.format(time=int(time.time()))
268 | try:
269 | resp = requests.get(url)
270 | if resp.status_code and resp.text == 'success':
271 | return False
272 | return True
273 | except Exception as e:
274 | _Log.error("requst url:{url} Error:{err}".format(url=url,err=e))
275 | return False
276 |
277 | def sendUDPPackage(self,base64Data,host=None):
278 | addr = None
279 | if host != None:
280 | addr = (host, 12305)
281 | else:
282 | addr = (self._host, 12305)
283 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
284 | bytePackge = base64.b64decode(base64Data)
285 | ret = True
286 | try:
287 | s.sendto(bytePackge,addr)
288 | ret = False
289 | except Exception as e:
290 | _Log.error("requst UDP Error:{err}, Package:{pkg}".format(err=e,pkg=base64Data))
291 | s.close()
292 |
293 | return ret
--------------------------------------------------------------------------------
/tts/__pycache__/baidu.cpython-36.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/1cf9d45a7a32b15f40468a2fbbe34ef17efd9991/tts/__pycache__/baidu.cpython-36.pyc
--------------------------------------------------------------------------------
/tts/baidu.py:
--------------------------------------------------------------------------------
1 | """
2 | Baidu TTS Developer by Charley
3 | """
4 | import voluptuous as vol
5 | from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG
6 | import homeassistant.helpers.config_validation as cv
7 |
8 | import requests
9 | import logging
10 | import json
11 |
12 | _Log=logging.getLogger(__name__)
13 |
14 | # 默认语言
15 | DEFAULT_LANG = 'zh'
16 |
17 | # 支持的语言
18 | SUPPORT_LANGUAGES = [
19 | 'zh',
20 | ]
21 |
22 | CONF_APIKEY = 'api_key'
23 | CONF_SECRETKEY = 'secret_key'
24 | CONF_SPEED = 'speed'
25 | CONF_PITCH = 'pitch'
26 | CONF_VOLUME = 'volume'
27 | PERSON = 'person'
28 |
29 |
30 | TOKEN_INTERFACE = 'https://openapi.baidu.com/oauth/2.0/token'
31 | TEXT2AUDIO_INTERFACE = 'http://tsn.baidu.com/text2audio'
32 |
33 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
34 | vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
35 | vol.Optional(CONF_APIKEY): cv.string,
36 | vol.Optional(CONF_SECRETKEY):cv.string,
37 | vol.Optional(CONF_SPEED,default='5'): cv.string,
38 | vol.Optional(CONF_PITCH,default='5'): cv.string,
39 | vol.Optional(CONF_VOLUME,default='5'): cv.string,
40 | vol.Optional(PERSON,default='0'): cv.string,
41 | })
42 |
43 | def get_engine(hass, config):
44 | lang = config.get(CONF_LANG)
45 | apiKey = config.get(CONF_APIKEY)
46 | secretKey = config.get(CONF_SECRETKEY)
47 | speed = config.get(CONF_SPEED)
48 | pitch = config.get(CONF_PITCH)
49 | volume = config.get(CONF_VOLUME)
50 | person = config.get(PERSON)
51 |
52 | if apiKey == None:
53 | _Log.error('Api Key is nil')
54 | return False
55 | if secretKey == None:
56 | _Log.error('secretKey is nil')
57 | return False
58 |
59 | return BaiduTTS(lang,apiKey,secretKey,speed,pitch,volume,person)
60 |
61 | class BaiduTTS (Provider):
62 |
63 | def __init__(self,lang,apiKey,secretKey,speed,pitch,volume,person):
64 | self._lang = lang
65 | self._apiKey = apiKey
66 | self._secretKey = secretKey
67 | self._speed = speed
68 | self._pitch = pitch
69 | self._volume = volume
70 | self._person = person
71 | token = self.getToken()
72 | _Log.info("token =====>" + token)
73 | self._Token = token
74 |
75 | def getToken(self):
76 | resp = requests.get(TOKEN_INTERFACE,params={'grant_type': 'client_credentials','client_id':self._apiKey,'client_secret':self._secretKey})
77 | if resp.status_code != 200:
78 | _Log.error('Get ToKen Http Error status_code:%s' % resp.status_code)
79 | return None
80 | resp.encoding = 'utf-8'
81 | # toKenjsonStr = resp.text
82 | tokenJson = resp.json()
83 |
84 | if not 'access_token' in tokenJson:
85 | _Log.error('Get ToKen Json Error!')
86 | return None
87 | return tokenJson['access_token']
88 |
89 | @property
90 | def default_language(self):
91 | """Default language."""
92 | return self._lang
93 |
94 | @property
95 | def supported_languages(self):
96 | """List of supported languages."""
97 | return SUPPORT_LANGUAGES
98 |
99 | def get_tts_audio(self, message, language, options=None):
100 | if self._Token == None:
101 | self._Token = self.getToken()
102 |
103 | if self._Token == None:
104 | _Log.error('get_tts_audio Self.ToKen is nil')
105 | return
106 |
107 | resp = requests.get(TEXT2AUDIO_INTERFACE,params={'tex':message,'lan':language,'tok':self._Token,'ctp':'1','cuid':'HomeAssistant','spd':self._speed,'pit':self._pitch,'vol':self._volume,'per':self._person})
108 |
109 | if resp.status_code == 500:
110 | _Log.error('Text2Audio Error:500 Not Support.')
111 | return
112 | if resp.status_code == 501:
113 | _Log.error('Text2Audio Error:501 Params Error')
114 | return
115 | if resp.status_code == 502:
116 | _Log.error('Text2Audio Error:502 TokenVerificationError.')
117 | _Log.Info('Now Get Token!')
118 | self._Token = self.getToken()
119 | self.get_tts_audio(message,language,options)
120 | return
121 | if resp.status_code == 503:
122 | _Log.error('Text2Audio Error:503 Composite Error.')
123 | return
124 |
125 | if resp.status_code != 200:
126 | _Log.error('get_tts_audio Http Error status_code:%s' % resp.status_code)
127 | return
128 |
129 | data = resp.content
130 | return ('mp3',data)
--------------------------------------------------------------------------------