├── BaiduTTS(legecy) ├── README.md └── baidu.py ├── LICENSE ├── README.md ├── aliyun_stock ├── README.md ├── __init__.py ├── manifest.json └── sensor.py ├── environment_variables ├── README.md ├── __init__.py └── manifest.json ├── gaode_travel_time ├── README.md ├── __init__.py ├── manifest.json └── sensor.py ├── introduction_hachina ├── __init__.py └── manifest.json ├── juhe_joke ├── README.md ├── __init__.py ├── manifest.json └── sensor.py ├── juhe_laohuangli ├── README.md ├── __init__.py ├── laohuangli.png ├── manifest.json └── sensor.py ├── juhe_stock ├── README.md ├── __init__.py ├── manifest.json └── sensor.py ├── program_train ├── README.md ├── hachina1.py ├── hachina2.py ├── hachina3.py ├── hachina4.py ├── hachina5.py ├── hachina6.py ├── hachina7.py ├── hachina8.py └── hachina9.py ├── pulseaudio ├── README.md ├── __init__.py ├── config_flow.py ├── const.py ├── ffmpeg2pa.py ├── manifest.json ├── media_player.py ├── strings.json └── translations │ └── en.json ├── scrape2 ├── README.md ├── __init__.py ├── manifest.json └── sensor.py └── tunnel2local ├── README.md ├── __init__.py ├── manifest.json └── server_diy.md /BaiduTTS(legecy)/README.md: -------------------------------------------------------------------------------- 1 | ***此组件已经在HomeAssistant0.59之后的版本中正式包含了。*** 2 | 3 | `baidu` tts平台使用[百度tts云服务](https://cloud.baidu.com/product/speech/tts)将文字转换成语音。 4 | 5 | 将以下内容放置在`configuration.yaml`文件中: 6 | ```yaml 7 | # configuration.yaml样例 8 | tts: 9 | - platform: baidu 10 | app_id: YOUR_APPID 11 | api_key: YOUR_APIKEY 12 | secret_key: YOUR_SECRETKEY 13 | person: 4 14 | ``` 15 | 16 | 可配置项: 17 | 18 | - **app_id** (*必须项*): 在百度云平台上登记的AppID。 19 | - **api_key** (*必须项*): 百度云平台上的Apikey。 20 | - **secret_key** (*必须项*): 百度云平台上的Secretkey。 21 | - **speed** (*可选项*): 语音速度,从0到9,缺省值为5。 22 | - **pitch** (*可选项*): 语调,从0到9,缺省值为5。 23 | - **volume** (*可选项*): 音量,从0到15,缺省值为5。 24 | - **person** (*可选项*): 可选项:0, 1, 3, 4。缺省值为0(女声)。 25 | 26 | 27 | This component has been added to Home-Assistant since 0.59. 28 | 29 | The `baidu` text-to-speech platform uses [Baidu TTS engine](https://cloud.baidu.com/product/speech/tts) to read a text with natural sounding voices. 30 | 31 | To get started, add the following lines to your `configuration.yaml`: 32 | 33 | ```yaml 34 | #Example configuration.yaml entry 35 | tts: 36 | - platform: baidu 37 | app_id: YOUR_APPID 38 | api_key: YOUR_APIKEY 39 | secret_key: YOUR_SECRETKEY 40 | person: 4 41 | ``` 42 | 43 | Configuration variables: 44 | 45 | - **app_id** (*Required*): AppID for use this service, registered on Baidu. 46 | - **api_key** (*Required*): Apikey from Baidu. 47 | - **secret_key** (*Required*): Secretkey from Baidu. 48 | - **speed** (*Optional*): Audio speed, from 0 to 9, default is 5. 49 | - **pitch** (*Optional*): Audio pitch, from 0 to 9, default is 5. 50 | - **volume** (*Optional*): Audio volume, from 0 to 15, default is 5. 51 | - **person** (*Optional*): You can choose 0, 1, 3, 4, default is 0(a female voice). 52 | -------------------------------------------------------------------------------- /BaiduTTS(legecy)/baidu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for the baidu speech service. 3 | 4 | """ 5 | 6 | import logging 7 | import voluptuous as vol 8 | 9 | from homeassistant.const import CONF_API_KEY 10 | from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG 11 | import homeassistant.helpers.config_validation as cv 12 | 13 | 14 | REQUIREMENTS = ["baidu-aip==1.6.6"] 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | SUPPORT_LANGUAGES = [ 20 | 'zh', 21 | ] 22 | DEFAULT_LANG = 'zh' 23 | 24 | 25 | CONF_APP_ID = 'app_id' 26 | CONF_SECRET_KEY = 'secret_key' 27 | CONF_SPEED = 'speed' 28 | CONF_PITCH = 'pitch' 29 | CONF_VOLUME = 'volume' 30 | CONF_PERSON = 'person' 31 | 32 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 33 | vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), 34 | vol.Required(CONF_APP_ID): cv.string, 35 | vol.Required(CONF_API_KEY): cv.string, 36 | vol.Required(CONF_SECRET_KEY): cv.string, 37 | vol.Optional(CONF_SPEED, default=5): vol.All( 38 | vol.Coerce(int), vol.Range(min=0, max=9)), 39 | vol.Optional(CONF_PITCH, default=5): vol.All( 40 | vol.Coerce(int), vol.Range(min=0, max=9)), 41 | vol.Optional(CONF_VOLUME, default=5): vol.All( 42 | vol.Coerce(int), vol.Range(min=0, max=15)), 43 | vol.Optional(CONF_PERSON, default=0): vol.All( 44 | vol.Coerce(int), vol.Range(min=0, max=4)), 45 | }) 46 | 47 | 48 | def get_engine(hass, config): 49 | """Set up Baidu TTS component.""" 50 | return BaiduTTSProvider(hass, config) 51 | 52 | 53 | class BaiduTTSProvider(Provider): 54 | """Baidu TTS speech api provider.""" 55 | 56 | def __init__(self, hass, conf): 57 | """Init Baidu TTS service.""" 58 | self.hass = hass 59 | self._lang = conf.get(CONF_LANG) 60 | self._codec = 'mp3' 61 | self.name = 'BaiduTTS' 62 | 63 | self._app_data = { 64 | 'appid': conf.get(CONF_APP_ID), 65 | 'apikey': conf.get(CONF_API_KEY), 66 | 'secretkey': conf.get(CONF_SECRET_KEY), 67 | } 68 | 69 | self._speech_conf_data = { 70 | 'spd': conf.get(CONF_SPEED), 71 | 'pit': conf.get(CONF_PITCH), 72 | 'vol': conf.get(CONF_VOLUME), 73 | 'per': conf.get(CONF_PERSON), 74 | } 75 | 76 | @property 77 | def default_language(self): 78 | """Return the default language.""" 79 | return self._lang 80 | 81 | @property 82 | def supported_languages(self): 83 | """Return list of supported languages.""" 84 | return SUPPORT_LANGUAGES 85 | 86 | def get_tts_audio(self, message, language, options=None): 87 | """Load TTS from BaiduTTS.""" 88 | from aip import AipSpeech 89 | aip_speech = AipSpeech( 90 | self._app_data['appid'], 91 | self._app_data['apikey'], 92 | self._app_data['secretkey'] 93 | ) 94 | 95 | result = aip_speech.synthesis( 96 | message, language, 1, self._speech_conf_data) 97 | 98 | if isinstance(result, dict): 99 | _LOGGER.error( 100 | "Baidu TTS error-- err_no:%d; err_msg:%s; err_detail:%s", 101 | result['err_no'], 102 | result['err_msg'], 103 | result['err_detail']) 104 | return (None, None) 105 | 106 | return (self._codec, result) 107 | 108 | -------------------------------------------------------------------------------- /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 | # HomeAssistant的一些组件程序 # 2 | 这儿是一些HomeAssistant的组件程序,主要为中国用户更好使用HomeAssistant所编写。 3 | 欢迎访问HomeAssistant的中国社区:[http://www.hachina.io](http://www.hachina.io) 4 | 5 | 部分组件程序已经融入HomeAssistant的官方正式版本中,还有部分没有提交。 6 | 7 | - **environment_variables**:设置homeassistant运行中的环境变量 8 | - **pulseaudio**:媒体播放器,支持本地耳机插口,也支持蓝牙音箱(已过期) 9 | - **BaiduTTS**: 文字转语音服务(使用百度云) 10 | - **Juhe_stock**:股票行情信息(使用聚合数据服务) 11 | - **aliyun_stock**:股票行情信息(使用阿里云服务) 12 | - **gaode_travel_time**:实时交通信息(使用高德地图开放API) 13 | - **juhe_joke**:笑话(使用聚合数据服务) 14 | - **juhe_laohuangli**:老黄历(使用聚合数据服务) 15 | - **program_train**:编写HomeAssistant组件与平台程序的学习样例 16 | 17 | 18 | 19 | # HAComponent # 20 | Components for HomeAssistant 21 | -------------------------------------------------------------------------------- /aliyun_stock/README.md: -------------------------------------------------------------------------------- 1 | The Aliyun stock platform uses Aliyun's stock cloud api. It can get the price of stock on Shanghai and Shenzhen's security market. 2 | 3 | To enable a sensor with aliyun_stock, add the following lines to your configuration.yaml: 4 |
#Example configuration.yaml entry
 5 | sensor:
 6 |   - platform: aliyun_stock
 7 |     appcode: xxxxxxxxxxxxxxxxxxxx
 8 |     symbols:
 9 |       - sz000002
10 |       - sh600600
11 |       - sh600000
12 | 
13 | 14 | 15 | variables: 16 | 20 | 21 | Put the file `sensor.py` `__init__.py` `manifest.json` in the dir: `~/.homeassistant/custom_components/aliyun_stock/` 22 | -------------------------------------------------------------------------------- /aliyun_stock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/aliyun_stock/__init__.py -------------------------------------------------------------------------------- /aliyun_stock/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "aliyun_stock", 3 | "name": "aliyun_stock", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/aliyun_stock", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /aliyun_stock/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The chinese stock market price information comes from Aliyun. 3 | 4 | by HAChina.io 5 | 6 | """ 7 | import logging 8 | import asyncio 9 | from datetime import timedelta 10 | 11 | import voluptuous as vol 12 | import http.client 13 | 14 | import homeassistant.helpers.config_validation as cv 15 | from homeassistant.components.sensor import PLATFORM_SCHEMA 16 | from homeassistant.const import ATTR_ATTRIBUTION 17 | from homeassistant.helpers.entity import Entity 18 | 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | ATTR_OPEN = 'open' 23 | ATTR_PREV_CLOSE = 'prev_close' 24 | ATTR_HIGH = 'high' 25 | ATTR_LOW = 'low' 26 | ATTR_NAME = 'friendly_name' 27 | 28 | CONF_ATTRIBUTION = "Chinese stock market information provided by Aliyun" 29 | CONF_SYMBOLS = 'symbols' 30 | CONF_APPCODE = 'appcode' 31 | 32 | 33 | DEFAULT_SYMBOL = 'sz000002' 34 | 35 | ICON = 'mdi:currency-cny' 36 | 37 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 38 | vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]): 39 | vol.All(cv.ensure_list, [cv.string]), 40 | vol.Required(CONF_APPCODE):cv.string, 41 | }) 42 | 43 | @asyncio.coroutine 44 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 45 | """Set up the Aliyun_stock sensor.""" 46 | 47 | symbols = config.get(CONF_SYMBOLS) 48 | appcode = config.get(CONF_APPCODE) 49 | 50 | dev = [] 51 | for symbol in symbols: 52 | data = AliyunStockData(hass, symbol, appcode) 53 | dev.append(AliyunStockSensor(data, symbol)) 54 | 55 | async_add_devices(dev, True) 56 | 57 | 58 | class AliyunStockSensor(Entity): 59 | """Representation of a Aliyun Stock sensor.""" 60 | 61 | def __init__(self, data, symbol): 62 | """Initialize the sensor.""" 63 | self.data = data 64 | self._symbol = symbol 65 | self._state = None 66 | self._unit_of_measurement = '元' 67 | self._name = symbol 68 | 69 | @property 70 | def name(self): 71 | """Return the name of the sensor.""" 72 | return self._name 73 | 74 | @property 75 | def unit_of_measurement(self): 76 | """Return the unit of measurement of this entity, if any.""" 77 | return self._unit_of_measurement 78 | 79 | @property 80 | def state(self): 81 | """Return the state of the sensor.""" 82 | return self._state 83 | 84 | @property 85 | def device_state_attributes(self): 86 | """Return the state attributes.""" 87 | if self._state is not None: 88 | return { 89 | ATTR_ATTRIBUTION: CONF_ATTRIBUTION, 90 | ATTR_OPEN: self.data.price_open, 91 | ATTR_PREV_CLOSE: self.data.prev_close, 92 | ATTR_HIGH: self.data.high, 93 | ATTR_LOW: self.data.low, 94 | ATTR_NAME: self.data.name, 95 | } 96 | 97 | @property 98 | def icon(self): 99 | """Return the icon to use in the frontend, if any.""" 100 | return ICON 101 | 102 | @asyncio.coroutine 103 | def async_update(self): 104 | """Get the latest data and updates the states.""" 105 | _LOGGER.debug("Updating sensor %s - %s", self._name, self._state) 106 | self.data.update() 107 | self._state = self.data.state 108 | 109 | 110 | class AliyunStockData(object): 111 | """Get data from Aliyun stock imformation.""" 112 | 113 | def __init__(self, hass, symbol, appcode): 114 | """Initialize the data object.""" 115 | 116 | 117 | self._symbol = symbol 118 | self.state = None 119 | self.price_open = None 120 | self.prev_close = None 121 | self.high = None 122 | self.low = None 123 | self.name = None 124 | self.hass = hass 125 | 126 | self.host = 'ali.api.intdata.cn' 127 | self.url = "/stock/hs_level2/real?code=" + self._symbol 128 | self.head = { 129 | "Authorization":"APPCODE "+ appcode, 130 | } 131 | 132 | 133 | def update(self): 134 | """Get the latest data and updates the states.""" 135 | conn = http.client.HTTPConnection(self.host) 136 | conn.request("GET",self.url,headers=self.head) 137 | result = conn.getresponse() 138 | 139 | if(result.status != 200): 140 | _LOGGER.error("Error http reponse: %d", result.status) 141 | 142 | data = eval(result.read()) 143 | 144 | if(data['state'] != 0): 145 | _LOGGER.error("Error Api return, state=%d, errmsg=%s", 146 | data['state'], 147 | data['errmsg'] 148 | ) 149 | return 150 | 151 | self.state = data['data']['price'] 152 | self.high = data['data']['high'] 153 | self.low = data['data']['low'] 154 | self.price_open = data['data']['open'] 155 | self.prev_close = data['data']['last_close'] 156 | self.name = data['data']['name'] 157 | -------------------------------------------------------------------------------- /environment_variables/README.md: -------------------------------------------------------------------------------- 1 | *本组件设置homeassistant运行时的环境变量* 2 | 3 | ## 配置 4 | 5 | 6 | ```yaml 7 | # configuration.yaml 8 | environment_variables: 9 | HTTPS_PROXY: http://homeassistant:7088 10 | ``` 11 | 12 | 以上配置设置HTTPS_PROXY环境变量。 13 | 14 | 比如,如果您在墙内想要直接使用官方的google_translate_tts集成,可以安装[simple-proxy Add-on](https://github.com/zhujisheng/hassio-addons/tree/master/simple-proxy),然后按照以上配置即可。 15 | -------------------------------------------------------------------------------- /environment_variables/__init__.py: -------------------------------------------------------------------------------- 1 | """The environment_variables component.""" 2 | 3 | import logging 4 | import os 5 | 6 | import voluptuous as vol 7 | 8 | from homeassistant.core import HomeAssistant 9 | from homeassistant.helpers import config_validation as cv 10 | from homeassistant.helpers.typing import ConfigType 11 | 12 | DOMAIN = "environment_variables" 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | CONFIG_SCHEMA = vol.Schema( 17 | {DOMAIN: {cv.string:cv.string}}, extra=vol.ALLOW_EXTRA 18 | ) 19 | 20 | 21 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 22 | """Set up the environment_variables component.""" 23 | conf = config.get(DOMAIN, {}) 24 | 25 | for name in conf: 26 | os.environ[name] = conf.get(name) 27 | _LOGGER.warning("Set environment variable: %s=%s", name, conf.get(name)) 28 | return True 29 | -------------------------------------------------------------------------------- /environment_variables/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "domain": "environment_variables", 4 | "name": "Set Environment Variables", 5 | "documentation": "https://www.home-assistant.io/integrations/environment_variables", 6 | "codeowners": ["@zhujisheng"], 7 | "iot_class": "local_push" 8 | } 9 | -------------------------------------------------------------------------------- /gaode_travel_time/README.md: -------------------------------------------------------------------------------- 1 |

