├── .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 | ![Donation](https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/master/Images/Donation.png) 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 | ![WeatherChina](https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/master/Images/WeatherChina.png) 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 | ![Donation](https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/master/Images/Donation.png) 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 | ![WeatherChina](https://raw.githubusercontent.com/charleyzhu/HomeAssistant_Components/master/Images/WeatherChina.png) 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 | 11 | -------------------------------------------------------------------------------- /service/emulated_hue_charley/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 | 88 | 89 | 90 | 91 | 92 | true 93 | DEFINITION_ORDER 94 | 95 | 96 | 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 |