├── 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 |
17 | - appcode(Required): AppCode from Aliyun.
18 | - symbols array(Optional): List of stock market symbols for given companies. If not specified, it defaults to sz000002 (万科A).
19 |
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 |
25 | - api_key(Required): 从高德开放平台申请获得的APIKEY。
26 | - name(Optional): 实体的名称,不能是中文,缺省值是gaode_travel_time。
27 | - friendly_name(Optional): 在界面上显示的名称,可以是中文。
28 | - travel_mode(Optional): 行程模式,可以是driving、walking或bicycling,缺省为driving。
29 | - strategy(Optional): 当travel_mode是driving时有效,支持0-9, 缺省为0(代表选择最快路径)。其余的选项可以参加高德开放平台上的描述。
30 | - origin(Required): 行程开始点。可以配置为longitude_latitude或者(city, address)。
31 | - destination(Required): 行程结束点。可以配置为longitude_latitude或者(city, address)。
32 | - longitude_latitude(Optional): 维度和经度信息,维度在前,经度在后,用逗号隔开。
33 | - city(Optional): 城市名
34 | - address(Optional): 具体的地址描述
35 |
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 |
65 | - api_key(Required): Key from Gaode.
66 | - name(Optional): Entity's ObjectID,the default is gaode_travel_time。
67 | - friendly_name(Optional): sensor's display name, can be Chinese
68 | - travel_mode(Optional): travel mode, support driving, walking and bicycling, default is driving
69 | - strategy(Optional): Used when the travel_mode is driving, support 0-9, default is 0, means choose the fast route.
70 | - origin(Required): The start address. You can configure with longitude_latitude or (city, address) imformation.
71 | - destination(Required): The destination address. You can configure with longitude_latitude or (city, address) imformation.
72 | - longitude_latitude(Optional): The longitude and latitudu of the address
73 | - city(Optional): The Chinese city name
74 | - address(Optional): The address in Chinese
75 |
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 | [](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 |
14 | - key(Required): 从聚合数据api申请获得的key.
15 |
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 |
33 | - key(Required): Key from Juhe.
34 |
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 |
14 | - key(Required): 从聚合数据api申请获得的key.
15 |
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 |
32 | - key(Required): Key from Juhe.
33 |
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 |
37 | - key(Required): Key from Juhe.
38 | - symbols array(Optional): List of stock market symbols for given companies. If not specified, it defaults to sz000002 (万科A).
39 |
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 |
--------------------------------------------------------------------------------