中文说明

2 | 3 | 高德地图行程时间(gaode_travel_time)使用高德的API获得信息:http://lbs.amap.com/api/webservice/guide/api/direction/. 4 | 5 | 如下配置configuration.yaml文件: 6 |
 7 | sensor:
 8 |   - platform: gaode_travel_time
 9 |     api_key: XXXXXXXXXXXXXXXXXXXXXXXX
10 |     name: driving_working
11 |     friendly_name: 从家去公司
12 |     travel_mode: driving
13 |     strategy: 0       #optional, 0-9, default 0 速度最快
14 |     origin:
15 |       #longitude_latitude: 116.481028,39.989643
16 |       city: 上海
17 |       address: 凤城路
18 |     destination:
19 |       #longitude_latitude: 121.3997,31.0456
20 |       city: 上海
21 |       address: 广富林路
22 | 
23 | 变量说明: 24 | 36 | 37 | 将文件`sensor.py`、`__init__.py`、`manifest.json`放置在目录`~/.homeassistant/custom_components/gaode_travel_time/`中。 38 | 39 | 组件每半小时更新一次信息,如果想获得当前的信息,调用服务:"sensor.gaode_travel_time_update"。 40 | 41 | 42 |

Description in English

43 | The gaode_travel_time sensor uses Gaode open api http://lbs.amap.com/api/webservice/guide/api/direction/. 44 | 45 | To enable a sensor with gaode_travel_time, add the following lines to your configuration.yaml: 46 | 47 |
48 | sensor:
49 |   - platform: gaode_travel_time
50 |     api_key: XXXXXXXXXXXXXXXXXXXXXXXX
51 |     friendly_name: 从家去公司
52 |     travel_mode: driving
53 |     strategy: 0       #optional, 0-9, default 0 速度最快
54 |     origin:
55 |       #longitude_latitude: 116.481028,39.989643
56 |       city: 上海
57 |       address: 凤城路
58 |     destination:
59 |       #longitude_latitude: 121.3997,31.0456
60 |       city: 上海
61 |       address: 广富林路
62 | 
63 | variables: 64 | 76 | 77 | Put the file `sensor.py` `__init__.py` `manifest.json` in the dir: `~/.homeassistant/custom_components/gaode_travel_time/` 78 | 79 | The sensor update imformation every half hour, if you want the current imformation, can call service `sensor.gaode_travel_time_update`. 80 | -------------------------------------------------------------------------------- /gaode_travel_time/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/gaode_travel_time/__init__.py -------------------------------------------------------------------------------- /gaode_travel_time/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "gaode_travel_time", 3 | "name": "gaode_travel_time", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/gaode_travel_time", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /gaode_travel_time/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Gaode travel time sensors. 3 | 4 | by HAChina 5 | 6 | """ 7 | 8 | 9 | import asyncio 10 | import async_timeout 11 | import aiohttp 12 | from datetime import timedelta 13 | import logging 14 | import json 15 | 16 | import voluptuous as vol 17 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 18 | from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) 19 | from homeassistant.helpers.entity import Entity 20 | from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY ) 21 | from homeassistant.helpers.event import async_track_time_interval 22 | import homeassistant.helpers.config_validation as cv 23 | import homeassistant.util.dt as dt_util 24 | 25 | 26 | 27 | _LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | CONF_ORIGIN = 'origin' 31 | CONF_DESTINATION = 'destination' 32 | CONF_TRAVEL_MODE = 'travel_mode' 33 | CONF_STRATEGY = 'strategy' 34 | CONF_ATTRIBUTION = "Transport information provided by Gaode" 35 | CONF_LONGITUDE_LATITUDE = "longitude_latitude" 36 | CONF_CITY = "city" 37 | CONF_ADDRESS = "address" 38 | CONF_NAME = "name" 39 | CONF_FRIENDLY_NAME = 'friendly_name' 40 | 41 | DEFAULT_NAME = 'Gaode_Travel_Time' 42 | DEFAULT_TRAVEL_MODE = 'driving' 43 | DEFAULT_STRATEGY = 0 44 | 45 | TIME_BETWEEN_UPDATES = timedelta(minutes=30) 46 | 47 | 48 | TRAVEL_MODE = ['driving', 'walking', 'bicycling'] 49 | 50 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 51 | vol.Required(CONF_API_KEY): cv.string, 52 | vol.Required(CONF_ORIGIN): vol.All(dict, vol.Schema({ 53 | vol.Optional(CONF_LONGITUDE_LATITUDE): cv.string, 54 | vol.Optional(CONF_CITY):cv.string, 55 | vol.Optional(CONF_ADDRESS):cv.string, 56 | })), 57 | vol.Required(CONF_DESTINATION): vol.All(dict, vol.Schema({ 58 | vol.Optional(CONF_LONGITUDE_LATITUDE): cv.string, 59 | vol.Optional(CONF_CITY):cv.string, 60 | vol.Optional(CONF_ADDRESS):cv.string, 61 | })), 62 | vol.Optional(CONF_NAME, default= DEFAULT_NAME): cv.string, 63 | vol.Optional(CONF_FRIENDLY_NAME, default= DEFAULT_NAME): cv.string, 64 | vol.Optional(CONF_TRAVEL_MODE, default=DEFAULT_TRAVEL_MODE): vol.In(TRAVEL_MODE), 65 | vol.Optional(CONF_STRATEGY,default=DEFAULT_STRATEGY):vol.All(vol.Coerce(int), vol.Range(min=0,max=9)), 66 | }) 67 | 68 | 69 | 70 | @asyncio.coroutine 71 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 72 | 73 | api_key = config.get(CONF_API_KEY) 74 | origin = config.get(CONF_ORIGIN) 75 | destination = config.get(CONF_DESTINATION) 76 | travel_mode = config.get(CONF_TRAVEL_MODE) 77 | strategy = config.get(CONF_STRATEGY) 78 | 79 | name = config.get(CONF_NAME) 80 | friendly_name = config.get(CONF_FRIENDLY_NAME) 81 | 82 | data = GaodeTravelTimeData( hass, api_key, origin, destination, travel_mode, strategy ) 83 | 84 | 85 | 86 | if(yield from data.async_setup()): 87 | yield from data.async_update(dt_util.now()) 88 | async_track_time_interval( hass, data.async_update, TIME_BETWEEN_UPDATES ) 89 | 90 | sensor = GaodeTravelTimeSensor( hass, name, friendly_name, data ) 91 | async_add_devices([sensor]) 92 | 93 | @asyncio.coroutine 94 | def async_update(call=None): 95 | '''Update the data by service call''' 96 | yield from data.async_update(dt_util.now()) 97 | sensor.async_update() 98 | 99 | hass.services.async_register(DOMAIN, name+'_update', async_update) 100 | 101 | 102 | 103 | 104 | 105 | 106 | class GaodeTravelTimeSensor(Entity): 107 | """Representation of a Gaode travel time sensor.""" 108 | 109 | def __init__(self, hass, name, friendly_name, data ): 110 | """Initialize the sensor.""" 111 | self._hass = hass 112 | self._name = name 113 | self._friendly_name = friendly_name 114 | self._unit_of_measurement = "分钟" 115 | self._data = data 116 | 117 | 118 | 119 | @property 120 | def state(self): 121 | """Return the state of the sensor.""" 122 | return self._data._duration 123 | 124 | @property 125 | def name(self): 126 | """Get the name of the sensor.""" 127 | return self._name 128 | 129 | @property 130 | def device_state_attributes(self): 131 | """Return the state attributes.""" 132 | if self._data is not None: 133 | return { 134 | ATTR_ATTRIBUTION: CONF_ATTRIBUTION, 135 | CONF_ORIGIN:self._data._origin, 136 | CONF_DESTINATION:self._data._destination, 137 | CONF_TRAVEL_MODE:self._data._travel_mode, 138 | CONF_STRATEGY:self._data._strategy, 139 | CONF_FRIENDLY_NAME:self._friendly_name, 140 | "distance":self._data._distance, 141 | "textguide": self._data._textguide, 142 | "update_time": self._data._update_time 143 | } 144 | 145 | @property 146 | def unit_of_measurement(self): 147 | """Return the unit this state is expressed in.""" 148 | return self._unit_of_measurement 149 | 150 | @asyncio.coroutine 151 | def async_update(self): 152 | pass 153 | 154 | 155 | class GaodeTravelTimeData(object): 156 | """Representation of a Gaode Travel Time sensor""" 157 | 158 | def __init__(self, hass, api_key, origin, destination, travel_mode, strategy ): 159 | 160 | self._hass = hass 161 | self._origin = origin 162 | self._destination = destination 163 | self._strategy = strategy 164 | 165 | 166 | self._duration = None 167 | self._distance = None 168 | self._textguide = None 169 | self._travel_mode = travel_mode 170 | self._api_key = api_key 171 | self._update_time = None 172 | 173 | 174 | 175 | @asyncio.coroutine 176 | def async_setup(self): 177 | 178 | origin_longitude_latitude = yield from self.async_get_longitude_latitude(self._origin) 179 | destination_longitude_latitude = yield from self.async_get_longitude_latitude(self._destination) 180 | 181 | if (origin_longitude_latitude is None) or (destination_longitude_latitude is None): 182 | _LOGGER.error("Cannot get the longitude_latitude" ) 183 | return False 184 | 185 | 186 | if( self._travel_mode == "walking" ): 187 | self._url = ( 'http://restapi.amap.com/v3/direction/walking?key=' 188 | + self._api_key 189 | + '&origin=' + origin_longitude_latitude 190 | + '&destination=' + destination_longitude_latitude 191 | + '&output=JSON' 192 | ) 193 | elif( self._travel_mode == "bicycling"): 194 | self._url = ( 'http://restapi.amap.com/v4/direction/bicycling?key=' 195 | + self._api_key 196 | + '&origin=' + origin_longitude_latitude 197 | + '&destination=' + destination_longitude_latitude 198 | ) 199 | else: 200 | self._url = ( 'http://restapi.amap.com/v3/direction/driving?key=' 201 | + self._api_key 202 | + '&origin=' + origin_longitude_latitude 203 | + '&destination=' + destination_longitude_latitude 204 | + '&extensions=base' 205 | + '&strategy=' + str(self._strategy) 206 | + '&output=JSON' 207 | ) 208 | return True 209 | 210 | 211 | @asyncio.coroutine 212 | def async_update(self, now): 213 | 214 | try: 215 | session = async_get_clientsession(self._hass) 216 | with async_timeout.timeout(15, loop=self._hass.loop): 217 | response = yield from session.get(self._url) 218 | 219 | except(asyncio.TimeoutError, aiohttp.ClientError): 220 | _LOGGER.error("Error while accessing: %s", self._url) 221 | return 222 | 223 | if response.status != 200: 224 | _LOGGER.error("Error while accessing: %s, status=%d", self._url, response.status) 225 | return 226 | 227 | data = yield from response.json() 228 | 229 | if data is None: 230 | _LOGGER.error("Request api Error: %s", self._url) 231 | return 232 | 233 | if(self._travel_mode != "bicycling"): 234 | if(data['status'] != '1'): 235 | _LOGGER.error("Error Api return, state=%s, errmsg=%s", 236 | data['status'], 237 | data['info'] 238 | ) 239 | return 240 | dataroute = data["route"] 241 | 242 | 243 | else: 244 | if(data['errcode'] != 0 ): 245 | _LOGGER.error("Error Api return, errcode=%s, errmsg=%s", 246 | data['errcode'], 247 | data['errmsg'], 248 | ) 249 | return 250 | dataroute = data['data'] 251 | 252 | 253 | self._origin = dataroute["origin"] 254 | self._destination = dataroute["destination"] 255 | if(self._travel_mode == "driving"): 256 | self._strategy = dataroute["paths"][0]["strategy"] 257 | 258 | self._duration = int(dataroute["paths"][0]["duration"])/60 259 | self._distance = float(dataroute["paths"][0]["distance"])/1000 260 | 261 | bypasstext = "途经" 262 | roadbefore = "" 263 | for step in dataroute["paths"][0]["steps"]: 264 | if ('road' in step.keys() and step["road"] != []): 265 | if( step["assistant_action"] == "到达目的地" ): 266 | if (step["road"] != roadbefore): 267 | bypasstext = bypasstext + roadbefore + "、" + step["road"] + "。" 268 | roadbefore = step["road"] 269 | else: 270 | bypasstext = bypasstext + roadbefore + "。" 271 | roadbefore = step["road"] 272 | else: 273 | if roadbefore == "": 274 | roadbefore = step["road"] 275 | elif (step["road"] != roadbefore): 276 | bypasstext = bypasstext + roadbefore + "、" 277 | roadbefore = step["road"] 278 | else: 279 | if( step["assistant_action"] == "到达目的地" ): 280 | bypasstext = bypasstext + roadbefore + "。" 281 | 282 | 283 | self._textguide = ("行程%.1f公里。需花时%d分钟。%s" 284 | %(self._distance, 285 | self._duration, 286 | bypasstext 287 | ) 288 | ) 289 | self._update_time = dt_util.now() 290 | 291 | 292 | @asyncio.coroutine 293 | def async_get_longitude_latitude(self, address_dict): 294 | 295 | if address_dict.get(CONF_LONGITUDE_LATITUDE) is not None: 296 | return address_dict.get(CONF_LONGITUDE_LATITUDE) 297 | 298 | if (address_dict.get(CONF_ADDRESS) is None) or (address_dict.get(CONF_CITY) is None): 299 | return 300 | 301 | url = ("http://restapi.amap.com/v3/geocode/geo?key=" 302 | + self._api_key 303 | + '&address=' + address_dict.get(CONF_ADDRESS) 304 | + '&city=' + address_dict.get(CONF_CITY) 305 | ) 306 | 307 | try: 308 | session = async_get_clientsession(self._hass) 309 | with async_timeout.timeout(15, loop=self._hass.loop): 310 | response = yield from session.get( url ) 311 | 312 | except(asyncio.TimeoutError, aiohttp.ClientError): 313 | _LOGGER.error("Error while accessing: %s", url) 314 | return 315 | 316 | if response.status != 200: 317 | _LOGGER.error("Error while accessing: %s, status=%d", url, response.status) 318 | return 319 | 320 | data = yield from response.json() 321 | 322 | if data is None: 323 | _LOGGER.error("Request api Error: %s", url) 324 | return 325 | elif (data['status'] != '1'): 326 | _LOGGER.error("Error Api return, state=%s, errmsg=%s", 327 | data['status'], 328 | data['info'] 329 | ) 330 | return 331 | 332 | return data['geocodes'][0]['location'] 333 | -------------------------------------------------------------------------------- /introduction_hachina/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | For more details about HAChina, 4 | https://www.hachina.io/ 5 | """ 6 | import asyncio 7 | import logging 8 | 9 | import voluptuous as vol 10 | 11 | DOMAIN = 'introduction' 12 | 13 | CONFIG_SCHEMA = vol.Schema({ 14 | DOMAIN: vol.Schema({}), 15 | }, extra=vol.ALLOW_EXTRA) 16 | 17 | 18 | @asyncio.coroutine 19 | def async_setup(hass, config=None): 20 | """Set up the introduction component.""" 21 | log = logging.getLogger(__name__) 22 | log.info(""" 23 | 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | 欢迎使用HACHINA.IO创建的镜像文件! 27 | 28 | 我们的网站https://www.hachina.io 29 | 30 | 31 | This message is generated by the introduction_hachina component. You can 32 | disable it in configuration.yaml. 33 | 34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | """) 36 | 37 | hass.components.persistent_notification.async_create(""" 38 | [![HACHINA](https://www.hachina.io/wp-content/themes/haChina/images/logo@2x.png)](https://www.hachina.io) 39 | 40 | 在此镜像文件中,我们安装与开放了以下服务: 41 | 42 | - [HomeAssistant](https://home-assistant.io/) 43 | - [Jupyter Notebook](http://jupyter.org/) 44 | - [Mosquitto](http://www.mosquitto.org/) 45 | - [Samba](https://www.samba.org/) 46 | - [SshD](https://www.openssh.com/) 47 | - [Node-RED](https://nodered.org/) 48 | 注:Node-RED服务已安装,但没有初始化启动。设置自启动服务命令`sudo systemctl enable nodered.service`。 49 | - [AppDaemon&DashBoard](https://appdaemon.readthedocs.io/) 50 | 注:AppDaemon&DashBoard服务已安装,但没有初始化启动。请在`/home/pi/appdaemon/appdaemon.yaml`中配置token后使用。设置自启动服务命令`sudo systemctl enable appdaemon@pi`。 51 | 52 | 53 | 密码与修改: 54 | 55 | - 操作系统的`pi`账号,初始密码为`hachina`。以`pi`账号登录后,使用`passwd`命令修改 56 | - Jupyter Notebook的初始访问密码为`hachina`。以`pi`账号登录后,使用`jupyter notebook password`命令修改 57 | - Mosquitto的用户名为`pi`,初始密码为`hachina`。以`pi`账号登录后,使用`sudo mosquitto_passwd /etc/mosquitto/passwd pi`命令修改 58 | - DashBoard的初始访问密码为`hachina`(访问端口为5050)。以`pi`账号登录后,在文件`/home/pi/appdaemon/appdaemon.yaml`中修改。 59 | - Node-RED初始用户与密码未设置。在文件`/home/pi/.node-red/settings.js`中修改(使用命令`node-red-admin hash-pw`生成密码的hash值)。 60 | 61 | 第一次启动,HomeAssistant会自动生成配置文件,与标准的HomeAssistant缺省配置比较,有以下不同: 62 | 63 | - 配置目录下空白的`known_devices.yaml`文件 64 | - 配置在sensor组件下的bitcoin平台 65 | - 将tts组件中google平台设置为中文 66 | - 媒体播放器(media_player)组件中配置vlc平台 67 | - [introduction_hachina组件](https://github.com/zhujisheng/HAComponent/tree/master/introduction_hachina) 68 | - [tunnel2local组件](https://github.com/zhujisheng/HAComponent/tree/master/tunnel2local) 69 | - [redpoint组件](https://github.com/HAChina/redpoint) 70 | 71 | 72 | 我们的网站:[https://www.hachina.io](https://www.hachina.io) 73 | 74 | 欲去除本卡信息,请编辑`configuration.yaml`文件,删除或注释`introduction_hachina`组件配置 75 | """, '欢迎使用HACHINA.IO创建的镜像文件!') # noqa 76 | 77 | return True 78 | -------------------------------------------------------------------------------- /introduction_hachina/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "introduction_hachina", 3 | "name": "introduction_hachina", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/blob/master/introduction_hachina/introduction_hachina.py", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /juhe_joke/README.md: -------------------------------------------------------------------------------- 1 |

