emhass 0.10.6__py3-none-any.whl → 0.15.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ import asyncio
2
+ import logging
3
+ import ssl
4
+ import time
5
+ import urllib.parse as urlparse
6
+ from typing import Any
7
+
8
+ import orjson
9
+ import websockets
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class WebSocketError(Exception):
15
+ pass
16
+
17
+
18
+ class AuthenticationError(WebSocketError):
19
+ pass
20
+
21
+
22
+ class ConnectionError(WebSocketError):
23
+ pass
24
+
25
+
26
+ class RequestError(WebSocketError):
27
+ def __init__(self, code: str, message: str):
28
+ self.code = code
29
+ self.message = message
30
+ super().__init__(f"{code}: {message}")
31
+
32
+
33
+ class AsyncWebSocketClient:
34
+ def __init__(
35
+ self,
36
+ hass_url: str,
37
+ long_lived_token: str,
38
+ logger: logging.Logger | None = None,
39
+ ping_interval: int = 30,
40
+ reconnect_delay: int = 5,
41
+ max_reconnect_attempts: int = 5,
42
+ ):
43
+ self.hass_url = hass_url
44
+ self.token = long_lived_token.strip()
45
+ self.logger = logger or logging.getLogger("AsyncWebSocketClient")
46
+ self.ping_interval = ping_interval
47
+ self.reconnect_delay = reconnect_delay
48
+ self.max_reconnect_attempts = max_reconnect_attempts
49
+
50
+ self._ws = None
51
+ self._connected = False
52
+ self._authenticated = False
53
+ self._id = 0
54
+ self._pending: dict[int, asyncio.Future] = {}
55
+ self._lock = asyncio.Lock()
56
+ self._ping_task: asyncio.Task | None = None
57
+ self._recv_task: asyncio.Task | None = None
58
+ self._reconnects = 0
59
+
60
+ @property
61
+ def websocket_url(self) -> str:
62
+ # For supervisor API, use the dedicated websocket endpoint
63
+ if self.hass_url.startswith("http://supervisor/core/api"):
64
+ return "ws://supervisor/core/websocket"
65
+ elif self.hass_url.startswith("https://supervisor/core/api"):
66
+ return "wss://supervisor/core/websocket"
67
+ else:
68
+ # Standard Home Assistant instance
69
+ parsed = urlparse.urlparse(self.hass_url)
70
+ ws_scheme = "wss" if parsed.scheme == "https" else "ws"
71
+ return f"{ws_scheme}://{parsed.netloc}/api/websocket"
72
+
73
+ @property
74
+ def connected(self) -> bool:
75
+ return self._connected and self._authenticated
76
+
77
+ def _get_ssl_context(self) -> ssl.SSLContext | None:
78
+ if self.websocket_url.startswith("wss://"):
79
+ return True # Use Python's secure defaults
80
+ return None
81
+
82
+ def _next_id(self) -> int:
83
+ self._id += 1
84
+ return self._id
85
+
86
+ async def startup(self):
87
+ """Connect and authenticate."""
88
+ await self._connect()
89
+
90
+ async def shutdown(self):
91
+ """Cleanly close connection."""
92
+ async with self._lock:
93
+ await self._cleanup()
94
+
95
+ async def reconnect(self):
96
+ """Force a reconnect."""
97
+ async with self._lock:
98
+ await self._cleanup()
99
+ await asyncio.sleep(1)
100
+ try:
101
+ await asyncio.wait_for(self._connect(), timeout=10.0)
102
+ except TimeoutError as e:
103
+ self.logger.error("Reconnection timed out")
104
+ raise ConnectionError("Reconnection timed out") from e
105
+ except Exception as e:
106
+ self.logger.error(f"Reconnection failed: {e}")
107
+ raise
108
+
109
+ async def _connect(self):
110
+ """Internal connect/authenticate and start background tasks."""
111
+ ssl_ctx = self._get_ssl_context()
112
+ self._ws = await websockets.connect(
113
+ self.websocket_url, ssl=ssl_ctx, ping_interval=None, max_size=None
114
+ )
115
+ # Authenticate
116
+ msg = await asyncio.wait_for(self._ws.recv(), timeout=5.0)
117
+ data = orjson.loads(msg)
118
+ if data.get("type") != "auth_required":
119
+ raise AuthenticationError("No auth_required")
120
+ auth = orjson.dumps({"type": "auth", "access_token": self.token}).decode()
121
+ await self._ws.send(auth)
122
+ resp = orjson.loads(await asyncio.wait_for(self._ws.recv(), timeout=5.0))
123
+ if resp.get("type") != "auth_ok":
124
+ raise AuthenticationError("Invalid auth")
125
+ self._authenticated = True
126
+ self._connected = True
127
+ self._recv_task = asyncio.create_task(self._receiver())
128
+ self._ping_task = asyncio.create_task(self._ping_loop())
129
+ self._reconnects = 0
130
+ self.logger.info("Connected and authenticated")
131
+
132
+ async def _cleanup(self):
133
+ """Stop tasks and close ws."""
134
+ self._connected = False
135
+ self._authenticated = False
136
+ if self._ping_task:
137
+ self._ping_task.cancel()
138
+ if self._recv_task:
139
+ self._recv_task.cancel()
140
+ if self._ws:
141
+ await self._ws.close()
142
+ self._ws = None
143
+ for fut in self._pending.values():
144
+ if not fut.done():
145
+ fut.set_exception(ConnectionError("Connection closed"))
146
+ self._pending.clear()
147
+
148
+ async def _ping_loop(self):
149
+ """Keep alive ping loop."""
150
+ try:
151
+ while self.connected:
152
+ await asyncio.sleep(self.ping_interval)
153
+ await self._ws.send(orjson.dumps({"id": self._next_id(), "type": "ping"}).decode())
154
+ except Exception:
155
+ pass
156
+
157
+ async def _receiver(self):
158
+ """Receive messages and route to pending futures."""
159
+ try:
160
+ while self.connected:
161
+ msg = await self._ws.recv()
162
+ data = orjson.loads(msg)
163
+ # route by id
164
+ mid = data.get("id")
165
+ if mid and mid in self._pending:
166
+ fut = self._pending.pop(mid)
167
+ if data.get("type") == "result" and not data.get("success", True):
168
+ err = data.get("error", {})
169
+ fut.set_exception(RequestError(err.get("code", ""), err.get("message", "")))
170
+ else:
171
+ fut.set_result(data.get("result", None))
172
+ except Exception as e:
173
+ self.logger.error(f"Receiver error: {e}")
174
+ self._connected = False
175
+ # Cancel pending futures
176
+ for fut in self._pending.values():
177
+ if not fut.done():
178
+ fut.set_exception(ConnectionError("Connection lost"))
179
+ self._pending.clear()
180
+
181
+ async def send(self, msg_type: str, **kwargs) -> Any:
182
+ """Send a command and await response."""
183
+ async with self._lock:
184
+ if not self.connected:
185
+ await self.reconnect()
186
+ mid = self._next_id()
187
+ payload = {"id": mid, "type": msg_type, **kwargs}
188
+ fut = asyncio.get_event_loop().create_future()
189
+ self._pending[mid] = fut
190
+ await self._ws.send(orjson.dumps(payload).decode())
191
+ return await fut
192
+
193
+ # Convenience API methods:
194
+ async def get_config(self) -> dict[str, Any]:
195
+ return await self.send("get_config")
196
+
197
+ async def get_states(self) -> list[dict[str, Any]]:
198
+ return await self.send("get_states")
199
+
200
+ async def get_state(self, entity_id: str) -> dict[str, Any] | None:
201
+ states = await self.get_states()
202
+ return next((s for s in states if s["entity_id"] == entity_id), None)
203
+
204
+ async def call_service(
205
+ self, domain: str, service: str, service_data: dict = None, target: dict = None
206
+ ):
207
+ return await self.send(
208
+ "call_service",
209
+ domain=domain,
210
+ service=service,
211
+ service_data=service_data or {},
212
+ target=target or {},
213
+ )
214
+
215
+ async def get_history(self, **kwargs):
216
+ return await self.send("history/history_during_period", **kwargs)
217
+
218
+ async def get_statistics(self, **kwargs):
219
+ return await self.send("recorder/statistics_during_period", **kwargs)
220
+
221
+ async def ping_time(self) -> float:
222
+ start = time.perf_counter()
223
+ await self.send("ping")
224
+ return (time.perf_counter() - start) * 1000
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: emhass
3
+ Version: 0.15.5
4
+ Summary: An Energy Management System for Home Assistant
5
+ Project-URL: Homepage, https://github.com/davidusb-geek/emhass
6
+ Project-URL: Source, https://github.com/davidusb-geek/emhass
7
+ Project-URL: Issues, https://github.com/davidusb-geek/emhass/issues
8
+ Project-URL: Documentation, https://emhass.readthedocs.io/en/latest/
9
+ Project-URL: Community, https://community.home-assistant.io/t/emhass-an-energy-management-for-home-assistant
10
+ Author-email: David HERNANDEZ TORRES <davidusb@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: energy,hass,management,optimization
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: <3.13,>=3.10
24
+ Requires-Dist: aiofiles
25
+ Requires-Dist: aiohttp
26
+ Requires-Dist: asyncio
27
+ Requires-Dist: gunicorn>=23.0.0
28
+ Requires-Dist: h5py>=3.12.1
29
+ Requires-Dist: highspy>=1.10.0
30
+ Requires-Dist: influxdb>=5.3.1
31
+ Requires-Dist: jinja2
32
+ Requires-Dist: numpy<2.3.0,>=2.0.0
33
+ Requires-Dist: orjson
34
+ Requires-Dist: pandas>=2.2.0
35
+ Requires-Dist: plotly>=6.0.0
36
+ Requires-Dist: protobuf>=5.29.3
37
+ Requires-Dist: pulp>=2.8.0
38
+ Requires-Dist: pvlib>=0.11.0
39
+ Requires-Dist: pytz>=2024.2
40
+ Requires-Dist: pyyaml>=6.0.2
41
+ Requires-Dist: quart
42
+ Requires-Dist: scipy>=1.15.0
43
+ Requires-Dist: skforecast<0.20.0,>=0.19.1
44
+ Requires-Dist: tables>=3.10.0
45
+ Requires-Dist: uvicorn>=0.30.0
46
+ Requires-Dist: waitress>=3.0.2
47
+ Requires-Dist: websockets
48
+ Provides-Extra: dev
49
+ Requires-Dist: ruff; extra == 'dev'
50
+ Provides-Extra: docs
51
+ Requires-Dist: myst-parser; extra == 'docs'
52
+ Requires-Dist: pydata-sphinx-theme; extra == 'docs'
53
+ Requires-Dist: sphinx; extra == 'docs'
54
+ Requires-Dist: sphinx-design; extra == 'docs'
55
+ Provides-Extra: test
56
+ Requires-Dist: aioresponses; extra == 'test'
57
+ Requires-Dist: coverage; extra == 'test'
58
+ Requires-Dist: hatchling; extra == 'test'
59
+ Requires-Dist: pytest; extra == 'test'
60
+ Requires-Dist: pytz; extra == 'test'
61
+ Requires-Dist: ruff; extra == 'test'
62
+ Requires-Dist: snakeviz; extra == 'test'
63
+ Requires-Dist: tabulate; extra == 'test'
64
+ Description-Content-Type: text/markdown
65
+
66
+ <div align="center">
67
+ <br>
68
+ <img alt="EMHASS" src="https://raw.githubusercontent.com/davidusb-geek/emhass/master/docs/images/logo_docs.png" width="700px">
69
+ <h1>Energy Management for Home Assistant</h1>
70
+ <strong></strong>
71
+ </div>
72
+ <br>
73
+
74
+ <p align="center">
75
+ <a style="text-decoration:none" href="https://pypi.org/project/emhass/">
76
+ <img alt="PyPI - Version" src="https://img.shields.io/pypi/v/emhass">
77
+ </a>
78
+ <a style="text-decoration:none" href="https://anaconda.org/channels/davidusb/packages/emhass/overview">
79
+ <img alt="Conda - Version" src="https://img.shields.io/conda/v/davidusb/emhass">
80
+ </a>
81
+ <a style="text-decoration:none" href="https://github.com/davidusb-geek/emhass/actions">
82
+ <img alt="EMHASS GitHub Workflow Status" src="https://github.com/davidusb-geek/emhass/actions/workflows/publish_docker.yaml/badge.svg?event=release">
83
+ </a>
84
+ <a style="text-decoration:none" href="https://github.com/davidusb-geek/emhass/blob/master/LICENSE">
85
+ <img alt="GitHub" src="https://img.shields.io/github/license/davidusb-geek/emhass">
86
+ </a>
87
+ <a style="text-decoration:none" href="https://pypi.org/project/emhass/">
88
+ <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/emhass">
89
+ </a>
90
+ <a style="text-decoration:none" href="https://pypi.org/project/emhass/">
91
+ <img alt="PyPI - Status" src="https://img.shields.io/pypi/status/emhass">
92
+ </a>
93
+ <a style="text-decoration:none" href="https://emhass.readthedocs.io/en/latest/">
94
+ <img alt="Read the Docs" src="https://img.shields.io/readthedocs/emhass">
95
+ </a>
96
+ <a hstyle="text-decoration:none" ref="https://codecov.io/github/davidusb-geek/emhass" >
97
+ <img src="https://codecov.io/github/davidusb-geek/emhass/branch/master/graph/badge.svg?token=BW7KSCHN90"/>
98
+ </a>
99
+ <a hstyle="text-decoration:none" ref="https://github.com/davidusb-geek/emhass/actions/workflows/codeql.yml" >
100
+ <img src="https://github.com/davidusb-geek/emhass/actions/workflows/codeql.yml/badge.svg?branch=master&event=schedule"/>
101
+ </a>
102
+ <a style="text-decoration:none" href="https://sonarcloud.io/summary/new_code?id=davidusb-geek_emhass">
103
+ <img alt="SonarQube security rating" src="https://sonarcloud.io/api/project_badges/measure?project=davidusb-geek_emhass&metric=security_rating">
104
+ </a>
105
+ <a style="text-decoration:none" href="https://sonarcloud.io/summary/new_code?id=davidusb-geek_emhass">
106
+ <img alt="SonarQube security Vulnerabilities" src="https://sonarcloud.io/api/project_badges/measure?project=davidusb-geek_emhass&metric=vulnerabilities">
107
+ </a>
108
+ <a style="text-decoration:none" href="https://sonarcloud.io/summary/new_code?id=davidusb-geek_emhass">
109
+ <img alt="SonarQube reliability" src="https://sonarcloud.io/api/project_badges/measure?project=davidusb-geek_emhass&metric=reliability_rating">
110
+ </a>
111
+ <a style="text-decoration:none" href="https://sonarcloud.io/summary/new_code?id=davidusb-geek_emhass">
112
+ <img alt="SonarQube bugs" src="https://sonarcloud.io/api/project_badges/measure?project=davidusb-geek_emhass&metric=bugs">
113
+ </a>
114
+
115
+ </p>
116
+
117
+ <div align="center">
118
+ <a style="text-decoration:none" href="https://emhass.readthedocs.io/en/latest/">
119
+ <img src="https://raw.githubusercontent.com/davidusb-geek/emhass/master/docs/images/Documentation_button.svg" alt="Documentation">
120
+ </a>
121
+ <a style="text-decoration:none" href="https://community.home-assistant.io/t/emhass-an-energy-management-for-home-assistant/338126">
122
+ <img src="https://raw.githubusercontent.com/davidusb-geek/emhass/master/docs/images/Community_button.svg" alt="Community">
123
+ </a>
124
+ <a style="text-decoration:none" href="https://github.com/davidusb-geek/emhass/issues">
125
+ <img src="https://raw.githubusercontent.com/davidusb-geek/emhass/master/docs/images/Issues_button.svg" alt="Issues">
126
+ </a>
127
+ <a style="text-decoration:none" href="https://github.com/davidusb-geek/emhass-add-on">
128
+ <img src="https://raw.githubusercontent.com/davidusb-geek/emhass/master/docs/images/EMHASS_Add_on_button.svg" alt="EMHASS Add-on">
129
+ </a>
130
+ </div>
131
+
132
+ <br>
133
+ <p align="left">
134
+ EMHASS is a Python module designed to optimize your home energy interfacing with Home Assistant.
135
+ </p>
136
+
137
+ ## Introduction
138
+
139
+ EMHASS (Energy Management for Home Assistant) is an optimization tool designed for residential households. The package uses a Linear Programming approach to optimize energy usage while considering factors such as electricity prices, power generation from solar panels, and energy storage from batteries. EMHASS provides a high degree of configurability, making it easy to integrate with Home Assistant and other smart home systems. Whether you have solar panels, energy storage, or just a controllable load, EMHASS can provide an optimized daily schedule for your devices, allowing you to save money and minimize your environmental impact.
140
+
141
+ The complete documentation for this package is [available here](https://emhass.readthedocs.io/en/latest/).
142
+
143
+ To get started you can follow our [🚀 Quick Start](/docs/quick_start.md) guide in the documentation.
144
+
145
+ Here are the guides for:
146
+ - [📦 Installation methods](/docs/installation_methods.md)
147
+ - [📖 Usage](/docs/usage_guide.md)
148
+ - [🤖 Home Assistant Automations](/docs/automations.md)
149
+
150
+ ## Development
151
+
152
+ Pull requests are very much accepted on this project. For development, you can find some instructions here [Development](/docs/develop.md).
153
+
154
+ ## License
155
+
156
+ MIT License
157
+
158
+ Copyright (c) 2021-2025 David HERNANDEZ
159
+
160
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
161
+
162
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
163
+
164
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,34 @@
1
+ emhass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ emhass/command_line.py,sha256=qW-PYDgnm1EYRXFihGwZCMZQOOqMer-L33W9e5hC_n8,92691
3
+ emhass/connection_manager.py,sha256=KDV46UrwSUwN_BhtemBvj3UA7940UaPXhqLuHyzFf8I,3611
4
+ emhass/forecast.py,sha256=Tv-LlmQZhdIz5R8hWiXPc4bAz0RWkmjjhRi5DdHbIwA,83220
5
+ emhass/machine_learning_forecaster.py,sha256=HazCCXOknBQ4y-P2kMubs3q9nycS2CIeY2AcC8RS744,28049
6
+ emhass/machine_learning_regressor.py,sha256=yUQ15kEx7RkSk65KqljSaKJjrBWxFl7QNeoR0MLCqwg,10868
7
+ emhass/optimization.py,sha256=m3fi8f9R4LkPgdjoLO8r-zI4N4gO9_v3zA2Hh-hrRx4,93773
8
+ emhass/retrieve_hass.py,sha256=83j9oB5SD78UeN2N2SPZopjyc9XHn0fmYokShNYgcuM,54637
9
+ emhass/utils.py,sha256=eiKk7M1LoJQUb5v9PPAQOs5QjjuYeCrOhgwSxyAAOs0,100809
10
+ emhass/web_server.py,sha256=QwLNhID23nIyqpY9yduDN43BsVKb_R82HMYGupnMabM,34928
11
+ emhass/websocket_client.py,sha256=FBw892hU_lzs2pi15ym96gIx8gjQ9iQbgeJwzI9AbuU,7830
12
+ emhass/data/associations.csv,sha256=rdtZcT1djB0zTXERnO3yIlPGVw5dMZpBssOzWqdWBwU,5521
13
+ emhass/data/cec_inverters.pbz2,sha256=KEo7IRiZNP_BwTAoj41h4IOX50mlFHAKahINtpQaoKE,189482
14
+ emhass/data/cec_modules.pbz2,sha256=b_2MX_pGsDkIe6oXZdfWGnXFw-IB9pWq2S9SR-uHz20,1885793
15
+ emhass/data/config_defaults.json,sha256=Wt_UZkgc3BaYFyDm1m5N2AvTrx5_lBnh-IjiVZ15w_Y,3846
16
+ emhass/img/emhass_icon.png,sha256=Kyx6hXQ1huJLHAq2CaBfjYXR25H9j99PSWHI0lShkaQ,19030
17
+ emhass/static/advanced.html,sha256=YtLZfvlUNZQwpbrMpA3OIYlc7jDAtd4RNUTijjlhmf8,2466
18
+ emhass/static/basic.html,sha256=ro2WwWgJyoUhqx_nJFzKCEG8FA8863vSHLmrjGYcEgs,677
19
+ emhass/static/configuration_list.html,sha256=ceVFzYnJaVmHQN0sqSNuYpHSfLTekWQc3M2A1aV8a44,1779
20
+ emhass/static/configuration_script.js,sha256=tE-gmfkZbUexi7d9h3b4A-KEwz2RLryIN__3xgjfztQ,33909
21
+ emhass/static/script.js,sha256=rahbdud8jd0MO5DF9dsVtZUzNw8T3aGHZ-aJg0e_TSY,17403
22
+ emhass/static/style.css,sha256=R5P2Mai_wmRJ8aYLtk9n9sKF-_DVngCN-Q7GF5muR-k,19261
23
+ emhass/static/data/param_definitions.json,sha256=IoyO9klhjPY2DRz-SjXi_MatBWPdkCR6uKqjg4o362s,28418
24
+ emhass/static/img/emhass_icon.png,sha256=Kyx6hXQ1huJLHAq2CaBfjYXR25H9j99PSWHI0lShkaQ,19030
25
+ emhass/static/img/emhass_logo_short.svg,sha256=yzMcqtBRCV8rH84-MwnigZh45_f9Eoqwho9P8nCodJA,66736
26
+ emhass/static/img/feather-sprite.svg,sha256=VHjMJQg88wXa9CaeYrKGhNtyK0xdd47zCqwSIa-hxo8,60319
27
+ emhass/templates/configuration.html,sha256=eieTNS8cn5tcxmFvSe2yRfo6Ad38YfIKi0H0F-c8yjY,2954
28
+ emhass/templates/index.html,sha256=bbaZ8jawK_3qtXKQBFOFHrvDGlJCR7T6NXpO_0rV2tA,3181
29
+ emhass/templates/template.html,sha256=llc1BIXZ-c2Yuwag0diP2Jtvf-dra55_yRZQ3S6qLw4,165
30
+ emhass-0.15.5.dist-info/METADATA,sha256=g12hQGW4rsPWMFWXwYUJkdDCQbyTM3IxTI-dm1WMyOQ,8905
31
+ emhass-0.15.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
+ emhass-0.15.5.dist-info/entry_points.txt,sha256=7gkK0C-EsZAmILX7EHqYnHHpMZ-5_8-wZFG79hJcYmk,57
33
+ emhass-0.15.5.dist-info/licenses/LICENSE,sha256=1X3-S1yvOCBDBeox1aK3dq00m7dA8NDtcPrpKPISzbE,1077
34
+ emhass-0.15.5.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ emhass = emhass.command_line:main_sync