中文说明

2 | 3 | 聚合笑话(juhe_joke)从聚合数据API获得数据。 4 | 在HA的configuration.yaml中的配置: 5 |
 6 | #Example configuration.yaml entry
 7 | sensor:
 8 |   - platform: juhe_joke
 9 |     key: xxxxxxxxxxxxxxxx
10 | 
11 | 12 | 配置变量: 13 | 16 | 17 | 将文件`sensor.py` `__init.py` `manifest.json`放在以下目录: `~/.homeassistant/custom_components/juhe_joke/`。 18 | 每天会更新20条笑话信息。如果您想更换,调用服务“sensor.jokes_update”。 19 | 20 |

description in English

21 | 22 | The Joke sensor uses Juhe's open platform's joke api. 23 | 24 | To enable a sensor with juhe_joke, add the following lines to your configuration.yaml: 25 |
26 | #Example configuration.yaml entry
27 | sensor:
28 |   - platform: juhe_joke
29 |     key: xxxxxxxxxxxxxxxxxx
30 | 
31 | variables: 32 | 35 | 36 | Put the file `sensor.py` `__init.py` `manifest.json` in the dir: `~/.homeassistant/custom_components/juhe_joke/` 37 | 38 | The sensor update imformation every day(get 20 jokes from Juhe), if you want to change some jokes, call service `sensor.jokes_update`. 39 | -------------------------------------------------------------------------------- /juhe_joke/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/juhe_joke/__init__.py -------------------------------------------------------------------------------- /juhe_joke/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "juhe_joke", 3 | "name": "juhe_joke", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/juhe_joke", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /juhe_joke/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The chinese jokes come from Juhe. 3 | 4 | by HAChina.io 5 | 6 | """ 7 | import logging 8 | import json 9 | from urllib import request, parse 10 | from random import randint 11 | import asyncio 12 | from datetime import timedelta 13 | 14 | import voluptuous as vol 15 | 16 | import homeassistant.helpers.config_validation as cv 17 | from homeassistant.components.sensor import (DOMAIN,PLATFORM_SCHEMA) 18 | from homeassistant.const import (ATTR_ATTRIBUTION, CONF_NAME) 19 | from homeassistant.helpers.entity import Entity 20 | from homeassistant.helpers.event import async_track_time_change 21 | import homeassistant.util.dt as dt_util 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | 26 | CONF_ATTRIBUTION = "Today's jokes provided by Juhe" 27 | CONF_KEY = 'key' 28 | 29 | DEFAULT_NAME = 'Jokes' 30 | ICON = 'mdi:book-open-variant' 31 | 32 | 33 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 34 | vol.Required(CONF_KEY):cv.string, 35 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 36 | }) 37 | 38 | @asyncio.coroutine 39 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 40 | """Set up the joke sensor.""" 41 | 42 | key = config.get(CONF_KEY) 43 | name = config.get(CONF_NAME) 44 | 45 | dev = [] 46 | data = JuheJokeData(hass, key) 47 | sensor = JuheJokeSensor(data, name) 48 | dev.append(sensor) 49 | 50 | async_add_devices(dev, True) 51 | 52 | def update(call=None): 53 | '''Update the data by service call''' 54 | data.update(dt_util.now()) 55 | sensor.async_update() 56 | 57 | hass.services.async_register(DOMAIN, name+'_update',update) 58 | 59 | 60 | 61 | 62 | class JuheJokeSensor(Entity): 63 | """Representation of a Juhe Joke sensor.""" 64 | 65 | def __init__(self, data, name): 66 | """Initialize the sensor.""" 67 | self._data = data 68 | self._name = name 69 | 70 | @property 71 | def name(self): 72 | """Return the name of the sensor.""" 73 | return self._name 74 | 75 | 76 | @property 77 | def state(self): 78 | """Return the state of the sensor.""" 79 | return self._data.state 80 | 81 | @property 82 | def device_state_attributes(self): 83 | """Return the state attributes.""" 84 | if self._data.state is not None: 85 | return self._data.story 86 | 87 | @property 88 | def icon(self): 89 | """Return the icon to use in the frontend, if any.""" 90 | return ICON 91 | 92 | @asyncio.coroutine 93 | def async_update(self): 94 | """Get the latest data and updates the states.""" 95 | 96 | 97 | 98 | class JuheJokeData(object): 99 | """Get data from Juhe Joke imformation.""" 100 | 101 | def __init__(self, hass, key): 102 | """Initialize the data object.""" 103 | 104 | 105 | self.story = {} 106 | self.hass = hass 107 | 108 | self.url = "http://v.juhe.cn/joke/content/text.php" 109 | self.key = key 110 | 111 | self.state = None 112 | 113 | self.update(dt_util.now()) 114 | async_track_time_change( self.hass, self.update, hour=[0], minute=[0], second=[1] ) 115 | 116 | 117 | def update(self, now): 118 | """Get the latest data and updates the states.""" 119 | 120 | params = { 121 | "key": self.key, 122 | "page": randint(1,25000), 123 | "pagesize": 20 124 | } 125 | 126 | f = request.urlopen( self.url, parse.urlencode(params).encode('utf-8') ) 127 | 128 | content = f.read() 129 | 130 | result = json.loads(content.decode('utf-8')) 131 | 132 | if result is None: 133 | _LOGGER.error("Request api Error") 134 | return 135 | elif (result["error_code"] != 0): 136 | _LOGGER.error("Error API return, errorcode=%s, reson=%s", 137 | result["error_code"], 138 | result["reason"], 139 | ) 140 | return 141 | 142 | self.story = {} 143 | i = 0 144 | for data in result["result"]["data"]: 145 | i = i+1 146 | self.story["story%d" %(i)] = data["content"] 147 | 148 | self.state = 'ready' 149 | 150 | -------------------------------------------------------------------------------- /juhe_laohuangli/README.md: -------------------------------------------------------------------------------- 1 |

中文说明

2 | 3 | 聚合数据老黄历信息(juhe_laohuangli)从聚合数据API获得数据。 4 | 在HA的configuration.yaml中的配置: 5 |
 6 | #Example configuration.yaml entry
 7 | sensor:
 8 |   - platform: juhe_laohuangli
 9 |     key: xxxxxxxxxxxxxxxx
10 | 
11 | 12 | 配置变量: 13 | 16 | 17 | 将文件`sensor.py` `__init__.py` `manifest.json`放在以下目录: `~/.homeassistant/custom_components/juhe_laohuangli/` 18 | 19 |

Description in English

20 | The Juhe Laohuangli uses Juhe's Loahuangli api. 21 | 22 | To enable a sensor with juhe_laohuangli, add the following lines to your configuration.yaml: 23 | 24 |
25 | #Example configuration.yaml entry
26 | sensor:
27 |   - platform: juhe_laohuangli
28 |     key: xxxxxxxxxxxxxxxxxx
29 | 
30 | variables: 31 | 34 | 35 | Put the file `sensor.py` `__init__.py` `manifest.json` in the dir: `~/.homeassistant/custom_components/juhe_laohuangli/` 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /juhe_laohuangli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/juhe_laohuangli/__init__.py -------------------------------------------------------------------------------- /juhe_laohuangli/laohuangli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/juhe_laohuangli/laohuangli.png -------------------------------------------------------------------------------- /juhe_laohuangli/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "juhe_laohuangli", 3 | "name": "juhe_laohuangli", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/juhe_laohuangli", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /juhe_laohuangli/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The chinese 老黄历 information comes from Juhe. 3 | 4 | by HAChina.io 5 | 6 | """ 7 | import asyncio 8 | import async_timeout 9 | import aiohttp 10 | import logging 11 | import json 12 | from datetime import timedelta 13 | import voluptuous as vol 14 | 15 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 16 | import homeassistant.helpers.config_validation as cv 17 | from homeassistant.components.sensor import PLATFORM_SCHEMA 18 | from homeassistant.const import ATTR_ATTRIBUTION 19 | from homeassistant.helpers.entity import Entity 20 | from homeassistant.helpers.event import async_track_time_change 21 | import homeassistant.util.dt as dt_util 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | 26 | CONF_ATTRIBUTION = "Today's laohuangli provided by Juhe" 27 | CONF_KEY = 'key' 28 | 29 | DEFAULT_NAME = 'LaoHuangLi' 30 | ICON = 'mdi:yin-yang' 31 | 32 | 33 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 34 | vol.Required(CONF_KEY):cv.string, 35 | }) 36 | 37 | @asyncio.coroutine 38 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 39 | """Set up the laohuangli sensor.""" 40 | 41 | key = config.get(CONF_KEY) 42 | 43 | data = JuheLaohuangliData(hass, key) 44 | yield from data.async_update(dt_util.now()) 45 | async_track_time_change( hass, data.async_update, hour=[0], minute=[0], second=[1] ) 46 | 47 | dev = [] 48 | dev.append(JuheLaohuangliSensor(data)) 49 | async_add_devices(dev, True) 50 | 51 | 52 | class JuheLaohuangliSensor(Entity): 53 | """Representation of a Juhe Laohuangli sensor.""" 54 | 55 | def __init__(self, data): 56 | """Initialize the sensor.""" 57 | self._data = data 58 | self._name = DEFAULT_NAME 59 | 60 | @property 61 | def name(self): 62 | """Return the name of the sensor.""" 63 | return self._name 64 | 65 | 66 | @property 67 | def state(self): 68 | """Return the state of the sensor.""" 69 | return self._data.yinli 70 | 71 | @property 72 | def device_state_attributes(self): 73 | """Return the state attributes.""" 74 | if self._data is not None: 75 | return { 76 | "阳历": self._data.yangli, 77 | "阴历": self._data.yinli, 78 | "五行": self._data.wuxing, 79 | "冲煞": self._data.chongsha, 80 | "百忌": self._data.baiji, 81 | "吉神": self._data.jishen, 82 | "宜": self._data.yi, 83 | "凶神": self._data.xiongshen, 84 | "忌": self._data.ji, 85 | } 86 | 87 | @property 88 | def icon(self): 89 | """Return the icon to use in the frontend, if any.""" 90 | return ICON 91 | 92 | @asyncio.coroutine 93 | def async_update(self): 94 | """Get the latest data and updates the states.""" 95 | 96 | 97 | 98 | class JuheLaohuangliData(object): 99 | """Get data from Juhe laohuangli imformation.""" 100 | 101 | def __init__(self, hass, key): 102 | """Initialize the data object.""" 103 | 104 | 105 | self.yangli = None 106 | self.yinli = None 107 | self.wuxing = None 108 | self.chongsha = None 109 | self.baiji = None 110 | self.jishen = None 111 | self.yi = None 112 | self.xiongshen = None 113 | self.ji = None 114 | 115 | self.hass = hass 116 | 117 | self.url = "http://v.juhe.cn/laohuangli/d" 118 | self.key = key 119 | 120 | 121 | @asyncio.coroutine 122 | def async_update(self, now): 123 | """Get the latest data and updates the states.""" 124 | 125 | date = now.strftime("%Y-%m-%d") 126 | params = { 127 | "key": self.key, 128 | "date": date, 129 | } 130 | 131 | try: 132 | session = async_get_clientsession(self.hass) 133 | with async_timeout.timeout(15, loop=self.hass.loop): 134 | response = yield from session.post( self.url, data=params ) 135 | 136 | except(asyncio.TimeoutError, aiohttp.ClientError): 137 | _LOGGER.error("Error while accessing: %s", self.url) 138 | return 139 | 140 | if response.status != 200: 141 | _LOGGER.error("Error while accessing: %s, status=%d", url, response.status) 142 | return 143 | 144 | result = yield from response.json() 145 | 146 | if result is None: 147 | _LOGGER.error("Request api Error: %s", url) 148 | return 149 | elif (result["error_code"] != 0): 150 | _LOGGER.error("Error API return, errorcode=%s, reson=%s", 151 | result["error_code"], 152 | result["reason"], 153 | ) 154 | return 155 | 156 | 157 | self.yangli = result["result"]["yangli"] 158 | self.yinli = result["result"]["yinli"] 159 | self.wuxing = result["result"]["wuxing"].replace(" ","、") 160 | self.chongsha = result["result"]["chongsha"] 161 | self.baiji = result["result"]["baiji"].replace(" ","、") 162 | self.jishen = result["result"]["jishen"].replace(" ","、") 163 | self.yi = result["result"]["yi"].replace(" ","、") 164 | self.xiongshen = result["result"]["xiongshen"].replace(" ","、") 165 | self.ji = result["result"]["ji"].replace(" ","、") 166 | -------------------------------------------------------------------------------- /juhe_stock/README.md: -------------------------------------------------------------------------------- 1 | 聚合数据股票信息组件使用[聚合云API](https://www.juhe.cn/docs/api/id/21)。组件获得上海和深圳证交所的股票交易信息。 2 | 3 | 将以下内容放置在`configuration.yaml`文件中: 4 | ```yaml 5 | # configuration.yaml样例 6 | sensor: 7 | - platform: juhe_stock 8 | key: xxxxxxxxxxxxxxxxxxxx 9 | symbols: 10 | - sz000002 11 | - sh600600 12 | - sh600000 13 | ``` 14 | 可配置项: 15 | - **key** (*必选项*): 聚合数据API的Key。 16 | - **symbols** (*列表 可选项*): 股票代码列表. 如果未配置, 缺省值是sz000002 (万科A)。 17 | 18 | 19 | ### Description in English ### 20 | The Juhe stock platform uses Juhe's stock cloud api. It can get the price of stock on Shanghai and Shenzhen's security market. 21 | 22 | To enable a sensor with juhe_stock, add the following lines to your configuration.yaml: 23 | ```yaml 24 | #Example configuration.yaml entry 25 | sensor: 26 | - platform: juhe_stock 27 | key: xxxxxxxxxxxxxxxxxxxx 28 | symbols: 29 | - sz000002 30 | - sh600600 31 | - sh600000 32 | ``` 33 | 34 | 35 | variables: 36 | 40 | 41 | Put the file `sensor.py` `__init__.py` `manifest.json` in the dir: `~/.homeassistant/custom_components/juhe_stock/` 42 | -------------------------------------------------------------------------------- /juhe_stock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/juhe_stock/__init__.py -------------------------------------------------------------------------------- /juhe_stock/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "juhe_stock", 3 | "name": "juhe_stock", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/juhe_stock", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /juhe_stock/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The chinese stock market price information comes from Juhe. 3 | 4 | by HAChina.io 5 | 6 | """ 7 | import logging 8 | import json 9 | import asyncio 10 | from datetime import timedelta 11 | 12 | import voluptuous as vol 13 | 14 | import http.client 15 | 16 | import homeassistant.helpers.config_validation as cv 17 | from homeassistant.components.sensor import PLATFORM_SCHEMA 18 | from homeassistant.const import ATTR_ATTRIBUTION 19 | from homeassistant.helpers.entity import Entity 20 | 21 | 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | ATTR_OPEN = 'open' 26 | ATTR_PREV_CLOSE = 'prev_close' 27 | ATTR_HIGH = 'high' 28 | ATTR_LOW = 'low' 29 | ATTR_NAME = 'friendly_name' 30 | 31 | CONF_ATTRIBUTION = "Chinese stock market information provided by Juhe" 32 | CONF_SYMBOLS = 'symbols' 33 | CONF_KEY = 'key' 34 | 35 | 36 | DEFAULT_SYMBOL = 'sz000002' 37 | 38 | ICON = 'mdi:currency-cny' 39 | 40 | 41 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 42 | vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]): 43 | vol.All(cv.ensure_list, [cv.string]), 44 | vol.Required(CONF_KEY):cv.string, 45 | }) 46 | 47 | @asyncio.coroutine 48 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 49 | """Set up the Aliyun_stock sensor.""" 50 | 51 | symbols = config.get(CONF_SYMBOLS) 52 | key = config.get(CONF_KEY) 53 | 54 | dev = [] 55 | for symbol in symbols: 56 | data = JuheStockData(hass, symbol, key) 57 | dev.append(JuheStockSensor(data, symbol)) 58 | 59 | async_add_devices(dev, True) 60 | 61 | 62 | class JuheStockSensor(Entity): 63 | """Representation of a Juhe Stock sensor.""" 64 | 65 | def __init__(self, data, symbol): 66 | """Initialize the sensor.""" 67 | self.data = data 68 | self._symbol = symbol 69 | self._state = None 70 | self._unit_of_measurement = '元' 71 | self._name = symbol 72 | 73 | @property 74 | def name(self): 75 | """Return the name of the sensor.""" 76 | return self._name 77 | 78 | @property 79 | def unit_of_measurement(self): 80 | """Return the unit of measurement of this entity, if any.""" 81 | return self._unit_of_measurement 82 | 83 | @property 84 | def state(self): 85 | """Return the state of the sensor.""" 86 | return self._state 87 | 88 | @property 89 | def device_state_attributes(self): 90 | """Return the state attributes.""" 91 | if self._state is not None: 92 | return { 93 | ATTR_ATTRIBUTION: CONF_ATTRIBUTION, 94 | ATTR_OPEN: self.data.price_open, 95 | ATTR_PREV_CLOSE: self.data.prev_close, 96 | ATTR_HIGH: self.data.high, 97 | ATTR_LOW: self.data.low, 98 | ATTR_NAME: self.data.name, 99 | } 100 | 101 | @property 102 | def icon(self): 103 | """Return the icon to use in the frontend, if any.""" 104 | return ICON 105 | 106 | @asyncio.coroutine 107 | def async_update(self): 108 | """Get the latest data and updates the states.""" 109 | _LOGGER.debug("Updating sensor %s - %s", self._name, self._state) 110 | self.data.update() 111 | self._state = self.data.state 112 | 113 | 114 | class JuheStockData(object): 115 | """Get data from Juhe stock imformation.""" 116 | 117 | def __init__(self, hass, symbol, key): 118 | """Initialize the data object.""" 119 | 120 | 121 | self._symbol = symbol 122 | self.state = None 123 | self.price_open = None 124 | self.prev_close = None 125 | self.high = None 126 | self.low = None 127 | self.name = None 128 | self.hass = hass 129 | 130 | self.host = 'web.juhe.cn:8080' 131 | self.url = "/finance/stock/hs?gid=" + self._symbol + "&key=" + key 132 | 133 | 134 | def update(self): 135 | """Get the latest data and updates the states.""" 136 | conn = http.client.HTTPConnection(self.host) 137 | conn.request("GET",self.url ) 138 | result = conn.getresponse() 139 | 140 | if(result.status != 200): 141 | _LOGGER.error("Error http reponse: %d", result.status) 142 | return 143 | 144 | #data = eval(result.read()) 145 | data = json.loads( str(result.read(),encoding = 'utf-8') ) 146 | 147 | if(data['resultcode'] != "200"): 148 | _LOGGER.error("Error Api return, resultcode=%s, reason=%s", 149 | data['resultcode'], 150 | data['reason'] 151 | ) 152 | return 153 | 154 | self.state = data['result'][0]['data']['nowPri'] 155 | self.high = data['result'][0]['data']['todayMax'] 156 | self.low = data['result'][0]['data']['todayMin'] 157 | self.price_open = data['result'][0]['data']['todayStartPri'] 158 | self.prev_close = data['result'][0]['data']['yestodEndPri'] 159 | self.name = data['result'][0]['data']['name'] 160 | -------------------------------------------------------------------------------- /program_train/README.md: -------------------------------------------------------------------------------- 1 | HomeAssistant中组件和平台编程的教学程序。 2 | 3 | 具体说明文档在HAChina中[开发ABC](https://www.hachina.io/docs/1891.html)。 4 | 5 | hachina**X**.py的内容,对应文档中第**X**课。 6 | -------------------------------------------------------------------------------- /program_train/hachina1.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名:hachina.py. 3 | 4 | 演示程序,三行代码创建一个新设备. 5 | """ 6 | 7 | 8 | def setup(hass, config): 9 | """HomeAssistant在配置文件中发现hachina域的配置后,会自动调用hachina.py文件中的setup函数.""" 10 | # 设置实体hachina.Hello_World的状态。 11 | # 注意1:实体并不需要被创建,只要设置了实体的状态,实体就自然存在了 12 | # 注意2:实体的状态可以是任何字符串 13 | hass.states.set("hachina.hello_world", "太棒了!") 14 | 15 | # 返回True代表初始化成功 16 | return True 17 | -------------------------------------------------------------------------------- /program_train/hachina2.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名 hachina.py. 3 | 4 | 演示程序,增加设备的属性值. 5 | """ 6 | 7 | # HomeAssistant的惯例,会在组件程序中定义域,域与组件程序名相同 8 | DOMAIN = "hachina" 9 | 10 | 11 | def setup(hass, config): 12 | """配置文件加载后,被调用的程序.""" 13 | # 准备一些属性值,在给实体设置状态的同时,设置实体的这些属性 14 | attr = {"icon": "mdi:yin-yang", 15 | "friendly_name": "迎接新世界", 16 | "slogon": "积木构建智慧空间!"} 17 | 18 | # 使用了在程序开头预定义的域 19 | # 设置状态的同时,设置实体的属性 20 | hass.states.set(DOMAIN+".hello_world", "太棒了!", attributes=attr) 21 | return True 22 | -------------------------------------------------------------------------------- /program_train/hachina3.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名 hachina.py. 3 | 4 | 演示程序,注册一个服务. 5 | """ 6 | 7 | # 引入记录日志的库 8 | import logging 9 | 10 | DOMAIN = "hachina" 11 | ENTITYID = DOMAIN + ".hello_world" 12 | 13 | # 在python中,__name__代表模块名字 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | def setup(hass, config): 18 | """配置文件加载后,setup被系统调用.""" 19 | attr = {"icon": "mdi:yin-yang", 20 | "friendly_name": "迎接新世界", 21 | "slogon": "积木构建智慧空间!", } 22 | hass.states.set(ENTITYID, '太棒了', attributes=attr) 23 | 24 | def change_state(call): 25 | """change_state函数切换改变实体的状态.""" 26 | # 记录info级别的日志 27 | _LOGGER.info("hachina's change_state service is called.") 28 | 29 | # 切换改变状态值 30 | if hass.states.get(ENTITYID).state == '太棒了': 31 | hass.states.set(ENTITYID, '真好', attributes=attr) 32 | else: 33 | hass.states.set(ENTITYID, '太棒了', attributes=attr) 34 | 35 | # 注册服务hachina.change_state 36 | hass.services.register(DOMAIN, 'change_state', change_state) 37 | 38 | return True 39 | -------------------------------------------------------------------------------- /program_train/hachina4.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名 hachina.py. 3 | 4 | 演示程序,读取配置文件的内容. 5 | """ 6 | 7 | import logging 8 | # 引入这两个库,用于配置文件格式校验 9 | import voluptuous as vol 10 | import homeassistant.helpers.config_validation as cv 11 | 12 | DOMAIN = "hachina" 13 | ENTITYID = DOMAIN + ".hello_world" 14 | 15 | # 预定义配置文件中的key值 16 | CONF_NAME_TOBE_DISPLAYED = "name_tobe_displayed" 17 | CONF_SLOGON = "slogon" 18 | 19 | # 预定义缺省的配置值 20 | DEFAULT_SLOGON = "积木构建智慧空间!" 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | # 配置文件的样式 25 | CONFIG_SCHEMA = vol.Schema( 26 | { 27 | DOMAIN: vol.Schema( 28 | { 29 | # “name_tobe_displayed”在配置文件中是必须存在的(Required),否则报错,它的类型是字符串 30 | vol.Required(CONF_NAME_TOBE_DISPLAYED): cv.string, 31 | # “slogon”在配置文件中可以没有(Optional),如果没有缺省值为“积木构建智慧空间!”,它的类型是字符串 32 | vol.Optional(CONF_SLOGON, default=DEFAULT_SLOGON): cv.string, 33 | }), 34 | }, 35 | extra=vol.ALLOW_EXTRA) 36 | 37 | 38 | def setup(hass, config): 39 | """配置文件加载后,setup被系统调用.""" 40 | # config[DOMAIN]代表这个域下的配置信息 41 | conf = config[DOMAIN] 42 | # 获得具体配置项信息 43 | friendly_name = conf.get(CONF_NAME_TOBE_DISPLAYED) 44 | slogon = conf.get(CONF_SLOGON) 45 | 46 | _LOGGER.info("Get the configuration %s=%s; %s=%s", 47 | CONF_NAME_TOBE_DISPLAYED, friendly_name, 48 | CONF_SLOGON, slogon) 49 | 50 | # 根据配置内容设置属性值 51 | attr = {"icon": "mdi:yin-yang", 52 | "friendly_name": friendly_name, 53 | "slogon": slogon} 54 | hass.states.set(ENTITYID, '太棒了', attributes=attr) 55 | 56 | return True 57 | -------------------------------------------------------------------------------- /program_train/hachina5.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名 hachina.py. 3 | 4 | 演示程序,事件触发状态值的改变. 5 | """ 6 | 7 | # 引入datetime库用于方便时间相关计算 8 | from datetime import timedelta 9 | import logging 10 | import voluptuous as vol 11 | 12 | # 引入HomeAssitant中定义的一些类与函数 13 | # track_time_interval是监听时间变化事件的一个函数 14 | from homeassistant.helpers.event import track_time_interval 15 | import homeassistant.helpers.config_validation as cv 16 | 17 | 18 | DOMAIN = "hachina" 19 | ENTITYID = DOMAIN + ".hello_world" 20 | 21 | CONF_STEP = "step" 22 | DEFAULT_STEP = 3 23 | 24 | # 定义时间间隔为3秒钟 25 | TIME_BETWEEN_UPDATES = timedelta(seconds=3) 26 | 27 | _LOGGER = logging.getLogger(__name__) 28 | 29 | CONFIG_SCHEMA = vol.Schema( 30 | { 31 | DOMAIN: vol.Schema( 32 | { 33 | # 一个配置参数“step”,只能是正整数,缺省值为3 34 | vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, 35 | }), 36 | }, 37 | extra=vol.ALLOW_EXTRA) 38 | 39 | 40 | def setup(hass, config): 41 | """配置文件加载后,setup被系统调用.""" 42 | conf = config[DOMAIN] 43 | step = conf.get(CONF_STEP) 44 | 45 | _LOGGER.info("Get the configuration %s=%d", 46 | CONF_STEP, step) 47 | 48 | attr = {"icon": "mdi:yin-yang", 49 | "friendly_name": "迎接新世界", 50 | "slogon": "积木构建智慧空间!", 51 | "unit_of_measurement": "steps"} 52 | 53 | # 构建类GrowingState 54 | GrowingState(hass, step, attr) 55 | 56 | return True 57 | 58 | 59 | class GrowingState(object): 60 | """定义一个类,此类中存储了状态与属性值,并定时更新状态.""" 61 | 62 | def __init__(self, hass, step, attr): 63 | """GrwoingState类的初始化函数,参数为hass、step和attr.""" 64 | # 定义类中的一些数据 65 | self._hass = hass 66 | self._step = step 67 | self._attr = attr 68 | self._state = 0 69 | 70 | # 在类初始化的时候,设置初始状态 71 | self._hass.states.set(ENTITYID, self._state, attributes=self._attr) 72 | 73 | # 每隔一段时间,更新一下实体的状态 74 | track_time_interval(self._hass, self.update, TIME_BETWEEN_UPDATES) 75 | 76 | def update(self, now): 77 | """在GrowingState类中定义函数update,更新状态.""" 78 | _LOGGER.info("GrowingState is updating…") 79 | 80 | # 状态值每次增加step 81 | self._state = self._state + self._step 82 | 83 | # 设置新的状态值 84 | self._hass.states.set(ENTITYID, self._state, attributes=self._attr) 85 | -------------------------------------------------------------------------------- /program_train/hachina6.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名:hachina.py. 3 | 4 | 文件位置:HomeAssistant配置目录/custom_components/sensor/hachina.py 5 | 演示程序,在sensor下创建一个新platform. 6 | 7 | """ 8 | # 引入一个产生随机数的库 9 | from random import randint 10 | import logging 11 | 12 | # 在homeassistant.const中定义了一些常量,我们在程序中会用到 13 | from homeassistant.const import ( 14 | ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, TEMP_CELSIUS) 15 | from homeassistant.helpers.entity import Entity 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | # 定义实体的OBJECT_ID与一些属性值 20 | OBJECT_ID = "hachina_temperature" 21 | ICON = "mdi:yin-yang" 22 | ATTRIBUTION = "随机显示的温度" 23 | FRIENDLY_NAME = "温度" 24 | 25 | 26 | def setup_platform(hass, config, add_devices, discovery_info=None): 27 | """配置文件在sensor域下出现hachina平台时,会自动调用sensor目录下hachina.py中的setup_platform函数.""" 28 | _LOGGER.info("setup platform sensor.hachina...") 29 | 30 | # 定义一个设备组,在其中装入了一个我们定义的设备HAChinaTemperatureSensor 31 | dev = [] 32 | sensor = HAChinaTemperatureSensor() 33 | dev.append(sensor) 34 | 35 | # 将设备加载入系统中 36 | add_devices(dev, True) 37 | 38 | 39 | class HAChinaTemperatureSensor(Entity): 40 | """定义一个温度传感器的类,继承自HomeAssistant的Entity类.""" 41 | 42 | def __init__(self): 43 | """初始化,状态值为空.""" 44 | self._state = None 45 | 46 | @property 47 | def name(self): 48 | """返回实体的名字。通过python装饰器@property,使访问更自然(方法变成属性调用,可以直接使用xxx.name).""" 49 | return OBJECT_ID 50 | 51 | @property 52 | def state(self): 53 | """返回当前的状态.""" 54 | return self._state 55 | 56 | @property 57 | def icon(self): 58 | """返回icon属性.""" 59 | return ICON 60 | 61 | @property 62 | def unit_of_measurement(self): 63 | """返回unit_of_measuremeng属性.""" 64 | return TEMP_CELSIUS 65 | 66 | @property 67 | def device_state_attributes(self): 68 | """设置其它一些属性值.""" 69 | if self._state is not None: 70 | return { 71 | ATTR_ATTRIBUTION: ATTRIBUTION, 72 | ATTR_FRIENDLY_NAME: FRIENDLY_NAME, 73 | } 74 | 75 | def update(self): 76 | """更新函数,在sensor组件下系统会定时自动调用(时间间隔在配置文件中可以调整,缺省为30秒).""" 77 | _LOGGER.info("Update the state...") 78 | self._state = randint(-100, 100) 79 | -------------------------------------------------------------------------------- /program_train/hachina7.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名:hachina.py. 3 | 4 | 文件位置:HomeAssistant配置目录/custom_components/sensor/hachina.py 5 | 演示程序,构建一个真正的温度传感器. 6 | 7 | """ 8 | 9 | # 因为京东万象的数据是以http方式提供的json数据,所以引入一些处理的库 10 | import json 11 | from urllib import request, parse 12 | 13 | import logging 14 | import voluptuous as vol 15 | 16 | # 引入sensor下的PLATFORM_SCHEMA 17 | from homeassistant.components.sensor import PLATFORM_SCHEMA 18 | 19 | from homeassistant.const import ( 20 | ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, TEMP_CELSIUS) 21 | from homeassistant.helpers.entity import Entity 22 | import homeassistant.helpers.config_validation as cv 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | # 配置文件中平台下的两个配置项 27 | CONF_CITY = "city" 28 | CONF_APPKEY = "appkey" 29 | 30 | ATTR_UPDATE_TIME = "更新时间" 31 | 32 | OBJECT_ID = "hachina_temperature" 33 | ICON = "mdi:thermometer" 34 | ATTRIBUTION = "来自京东万象的天气数据" 35 | FRIENDLY_NAME = "当前室外温度" 36 | 37 | # 扩展基础的SCHEMA。在我们这个platform上,城市与京东万象的APPKEY是获得温度必须要配置的项 38 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 39 | vol.Required(CONF_CITY): cv.string, 40 | vol.Required(CONF_APPKEY): cv.string, 41 | }) 42 | 43 | 44 | def setup_platform(hass, config, add_devices, discovery_info=None): 45 | """根据配置文件,setup_platform函数会自动被系统调用.""" 46 | _LOGGER.info("setup platform sensor.hachina...") 47 | 48 | # config仅仅包含配置文件中此平台下的内容 49 | # 以城市与appkey作为输入参数,初始化需要的传感器对象 50 | sensor = HAChinaTemperatureSensor( 51 | config.get(CONF_CITY), 52 | config.get(CONF_APPKEY)) 53 | 54 | dev = [] 55 | dev.append(sensor) 56 | 57 | add_devices(dev, True) 58 | 59 | 60 | class HAChinaTemperatureSensor(Entity): 61 | """定义一个温度传感器的类,继承自HomeAssistant的Entity类.""" 62 | 63 | def __init__(self, city, appkey): 64 | """初始化.""" 65 | self._state = None 66 | self._updatetime = None 67 | 68 | # 组装访问京东万象api需要的一些信息 69 | self._url = "https://way.jd.com/he/freeweather" 70 | self._params = {"city": city, 71 | "appkey": appkey, } 72 | 73 | @property 74 | def name(self): 75 | """返回实体的名字.""" 76 | return OBJECT_ID 77 | 78 | @property 79 | def state(self): 80 | """返回当前的状态.""" 81 | return self._state 82 | 83 | @property 84 | def icon(self): 85 | """返回icon属性.""" 86 | return ICON 87 | 88 | @property 89 | def unit_of_measurement(self): 90 | """返回unit_of_measuremeng属性.""" 91 | return TEMP_CELSIUS 92 | 93 | @property 94 | def device_state_attributes(self): 95 | """设置其它一些属性值.""" 96 | if self._state is not None: 97 | return { 98 | ATTR_ATTRIBUTION: ATTRIBUTION, 99 | ATTR_FRIENDLY_NAME: FRIENDLY_NAME, 100 | # 增加updatetime作为属性,表示温度数据的时间 101 | ATTR_UPDATE_TIME: self._updatetime 102 | } 103 | 104 | def update(self): 105 | """更新函数,在sensor组件下系统会定时自动调用(时间间隔在配置文件中可以调整,缺省为30秒).""" 106 | # update更新_state和_updatetime两个变量 107 | _LOGGER.info("Update the state...") 108 | 109 | # 通过HTTP访问,获取需要的信息 110 | infomation_file = request.urlopen( 111 | self._url, 112 | parse.urlencode(self._params).encode('utf-8')) 113 | 114 | content = infomation_file.read() 115 | result = json.loads(content.decode('utf-8')) 116 | 117 | if result is None: 118 | _LOGGER.error("Request api Error") 119 | return 120 | elif result["code"] != "10000": 121 | _LOGGER.error("Error API return, code=%s, msg=%s", 122 | result["code"], 123 | result["msg"]) 124 | return 125 | 126 | # 根据http返回的结果,更新_state和_updatetime 127 | all_result = result["result"]["HeWeather5"][0] 128 | self._state = all_result["now"]["tmp"] 129 | self._updatetime = all_result["basic"]["update"]["loc"] 130 | -------------------------------------------------------------------------------- /program_train/hachina8.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名:hachina.py. 3 | 4 | 文件位置:HomeAssistant配置目录/custom_components/sensor/hachina.py 5 | 演示程序,一个平台实现多个传感器. 6 | 7 | """ 8 | 9 | import json 10 | from urllib import request, parse 11 | import logging 12 | from datetime import timedelta 13 | import voluptuous as vol 14 | 15 | from homeassistant.components.sensor import PLATFORM_SCHEMA 16 | from homeassistant.const import ( 17 | ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, TEMP_CELSIUS) 18 | from homeassistant.helpers.entity import Entity 19 | import homeassistant.helpers.config_validation as cv 20 | from homeassistant.helpers.event import track_time_interval 21 | import homeassistant.util.dt as dt_util 22 | 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | TIME_BETWEEN_UPDATES = timedelta(seconds=600) 27 | 28 | # 配置文件中三个配置项的名称 29 | CONF_OPTIONS = "options" 30 | CONF_CITY = "city" 31 | CONF_APPKEY = "appkey" 32 | 33 | # 定义三个可选项:温度、湿度、PM2.5 34 | # 格式:配置项名称:[OBJECT_ID, friendly_name, icon, 单位] 35 | OPTIONS = { 36 | "temprature": [ 37 | "hachina_temperature", "室外温度", "mdi:thermometer", TEMP_CELSIUS], 38 | "humidity": ["hachina_humidity", "室外湿度", "mdi:water-percent", "%"], 39 | "pm25": ["hachina_pm25", "PM2.5", "mdi:walk", "μg/m3"], 40 | } 41 | 42 | ATTR_UPDATE_TIME = "更新时间" 43 | ATTRIBUTION = "来自京东万象的天气数据" 44 | 45 | 46 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 47 | vol.Required(CONF_CITY): cv.string, 48 | vol.Required(CONF_APPKEY): cv.string, 49 | # 配置项的options是一个列表,列表内容只能是OPTIONS中定义的三个可选项 50 | vol.Required(CONF_OPTIONS, 51 | default=[]): vol.All(cv.ensure_list, [vol.In(OPTIONS)]), 52 | }) 53 | 54 | 55 | def setup_platform(hass, config, add_devices, discovery_info=None): 56 | """根据配置文件,setup_platform函数会自动被系统调用.""" 57 | _LOGGER.info("setup platform sensor.hachina...") 58 | 59 | city = config.get(CONF_CITY) 60 | appkey = config.get(CONF_APPKEY) 61 | 62 | # 定义一个新的数据对象,用于从京东万象获取并存储天气数据。Sensor的实际数据从这个对象中获得。 63 | data = WeatherData(hass, city, appkey) 64 | 65 | # 根据配置文件options中的内容,添加若干个设备 66 | dev = [] 67 | for option in config[CONF_OPTIONS]: 68 | dev.append(HAChinaWeatherSensor(data, option)) 69 | add_devices(dev, True) 70 | 71 | 72 | class HAChinaWeatherSensor(Entity): 73 | """定义一个温度传感器的类,继承自HomeAssistant的Entity类.""" 74 | 75 | def __init__(self, data, option): 76 | """初始化.""" 77 | self._data = data 78 | self._object_id = OPTIONS[option][0] 79 | self._friendly_name = OPTIONS[option][1] 80 | self._icon = OPTIONS[option][2] 81 | self._unit_of_measurement = OPTIONS[option][3] 82 | 83 | self._type = option 84 | self._state = None 85 | self._updatetime = None 86 | 87 | @property 88 | def name(self): 89 | """返回实体的名字.""" 90 | return self._object_id 91 | 92 | @property 93 | def state(self): 94 | """返回当前的状态.""" 95 | return self._state 96 | 97 | @property 98 | def icon(self): 99 | """返回icon属性.""" 100 | return self._icon 101 | 102 | @property 103 | def unit_of_measurement(self): 104 | """返回unit_of_measuremeng属性.""" 105 | return self._unit_of_measurement 106 | 107 | @property 108 | def device_state_attributes(self): 109 | """设置其它一些属性值.""" 110 | if self._state is not None: 111 | return { 112 | ATTR_ATTRIBUTION: ATTRIBUTION, 113 | ATTR_FRIENDLY_NAME: self._friendly_name, 114 | ATTR_UPDATE_TIME: self._updatetime 115 | } 116 | 117 | def update(self): 118 | """更新函数,在sensor组件下系统会定时自动调用(时间间隔在配置文件中可以调整,缺省为30秒).""" 119 | # update只是从WeatherData中获得数据,数据由WeatherData维护。 120 | self._updatetime = self._data.updatetime 121 | 122 | if self._type == "temprature": 123 | self._state = self._data.temprature 124 | elif self._type == "humidity": 125 | self._state = self._data.humidity 126 | elif self._type == "pm25": 127 | self._state = self._data.pm25 128 | 129 | 130 | class WeatherData(object): 131 | """天气相关的数据,存储在这个类中.""" 132 | 133 | def __init__(self, hass, city, appkey): 134 | """初始化函数.""" 135 | self._url = "https://way.jd.com/he/freeweather" 136 | self._params = {"city": city, 137 | "appkey": appkey} 138 | self._temprature = None 139 | self._humidity = None 140 | self._pm25 = None 141 | self._updatetime = None 142 | 143 | self.update(dt_util.now()) 144 | # 每隔TIME_BETWEEN_UPDATES,调用一次update(),从京东万象获取数据 145 | track_time_interval(hass, self.update, TIME_BETWEEN_UPDATES) 146 | 147 | @property 148 | def temprature(self): 149 | """温度.""" 150 | return self._temprature 151 | 152 | @property 153 | def humidity(self): 154 | """湿度.""" 155 | return self._humidity 156 | 157 | @property 158 | def pm25(self): 159 | """pm2.5.""" 160 | return self._pm25 161 | 162 | @property 163 | def updatetime(self): 164 | """更新时间.""" 165 | return self._updatetime 166 | 167 | def update(self, now): 168 | """从远程更新信息.""" 169 | _LOGGER.info("Update from JingdongWangxiang's OpenAPI...") 170 | 171 | # 通过HTTP访问,获取需要的信息 172 | infomation_file = request.urlopen( 173 | self._url, parse.urlencode(self._params).encode('utf-8')) 174 | 175 | content = infomation_file.read() 176 | result = json.loads(content.decode('utf-8')) 177 | 178 | if result is None: 179 | _LOGGER.error("Request api Error") 180 | return 181 | elif result["code"] != "10000": 182 | _LOGGER.error("Error API return, code=%s, msg=%s", 183 | result["code"], 184 | result["msg"]) 185 | return 186 | 187 | # 根据http返回的结果,更新数据 188 | all_result = result["result"]["HeWeather5"][0] 189 | self._temprature = all_result["now"]["tmp"] 190 | self._humidity = all_result["now"]["hum"] 191 | self._pm25 = all_result["aqi"]["city"]["pm25"] 192 | self._updatetime = all_result["basic"]["update"]["loc"] 193 | -------------------------------------------------------------------------------- /program_train/hachina9.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文件名:hachina.py. 3 | 4 | 文件位置:HomeAssistant配置目录/custom_components/sensor/hachina.py 5 | 演示程序,异步工作的平台程序. 6 | 7 | """ 8 | 9 | import logging 10 | from datetime import timedelta 11 | 12 | # 此处引入了几个异步处理的库 13 | import asyncio 14 | import async_timeout 15 | import aiohttp 16 | 17 | import voluptuous as vol 18 | 19 | # aiohttp_client将aiohttp的session与hass关联起来 20 | # track_time_interval需要使用对应的异步的版本 21 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 22 | from homeassistant.helpers.event import async_track_time_interval 23 | 24 | from homeassistant.components.sensor import PLATFORM_SCHEMA 25 | from homeassistant.const import ( 26 | ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, TEMP_CELSIUS) 27 | from homeassistant.helpers.entity import Entity 28 | import homeassistant.helpers.config_validation as cv 29 | import homeassistant.util.dt as dt_util 30 | 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | TIME_BETWEEN_UPDATES = timedelta(seconds=600) 35 | 36 | CONF_OPTIONS = "options" 37 | CONF_CITY = "city" 38 | CONF_APPKEY = "appkey" 39 | 40 | # 定义三个可选项:温度、湿度、PM2.5 41 | OPTIONS = { 42 | "temprature": [ 43 | "hachina_temperature", "室外温度", "mdi:thermometer", TEMP_CELSIUS], 44 | "humidity": ["hachina_humidity", "室外湿度", "mdi:water-percent", "%"], 45 | "pm25": ["hachina_pm25", "PM2.5", "mdi:walk", "μg/m3"], 46 | } 47 | 48 | ATTR_UPDATE_TIME = "更新时间" 49 | ATTRIBUTION = "来自京东万象的天气数据" 50 | 51 | 52 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 53 | vol.Required(CONF_CITY): cv.string, 54 | vol.Required(CONF_APPKEY): cv.string, 55 | vol.Required(CONF_OPTIONS, 56 | default=[]): vol.All(cv.ensure_list, [vol.In(OPTIONS)]), 57 | }) 58 | 59 | 60 | @asyncio.coroutine 61 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 62 | """这个协程是程序的入口,其中add_devices函数也变成了异步版本.""" 63 | _LOGGER.info("setup platform sensor.hachina...") 64 | 65 | city = config.get(CONF_CITY) 66 | appkey = config.get(CONF_APPKEY) 67 | 68 | data = WeatherData(hass, city, appkey) 69 | # 大家可以尝试把yield from去除,看看是什么效果(参见知识点小结) 70 | yield from data.async_update(dt_util.now()) 71 | async_track_time_interval(hass, data.async_update, TIME_BETWEEN_UPDATES) 72 | 73 | # 根据配置文件options中的内容,添加若干个设备 74 | dev = [] 75 | for option in config[CONF_OPTIONS]: 76 | dev.append(HAChinaWeatherSensor(data, option)) 77 | async_add_devices(dev, True) 78 | 79 | 80 | class HAChinaWeatherSensor(Entity): 81 | """定义一个温度传感器的类,继承自HomeAssistant的Entity类.""" 82 | 83 | def __init__(self, data, option): 84 | """初始化.""" 85 | self._data = data 86 | self._object_id = OPTIONS[option][0] 87 | self._friendly_name = OPTIONS[option][1] 88 | self._icon = OPTIONS[option][2] 89 | self._unit_of_measurement = OPTIONS[option][3] 90 | 91 | self._type = option 92 | self._state = None 93 | self._updatetime = None 94 | 95 | @property 96 | def name(self): 97 | """返回实体的名字.""" 98 | return self._object_id 99 | 100 | @property 101 | def state(self): 102 | """返回当前的状态.""" 103 | return self._state 104 | 105 | @property 106 | def icon(self): 107 | """返回icon属性.""" 108 | return self._icon 109 | 110 | @property 111 | def unit_of_measurement(self): 112 | """返回unit_of_measuremeng属性.""" 113 | return self._unit_of_measurement 114 | 115 | @property 116 | def device_state_attributes(self): 117 | """设置其它一些属性值.""" 118 | if self._state is not None: 119 | return { 120 | ATTR_ATTRIBUTION: ATTRIBUTION, 121 | ATTR_FRIENDLY_NAME: self._friendly_name, 122 | ATTR_UPDATE_TIME: self._updatetime 123 | } 124 | 125 | @asyncio.coroutine 126 | def async_update(self): 127 | """update函数变成了async_update.""" 128 | self._updatetime = self._data.updatetime 129 | 130 | if self._type == "temprature": 131 | self._state = self._data.temprature 132 | elif self._type == "humidity": 133 | self._state = self._data.humidity 134 | elif self._type == "pm25": 135 | self._state = self._data.pm25 136 | 137 | 138 | class WeatherData(object): 139 | """天气相关的数据,存储在这个类中.""" 140 | 141 | def __init__(self, hass, city, appkey): 142 | """初始化函数.""" 143 | self._hass = hass 144 | 145 | self._url = "https://way.jd.com/he/freeweather" 146 | self._params = {"city": city, 147 | "appkey": appkey} 148 | self._temprature = None 149 | self._humidity = None 150 | self._pm25 = None 151 | self._updatetime = None 152 | 153 | @property 154 | def temprature(self): 155 | """温度.""" 156 | return self._temprature 157 | 158 | @property 159 | def humidity(self): 160 | """湿度.""" 161 | return self._humidity 162 | 163 | @property 164 | def pm25(self): 165 | """pm2.5.""" 166 | return self._pm25 167 | 168 | @property 169 | def updatetime(self): 170 | """更新时间.""" 171 | return self._updatetime 172 | 173 | @asyncio.coroutine 174 | def async_update(self, now): 175 | """从远程更新信息.""" 176 | _LOGGER.info("Update from JingdongWangxiang's OpenAPI...") 177 | 178 | """ 179 | # 异步模式的测试代码 180 | import time 181 | _LOGGER.info("before time.sleep") 182 | time.sleep(40) 183 | _LOGGER.info("after time.sleep and before asyncio.sleep") 184 | asyncio.sleep(40) 185 | _LOGGER.info("after asyncio.sleep and before yield from asyncio.sleep") 186 | yield from asyncio.sleep(40) 187 | _LOGGER.info("after yield from asyncio.sleep") 188 | """ 189 | 190 | # 通过HTTP访问,获取需要的信息 191 | # 此处使用了基于aiohttp库的async_get_clientsession 192 | try: 193 | session = async_get_clientsession(self._hass) 194 | with async_timeout.timeout(15, loop=self._hass.loop): 195 | response = yield from session.post( 196 | self._url, data=self._params) 197 | 198 | except(asyncio.TimeoutError, aiohttp.ClientError): 199 | _LOGGER.error("Error while accessing: %s", self._url) 200 | return 201 | 202 | if response.status != 200: 203 | _LOGGER.error("Error while accessing: %s, status=%d", 204 | self._url, 205 | response.status) 206 | return 207 | 208 | try: 209 | result = yield from response.json() 210 | except(aiohttp.client_exceptions.ContentTypeError): 211 | _LOGGER.error("Error return type: %s", str(response)) 212 | return 213 | 214 | if result is None: 215 | _LOGGER.error("Request api Error") 216 | return 217 | elif result["code"] != "10000": 218 | _LOGGER.error("Error API return, code=%s, msg=%s", 219 | result["code"], 220 | result["msg"]) 221 | return 222 | 223 | # 根据http返回的结果,更新数据 224 | all_result = result["result"]["HeWeather5"][0] 225 | self._temprature = all_result["now"]["tmp"] 226 | self._humidity = all_result["now"]["hum"] 227 | self._pm25 = all_result["aqi"]["city"]["pm25"] 228 | self._updatetime = all_result["basic"]["update"]["loc"] 229 | -------------------------------------------------------------------------------- /pulseaudio/README.md: -------------------------------------------------------------------------------- 1 | **最新的HomeAssitant core已经不支持本组件** 2 | 3 | 本组件连接PulseAudio服务,进行声音播放 4 | 5 | 本组件可以在前端`集成`菜单中配置(推荐),也可以在`configuration.yaml`文件中配置: 6 | 7 | ```yaml 8 | # configuration.yaml样例 9 | media_player: 10 | - platform: pulseaudio 11 | name: xxxxxx 12 | sink: alsa_output.1.stereo-fallback 13 | ``` 14 | 15 | 可配置项: 16 | - **name** (*可选项*): media_player的实体名,缺省值为`Pulse Audio Speaker` 17 | - **sink** (*可选项*): PulseAudio服务中音频输出设备,可以通过命令`pactl list sinks`查看系统中所有的sink。sink缺省为`default`,表示使用系统的缺省音频输出设备。 18 | 19 | ## 在hassio中配置蓝牙音箱 20 | 21 | 1. [进入主操作系统](https://developers.home-assistant.io/docs/operating-system/debugging) 22 | 23 | 2. 蓝牙音箱连接,运行命令`bluetoothctl` 24 | 25 | ``` 26 | scan on 27 | pair 28 | trust 29 | connect [enter your MAC add] 30 | discoverable on 31 | pairable on 32 | default-agent 33 | ``` 34 | 35 | 3. 在前端集成中配置,或者在`configuration.yaml`中配置。如果在`configuration.yaml`中配置,sink的名称可以通过命令`docker exec homeassistant pactl list sinks`查看 36 | 37 | 注:如果你不是在HASSOS上运行hassio-supervisor,你可能需要运行以下命令,去除Host操作系统上的`pulseaudio`和`bluealsa`,因为它们会首先截获蓝牙设备连接信息。 38 | ``` 39 | sudo apt-get remove pulseaudio 40 | sudo apt-get remove bluealsa 41 | sudo reboot 42 | ``` 43 | -------------------------------------------------------------------------------- /pulseaudio/__init__.py: -------------------------------------------------------------------------------- 1 | """The Pulse Audio integration.""" 2 | import asyncio 3 | 4 | import voluptuous as vol 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.const import CONF_NAME 9 | 10 | from .const import DOMAIN, CONF_SINK 11 | 12 | PLATFORMS = ["media_player"] 13 | 14 | 15 | async def async_setup(hass: HomeAssistant, config: dict): 16 | """Set up the Pulse Audio component.""" 17 | return True 18 | 19 | 20 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 21 | """Set up Pulse Audio from a config entry.""" 22 | name = entry.data[CONF_NAME] 23 | sink = entry.data[CONF_SINK] 24 | 25 | hass.data.setdefault(DOMAIN, {}) 26 | hass.data[DOMAIN][entry.entry_id] = { 27 | CONF_NAME: name, 28 | CONF_SINK: sink, 29 | } 30 | 31 | for component in PLATFORMS: 32 | hass.async_create_task( 33 | hass.config_entries.async_forward_entry_setup(entry, component) 34 | ) 35 | 36 | return True 37 | 38 | 39 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 40 | """Unload a config entry.""" 41 | unload_ok = all( 42 | await asyncio.gather( 43 | *[ 44 | hass.config_entries.async_forward_entry_unload(entry, component) 45 | for component in PLATFORMS 46 | ] 47 | ) 48 | ) 49 | if unload_ok: 50 | hass.data[DOMAIN].pop(entry.entry_id) 51 | 52 | return unload_ok 53 | -------------------------------------------------------------------------------- /pulseaudio/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Pulse Audio integration.""" 2 | import logging 3 | import asyncio 4 | import shlex 5 | import voluptuous as vol 6 | from subprocess import PIPE, Popen 7 | 8 | from homeassistant import config_entries, core, exceptions 9 | from homeassistant.const import CONF_NAME 10 | 11 | from .const import DOMAIN, CONF_SINK # pylint:disable=unused-import 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | async def get_sinks(): 16 | cmd = "pactl list short sinks" 17 | proc = await asyncio.create_subprocess_shell( 18 | cmd, 19 | stdout=asyncio.subprocess.PIPE, 20 | stderr=asyncio.subprocess.PIPE) 21 | stdout, _ = await proc.communicate() 22 | return {shlex.split(d)[1]:shlex.split(d)[0]+":"+shlex.split(d)[1] for d in stdout.decode().splitlines()} 23 | 24 | async def validate_input(hass: core.HomeAssistant, data): 25 | """Validate the user input allows us to connect. 26 | 27 | Data has the keys from DATA_SCHEMA with values provided by the user. 28 | """ 29 | 30 | sinks = await get_sinks() 31 | 32 | if data[CONF_SINK] not in sinks: 33 | raise InvalidSinkID 34 | 35 | # Return info that you want to store in the config entry. 36 | return {"title": "PulseAudio: "+data[CONF_SINK]} 37 | 38 | 39 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 40 | """Handle a config flow for Pulse Audio.""" 41 | 42 | VERSION = 1 43 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 44 | 45 | async def async_step_user(self, user_input=None): 46 | """Handle the initial step.""" 47 | errors = {} 48 | if user_input is not None: 49 | try: 50 | info = await validate_input(self.hass, user_input) 51 | 52 | return self.async_create_entry(title=info["title"], data=user_input) 53 | except InvalidSinkID: 54 | errors["base"] = "invalid_sink_id" 55 | except Exception: # pylint: disable=broad-except 56 | _LOGGER.exception("Unexpected exception") 57 | errors["base"] = "unknown" 58 | 59 | sinks = await get_sinks() 60 | #sinks = {sink.id:sink.name+':'+sink.id for sink in all_speakers()} 61 | DATA_SCHEMA = vol.Schema( 62 | {vol.Required(CONF_NAME, default="PulseAudio Speaker"): str, 63 | vol.Required(CONF_SINK): vol.In(sinks)} 64 | ) 65 | return self.async_show_form( 66 | step_id="user", data_schema=DATA_SCHEMA, errors=errors 67 | ) 68 | 69 | class InvalidSinkID(exceptions.HomeAssistantError): 70 | """Error to indicate there is invalid sink ID.""" 71 | -------------------------------------------------------------------------------- /pulseaudio/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Pulse Audio integration.""" 2 | 3 | DOMAIN = "pulseaudio" 4 | 5 | CONF_SINK = 'sink' 6 | DEFAULT_NAME = 'Pulse Audio Speaker' 7 | DEFAULT_SINK = 'default' -------------------------------------------------------------------------------- /pulseaudio/ffmpeg2pa.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | from subprocess import PIPE, Popen 3 | 4 | class AudioPlay(object): 5 | """ 6 | Wraps a binary ffmpeg and pacat executable 7 | """ 8 | def __init__(self, ffmpeg_exe_file, device_option, volume=65536): 9 | self._FfmpegProc = self._PacatProc = None 10 | self._FfmpegCmd = "%s -hide_banner -loglevel panic -i %s -acodec pcm_s16le -f s16le -ac 1 -ar 16k -" %(ffmpeg_exe_file, '%s') 11 | self._PacatCmd = "pacat --format=s16le --rate=16000 --channels=1 %s %s" %(device_option, '%s') 12 | self._volume = volume 13 | 14 | def play(self, audiofile): 15 | volume_option = "--volume=%d" %(self._volume) 16 | FfmpegArgv = shlex.split(str(self._FfmpegCmd % (audiofile))) 17 | PacatArgv = shlex.split(str(self._PacatCmd) % (volume_option)) 18 | 19 | if self._FfmpegProc is not None and self._FfmpegProc.poll() is None: 20 | self._FfmpegProc.terminate() 21 | if self._PacatProc is not None and self._PacatProc.poll() is None: 22 | self._PacatProc.terminate() 23 | 24 | self._FfmpegProc = Popen(FfmpegArgv, stdin=PIPE, stdout=PIPE) 25 | self._PacatProc = Popen(PacatArgv, stdin=self._FfmpegProc.stdout) 26 | 27 | def stop(self): 28 | if self._FfmpegProc is not None and self._FfmpegProc.poll() is None: 29 | self._FfmpegProc.stdin.write(b'q') 30 | self._FfmpegProc.stdin.flush() 31 | 32 | def set_volume(self, volume): 33 | self._volume = volume 34 | 35 | @property 36 | def volume(self): 37 | return self._volume 38 | 39 | @property 40 | def is_running(self): 41 | return self._PacatProc is not None and self._PacatProc.poll() is None 42 | -------------------------------------------------------------------------------- /pulseaudio/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "pulseaudio", 3 | "name": "Pulse Audio", 4 | "version": "1.1.0", 5 | "config_flow": true, 6 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/pulseaudio", 7 | "requirements": [], 8 | "ssdp": [], 9 | "zeroconf": [], 10 | "homekit": {}, 11 | "dependencies": ["ffmpeg"], 12 | "codeowners": [ 13 | "@zhujisheng" 14 | ] 15 | } -------------------------------------------------------------------------------- /pulseaudio/media_player.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for PulseAudio speakers(sinks) 3 | 4 | """ 5 | from __future__ import annotations 6 | import voluptuous as vol 7 | import logging 8 | import asyncio 9 | 10 | from homeassistant.components.media_player.const import ( 11 | MEDIA_TYPE_MUSIC, 12 | SUPPORT_BROWSE_MEDIA, 13 | SUPPORT_PLAY, 14 | SUPPORT_PLAY_MEDIA, 15 | SUPPORT_VOLUME_SET, 16 | ) 17 | 18 | from homeassistant.components.media_player import ( 19 | BrowseMedia, 20 | async_process_play_media_url, 21 | MediaPlayerEntity, 22 | PLATFORM_SCHEMA) 23 | from homeassistant.const import ( 24 | CONF_NAME, STATE_IDLE, STATE_PLAYING) 25 | import homeassistant.helpers.config_validation as cv 26 | from homeassistant.components.ffmpeg import DATA_FFMPEG 27 | from homeassistant.components import media_source 28 | 29 | from .ffmpeg2pa import AudioPlay 30 | 31 | from .const import ( 32 | DOMAIN, 33 | CONF_SINK, 34 | DEFAULT_NAME, 35 | DEFAULT_SINK, 36 | ) 37 | 38 | SUPPORT_PULSEAUDIO = ( 39 | SUPPORT_PLAY 40 | | SUPPORT_PLAY_MEDIA 41 | | SUPPORT_VOLUME_SET 42 | | SUPPORT_BROWSE_MEDIA 43 | ) 44 | 45 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 46 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 47 | vol.Optional(CONF_SINK, default=DEFAULT_SINK): cv.string, 48 | }) 49 | 50 | _LOGGER = logging.getLogger(__name__) 51 | 52 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 53 | """Setup the Pulse Audio Speaker platform.""" 54 | name = config.get(CONF_NAME) 55 | sink = config.get(CONF_SINK) 56 | 57 | async_add_entities([PulseAudioSpeaker(hass, name, sink)]) 58 | return True 59 | 60 | 61 | async def async_setup_entry(hass, config_entry, async_add_entities): 62 | """Add Pulse Audio entities from a config_entry.""" 63 | name = config_entry.data[CONF_NAME] 64 | sink = config_entry.data[CONF_SINK] 65 | 66 | async_add_entities([PulseAudioSpeaker(hass, name, sink)]) 67 | 68 | 69 | class PulseAudioSpeaker(MediaPlayerEntity): 70 | """Representation of a Pulse Audio Speaker local.""" 71 | 72 | def __init__(self, hass, name, sink): 73 | """Initialize the device.""" 74 | 75 | self._hass = hass 76 | self._name = name 77 | self._sink = sink 78 | self._state = STATE_IDLE 79 | self._volume = 1.0 80 | if(sink == DEFAULT_SINK): 81 | device_option = "" 82 | else: 83 | device_option = "--device=%s"%(sink) 84 | self._AudioPlayer = AudioPlay(hass.data[DATA_FFMPEG].binary, device_option) 85 | 86 | @property 87 | def name(self): 88 | """Return the name of the device.""" 89 | return self._name 90 | 91 | @property 92 | def unique_id(self): 93 | """Return the unique ID of the device.""" 94 | return self._sink 95 | 96 | @property 97 | def state(self): 98 | """Return the state of the device.""" 99 | return self._state 100 | 101 | @property 102 | def volume_level(self): 103 | """Volume level of the media player (0..1).""" 104 | return self._volume 105 | 106 | @property 107 | def supported_features(self): 108 | """Flag media player features that are supported.""" 109 | return SUPPORT_PULSEAUDIO 110 | 111 | def play_media(self, media_type, media_id, **kwargs): 112 | """Send play commmand.""" 113 | 114 | if media_source.is_media_source_id(media_id): 115 | sourced_media = asyncio.run_coroutine_threadsafe( 116 | media_source.async_resolve_media(self._hass, media_id), 117 | self._hass.loop 118 | ).result() 119 | media_type = sourced_media.mime_type 120 | media_id = sourced_media.url 121 | 122 | if media_type != MEDIA_TYPE_MUSIC and not media_type.startswith("audio/"): 123 | raise HomeAssistantError( 124 | f"Invalid media type {media_type}. Only {MEDIA_TYPE_MUSIC} is supported" 125 | ) 126 | 127 | # If media ID is a relative URL, we serve it from HA. 128 | media_id = async_process_play_media_url( 129 | self._hass, media_id 130 | ) 131 | 132 | _LOGGER.info('play_media: %s', media_id) 133 | self._AudioPlayer.play(media_id) 134 | self._state = STATE_PLAYING 135 | self.schedule_update_ha_state() 136 | 137 | def set_volume_level(self, volume): 138 | """Set volume level, range 0..1.""" 139 | self._AudioPlayer.set_volume(int(volume * 65536)) 140 | self._volume = volume 141 | self.schedule_update_ha_state() 142 | 143 | def media_stop(self): 144 | """Send stop command.""" 145 | self._AudioPlayer.stop() 146 | self._state = STATE_IDLE 147 | self.schedule_update_ha_state() 148 | 149 | def update(self): 150 | """Get the latest details from the device.""" 151 | if self._AudioPlayer.is_running: 152 | self._state = STATE_PLAYING 153 | else: 154 | self._state = STATE_IDLE 155 | self._volume = self._AudioPlayer.volume/65536.0 156 | return True 157 | 158 | async def async_browse_media( 159 | self, media_content_type: str | None = None, media_content_id: str | None = None 160 | ) -> BrowseMedia: 161 | """Implement the websocket media browsing helper.""" 162 | return await media_source.async_browse_media( 163 | self.hass, 164 | media_content_id, 165 | content_filter=lambda item: item.media_content_type.startswith("audio/"), 166 | ) -------------------------------------------------------------------------------- /pulseaudio/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Pulse Audio MediaPlayer", 6 | "description": "Set up A MediaPlayer based on the local Pulse Audio service. If you have problems with configuration go to: https://www.home-assistant.io/integrations/pulseaudio\n", 7 | "data": { 8 | "name": "The name to use in the frontend.", 9 | "sink": "The output device id in PulseAudio." 10 | } 11 | } 12 | }, 13 | "error": { 14 | "invalid_sink_id": "Invalid Sink ID", 15 | "unknown": "[%key:common::config_flow::error::unknown%]" 16 | }, 17 | "abort": { 18 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /pulseaudio/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 5 | }, 6 | "error": { 7 | "invalid_sink_id": "Invalid Sink ID", 8 | "unknown": "[%key:common::config_flow::error::unknown%]" 9 | }, 10 | "step": { 11 | "user": { 12 | "title": "Pulse Audio MediaPlayer", 13 | "description": "Set up A MediaPlayer based on the local Pulse Audio service. If you have problems with configuration go to: https://www.home-assistant.io/integrations/pulseaudio\n", 14 | "data": { 15 | "name": "The name to use in the frontend.", 16 | "sink": "The output device id in PulseAudio." 17 | } 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /scrape2/README.md: -------------------------------------------------------------------------------- 1 | *本组件使用chrome浏览器访问页面获得内容后,使用beautiful soap selector获得其中的元素。* 2 | 3 | ## 配置 4 | 5 | *可以参见官方的[scrape集成](https://www.home-assistant.io/integrations/scrape/)的配置;两者配置方式相同,仅获取网页的方式不同* 6 | 7 | ```yaml 8 | # configuration.yaml样例 9 | sensor: 10 | - platform: scrape2 11 | name: HA最新版本号 12 | resource: https://www.home-assistant.io 13 | select: ".current-version h1" 14 | value_template: '{{ value.split(":")[1] }}' 15 | ``` 16 | 17 | ## 自定义组件安装 18 | 19 | - `sensor.py` `__init__.py` `manifest.json`文件放置在`.homeassistant/custom_components/scrape2/`目录中 20 | - 安装chromedriver和chrome浏览器 21 | 22 | 23 | ## 安装chromedriver和chrome浏览器 24 | 25 | - 以docker方式运行的HomeAssistant(包括hassio和hassos方式安装的HomeAssistant) 26 | 27 | 进入homeassistant docker后,运行 28 | 29 | `apk add chromium-chromedriver` 30 | 31 | `apk add chromium` 32 | 33 | 如果是在中国境内,可以先运行以下命令,设置apk安装国内镜像: 34 | 35 | `sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories` 36 | 37 | 38 | - 树莓派上非docker方式运行的HomeAssistant 39 | 40 | [树莓派对应下载地址](https://launchpad.net/ubuntu/trusty/armhf/chromium-chromedriver/65.0.3325.181-0ubuntu0.14.04.1) 41 | 42 | 下载后运行: 43 | 44 | `sudo dpkg -i chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb` 45 | -------------------------------------------------------------------------------- /scrape2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhujisheng/HAComponent/88284c58de6e768b07fffa169098e99c3439c644/scrape2/__init__.py -------------------------------------------------------------------------------- /scrape2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "scrape2", 3 | "name": "scrape2", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/scrape2", 5 | "requirements": ["beautifulsoup4==4.9.3", "selenium==3.141.0"], 6 | "dependencies": [], 7 | "codeowners": [], 8 | "version": "1.0.0" 9 | } -------------------------------------------------------------------------------- /scrape2/sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import voluptuous as vol 5 | 6 | from selenium import webdriver 7 | from selenium.webdriver.chrome.options import Options 8 | from bs4 import BeautifulSoup 9 | 10 | from homeassistant.components.sensor import PLATFORM_SCHEMA 11 | from homeassistant.const import ( 12 | CONF_NAME, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, 13 | CONF_VALUE_TEMPLATE ) 14 | from homeassistant.helpers.entity import Entity 15 | import homeassistant.helpers.config_validation as cv 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | CONF_ATTR = 'attribute' 20 | CONF_SELECT = 'select' 21 | 22 | DEFAULT_NAME = 'Web scrape2' 23 | 24 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 25 | vol.Required(CONF_RESOURCE): cv.string, 26 | vol.Required(CONF_SELECT): cv.string, 27 | vol.Optional(CONF_ATTR): cv.string, 28 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 29 | vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, 30 | vol.Optional(CONF_VALUE_TEMPLATE): cv.template 31 | }) 32 | 33 | 34 | def setup_platform(hass, config, add_entities, discovery_info=None): 35 | """Set up the Web scrape sensor.""" 36 | name = config.get(CONF_NAME) 37 | resource = config.get(CONF_RESOURCE) 38 | select = config.get(CONF_SELECT) 39 | attr = config.get(CONF_ATTR) 40 | unit = config.get(CONF_UNIT_OF_MEASUREMENT) 41 | value_template = config.get(CONF_VALUE_TEMPLATE) 42 | if value_template is not None: 43 | value_template.hass = hass 44 | 45 | if os.path.exists("/usr/lib/chromium/chromedriver"): 46 | driver = "/usr/lib/chromium/chromedriver" 47 | elif os.path.exists("/usr/lib/chromium-browser/chromedriver"): 48 | driver = "/usr/lib/chromium-browser/chromedriver" 49 | else: 50 | _LOGGER.error("chromedriver hasn't been installed") 51 | return False 52 | 53 | add_entities([ 54 | Scrape2Sensor(resource, name, select, attr, value_template, unit, driver)], True) 55 | 56 | 57 | class Scrape2Sensor(Entity): 58 | """Representation of a web scrape sensor.""" 59 | 60 | def __init__(self, resource, name, select, attr, value_template, unit, driver): 61 | """Initialize a web scrape sensor.""" 62 | self._resource = resource 63 | self._name = name 64 | self._state = None 65 | self._select = select 66 | self._attr = attr 67 | self._value_template = value_template 68 | self._unit_of_measurement = unit 69 | self._driver = driver 70 | 71 | @property 72 | def name(self): 73 | """Return the name of the sensor.""" 74 | return self._name 75 | 76 | @property 77 | def unit_of_measurement(self): 78 | """Return the unit the value is expressed in.""" 79 | return self._unit_of_measurement 80 | 81 | @property 82 | def state(self): 83 | """Return the state of the device.""" 84 | return self._state 85 | 86 | def update(self): 87 | """Get the latest data from the source and updates the state.""" 88 | 89 | chrome_options = Options() 90 | chrome_options.add_argument('--headless') 91 | chrome_options.add_argument("--no-sandbox") 92 | chrome_options.add_argument("--disable-dev-shm-using") 93 | driver = webdriver.Chrome(self._driver, options=chrome_options) 94 | driver.get( self._resource) 95 | innerHTML = driver.execute_script("return document.body.innerHTML") 96 | driver.quit() 97 | 98 | raw_data = BeautifulSoup(innerHTML, 'html.parser') 99 | _LOGGER.debug(raw_data) 100 | 101 | try: 102 | if self._attr is not None: 103 | value = raw_data.select(self._select)[0][self._attr] 104 | else: 105 | value = raw_data.select(self._select)[0].text 106 | _LOGGER.debug(value) 107 | except IndexError: 108 | _LOGGER.error("Unable to extract data from HTML") 109 | return 110 | 111 | if self._value_template is not None: 112 | self._state = self._value_template.render_with_possible_json_value( 113 | value, None) 114 | else: 115 | self._state = value 116 | -------------------------------------------------------------------------------- /tunnel2local/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 说明: 3 | - 本组件使用[frp](https://github.com/fatedier/frp)作为建立隧道的工具 4 | - 本组件基于域名hachina.802154.com的http虚拟主机头,实现在INTERNET上访问homeassistant开放的端口 5 | - 自己搭建frp服务器端,也可以使用本组件,以tcp转发方式实现内网homeassistant的外网访问 6 | 7 | 8 | ## 下载frp: 9 | https://github.com/fatedier/frp/releases 10 | 11 | 找到您homeassistant所在的操作系统,下载对应的文件。 12 | 13 | **我们仅需要其中的frpc程序。** 14 | 15 | **缺省的服务器端为0.32.1版。如果你使用缺省服务器,请下载对应客户端版本;如果你自己搭建服务器端,服务器端与客户端版本一致即可** 16 | 17 | **缺省服务器端仅供测试使用,流量较大,网络质量不好;建议自己搭建服务器端** 18 | 19 | 例如:如果是树莓派,如果要使用0.18版本,对应文件为`frp_0.18.0_linux_arm.tar.gz`,解压缩后获得frpc文件(可能需要增加可执行权限`chmod +x frpc`),在下面的配置文件中配置其地址。 20 | 21 | 22 | ## 配置HomeAssistant: 23 | - 在`~/.homeassistant/custom_components/`目录下构建子目录`tunnel2local` 24 | - 下载文件`__init__.py`与`manifest.json`,放置在目录`tunnel2local`中 25 | - 配置文件: 26 | 27 | ```yaml 28 | tunnel2local: 29 | # frpc命令位置 30 | frpc_bin: "C:/local/frp_0.32.1_windows_amd64/frpc.exe" 31 | 32 | ``` 33 | ## (可选)搭建自己的frp服务器 34 | 如果您选择搭建自己的frp服务器,参见:[server_diy.md](server_diy.md) 35 | -------------------------------------------------------------------------------- /tunnel2local/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | For more details about HAChina, 4 | https://www.hachina.io/ 5 | """ 6 | import asyncio 7 | import logging 8 | import os 9 | import uuid 10 | import base64 11 | import hashlib 12 | 13 | import voluptuous as vol 14 | from homeassistant.core import Event 15 | from homeassistant.helpers import config_validation as cv 16 | from homeassistant.const import EVENT_HOMEASSISTANT_STOP 17 | from homeassistant.helpers.event import async_call_later 18 | 19 | 20 | DOMAIN = 'tunnel2local' 21 | DATA_TUNNEL2LOCAL = 'tunnel2local' 22 | 23 | 24 | CONF_FRPS = "frps" 25 | CONF_FRPS_PORT = "frps_port" 26 | CONF_FRPC_BIN = "frpc_bin" 27 | CONF_TOKEN = "frp_token" 28 | CONF_REMOTE_PORT = "remote_port" 29 | CONF_SUBDOMAIN = "subdomain" 30 | 31 | DEFAULT_FRPS_PORT = 7000 32 | DEFAULT_FRPC_BIN = "frpc" 33 | DEFAULT_TOKEN = "" 34 | DEFAULT_REMOTE_PORT = 8123 35 | 36 | _LOGGER = logging.getLogger(__name__) 37 | 38 | CONFIG_SCHEMA = vol.Schema( 39 | { 40 | DOMAIN: vol.Schema( 41 | { 42 | vol.Optional(CONF_FRPS): cv.string, 43 | vol.Optional(CONF_FRPS_PORT, default=DEFAULT_FRPS_PORT): cv.port, 44 | vol.Optional(CONF_FRPC_BIN, default=DEFAULT_FRPC_BIN): cv.string, 45 | vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN): cv.string, 46 | vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port, 47 | vol.Optional(CONF_SUBDOMAIN): cv.string, 48 | }), 49 | }, 50 | extra=vol.ALLOW_EXTRA) 51 | 52 | 53 | @asyncio.coroutine 54 | def async_setup(hass, config): 55 | """Set up the component.""" 56 | conf = config[DOMAIN] 57 | 58 | port_local = hass.config.api.port 59 | command = conf.get(CONF_FRPC_BIN) 60 | 61 | subdomain = conf.get(CONF_SUBDOMAIN) 62 | if subdomain is None: 63 | mid1 = uuid.getnode() 64 | mid2 = mid1.to_bytes((mid1.bit_length() + 7) // 8, byteorder='big') 65 | mid3 = base64.b32encode(mid2) 66 | subdomain = mid3.decode().rstrip('=').lower() 67 | 68 | if conf.get(CONF_FRPS) is None: 69 | host = "hachinafrps.duckdns.org" 70 | port = 7000 71 | token = "welcome2ha" 72 | subdomain_host = "hachina.802154.com" 73 | 74 | h = hashlib.md5() 75 | h.update(bytes(token,encoding='utf-8')) 76 | h.update(b'\xe4\xb3\xad\xe1\x96\x37') 77 | h.update(bytes("hachina",encoding='utf-8')) 78 | token = h.hexdigest() 79 | 80 | url = "http://%s.%s"%(subdomain, subdomain_host) 81 | 82 | run_cmd = [command, 83 | 'http', 84 | '-s', "%s:%d"%(host,port), 85 | '--sd', subdomain, 86 | '-d', subdomain, 87 | '-i', get_local_ip(), 88 | '-l', str(port_local), 89 | '-t', token, 90 | '-n', 'hachina_'+subdomain, 91 | '--locations', '/', 92 | '--http_user', '', 93 | '--http_pwd', '', 94 | ] 95 | 96 | else: 97 | host = conf.get(CONF_FRPS) 98 | port = conf.get(CONF_FRPS_PORT) 99 | token = conf.get(CONF_TOKEN) 100 | remote_port = conf.get(CONF_REMOTE_PORT) 101 | url = "http://%s:%d"%(host, remote_port) 102 | 103 | run_cmd = [command, 104 | 'tcp', 105 | '-s', "%s:%d"%(host,port), 106 | '-i', get_local_ip(), 107 | '-l', str(port_local), 108 | '-t', token, 109 | '-r', str(remote_port), 110 | '-n', 'hachina_'+subdomain, 111 | ] 112 | 113 | try: 114 | process = yield from run2(run_cmd) 115 | except: 116 | _LOGGER.error("Can't start %s", run_cmd[0]) 117 | return False 118 | 119 | hass.data[DATA_TUNNEL2LOCAL] = process 120 | 121 | _LOGGER.info("tunnel2local started, hass can be visited from internet - %s", url) 122 | 123 | hass.states.async_set("sensor.tunnel2local", 124 | url, 125 | attributes={"icon": "mdi:router-wireless", 126 | "friendly_name": "外网访问地址"} 127 | ) 128 | 129 | def probe_frpc(now): 130 | if(process.returncode): 131 | _LOGGER.error("frpc exited, returncode: %d", process.returncode ) 132 | else: 133 | _LOGGER.info("frpc pid: %d", process.pid ) 134 | 135 | async_call_later(hass, 60, probe_frpc) 136 | 137 | def stop_frpc(event: Event): 138 | """Stop frpc process.""" 139 | hass.data[DATA_TUNNEL2LOCAL].terminate() 140 | 141 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_frpc) 142 | 143 | return True 144 | 145 | # Taken from: http://stackoverflow.com/a/11735897 146 | def get_local_ip(): 147 | import socket 148 | """Try to determine the local IP address of the machine.""" 149 | try: 150 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 151 | 152 | # Use Google Public DNS server to determine own IP 153 | sock.connect(('8.8.8.8', 80)) 154 | 155 | return sock.getsockname()[0] 156 | except socket.error: 157 | try: 158 | return socket.gethostbyname(socket.gethostname()) 159 | except socket.gaierror: 160 | return '127.0.0.1' 161 | finally: 162 | sock.close() 163 | 164 | 165 | @asyncio.coroutine 166 | def run2(frpc_command): 167 | #_LOGGER.error(frpc_command) 168 | 169 | p = yield from asyncio.create_subprocess_exec(*frpc_command, stdout=asyncio.subprocess.PIPE, 170 | stderr=asyncio.subprocess.PIPE, 171 | stdin=asyncio.subprocess.PIPE) 172 | return p 173 | -------------------------------------------------------------------------------- /tunnel2local/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "tunnel2local", 3 | "name": "tunnel2local", 4 | "documentation": "https://github.com/zhujisheng/HAComponent/tree/master/tunnel2local", 5 | "requirements": [], 6 | "dependencies": [], 7 | "codeowners": [] 8 | } -------------------------------------------------------------------------------- /tunnel2local/server_diy.md: -------------------------------------------------------------------------------- 1 | 如果您不想使用组件中现成的公网隧道,可以使用frp自己搭建服务器端——前提条件是:您有一台公网能直接访问到的服务器(云主机)。 2 | 3 | 4 | ## 服务器端搭建 5 | 使用下载的frp包中的frps程序,在服务器上运行。配置文件`frps.ini`如下: 6 | ```ini 7 | [common] 8 | bind_port = 7000 9 | token = 12345678 10 | ``` 11 | 其余的配置项可以参见frps项目的配置说明 12 | 13 | ## HomeAssistant配置 14 | - 如[readme](https://github.com/zhujisheng/HAComponent/tree/master/tunnel2local)中所述,安放`__init__.py`和`manifest`文件 15 | - 配置文件: 16 | ```yaml 17 | tunnel2local: 18 | # frpc命令位置 19 | frpc_bin: "C:/local/frp_0.32.1_windows_amd64/frpc.exe" 20 | frps: 1.2.3.4 #服务器地址 21 | frps_port: 7000 #缺省值为7000 22 | frp_token: "12345678" #缺省值为空 23 | remote_port: 8123 #缺省值为8123 24 | 25 | ``` 26 | --------------------------------------------------------------------------------