pyindrav2h 0.0.2__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.
- pyindrav2h/__init__.py +7 -0
- pyindrav2h/cli.py +75 -0
- pyindrav2h/connection.py +115 -0
- pyindrav2h/exceptions.py +27 -0
- pyindrav2h/v2hclient.py +23 -0
- pyindrav2h/v2hdevice.py +146 -0
- pyindrav2h-0.0.2.dist-info/LICENSE +21 -0
- pyindrav2h-0.0.2.dist-info/METADATA +121 -0
- pyindrav2h-0.0.2.dist-info/RECORD +12 -0
- pyindrav2h-0.0.2.dist-info/WHEEL +5 -0
- pyindrav2h-0.0.2.dist-info/entry_points.txt +2 -0
- pyindrav2h-0.0.2.dist-info/top_level.txt +1 -0
pyindrav2h/__init__.py
ADDED
pyindrav2h/cli.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from getpass import getpass
|
|
3
|
+
import argparse
|
|
4
|
+
import configparser
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from pyindrav2h.connection import Connection
|
|
8
|
+
from pyindrav2h.v2hclient import v2hClient
|
|
9
|
+
|
|
10
|
+
logging.basicConfig()
|
|
11
|
+
logging.root.setLevel(logging.WARNING)
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
async def main(args):
|
|
16
|
+
userEmail = args.email or input("Please enter your Indra login email: ")
|
|
17
|
+
userPass = args.password or getpass(prompt="Indra password: ")
|
|
18
|
+
|
|
19
|
+
if args.debug:
|
|
20
|
+
logging.root.setLevel(logging.DEBUG)
|
|
21
|
+
|
|
22
|
+
_LOGGER.debug(f"using {userEmail}, {userPass}")
|
|
23
|
+
|
|
24
|
+
# create connection
|
|
25
|
+
conn = Connection(userEmail, userPass)
|
|
26
|
+
await conn.checkAPICreds()
|
|
27
|
+
|
|
28
|
+
client = v2hClient(conn)
|
|
29
|
+
|
|
30
|
+
# refresh device/stats data
|
|
31
|
+
await client.refresh()
|
|
32
|
+
|
|
33
|
+
if (args.command == "device"):
|
|
34
|
+
print(client.device.showDevice())
|
|
35
|
+
elif (args.command == "statistics"):
|
|
36
|
+
print(client.device.showStats())
|
|
37
|
+
elif (args.command == "all"):
|
|
38
|
+
print(client.device.showAll())
|
|
39
|
+
elif (args.command == "loadmatch"):
|
|
40
|
+
print(await client.device.load_match())
|
|
41
|
+
elif (args.command == "idle"):
|
|
42
|
+
print(await client.device.idle())
|
|
43
|
+
elif (args.command == "schedule"):
|
|
44
|
+
print(await client.device.schedule())
|
|
45
|
+
|
|
46
|
+
def cli():
|
|
47
|
+
config = configparser.ConfigParser()
|
|
48
|
+
config["indra-account"] = {"email": "", "password": ""}
|
|
49
|
+
config.read([".indra.cfg", os.path.expanduser("~/.indra.cfg")])
|
|
50
|
+
parser = argparse.ArgumentParser(prog="indracli", description="Indra V2H CLI")
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"-u",
|
|
53
|
+
"--email",
|
|
54
|
+
dest="email",
|
|
55
|
+
default=config.get("indra-account", "email"),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"-p",
|
|
59
|
+
"--password",
|
|
60
|
+
dest="password",
|
|
61
|
+
default=config.get("indra-account", "password")
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
|
|
64
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
65
|
+
subparsers.add_parser("statistics", help="show device statistics")
|
|
66
|
+
subparsers.add_parser("device", help="show device info")
|
|
67
|
+
subparsers.add_parser("all", help="show all info")
|
|
68
|
+
subparsers.add_parser("loadmatch", help="set mode to load matching")
|
|
69
|
+
subparsers.add_parser("idle", help="set mode to IDLE")
|
|
70
|
+
subparsers.add_parser("schedule", help="return to scheuduled mode")
|
|
71
|
+
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
loop = asyncio.get_event_loop()
|
|
75
|
+
loop.run_until_complete(main(args))
|
pyindrav2h/connection.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import httpx
|
|
3
|
+
from typing import Text
|
|
4
|
+
from bs4 import BeautifulSoup as bs
|
|
5
|
+
|
|
6
|
+
from .exceptions import V2HException
|
|
7
|
+
from .exceptions import WrongCredentialsException
|
|
8
|
+
from .exceptions import TimeoutException
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
loginUrl = "https://smartportal.indra.co.uk"
|
|
13
|
+
apiBaseUrl = "https://api.indra.co.uk/api"
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
def __init__(
|
|
17
|
+
self, userEmail: Text = None, userPass: Text = None, timeout: int = 20
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Initialize connection object."""
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
self.email = userEmail
|
|
22
|
+
self.password = userPass
|
|
23
|
+
self._headers = {"User-Agent": "Wget/1.14 (linux-gnu)"}
|
|
24
|
+
self._xsrfToken = None
|
|
25
|
+
self._bearerToken = None
|
|
26
|
+
self._cookies = None
|
|
27
|
+
_LOGGER.debug("New connection created")
|
|
28
|
+
|
|
29
|
+
async def checkAPICreds(self):
|
|
30
|
+
self._xsrfToken = await self.getXsrf(loginUrl)
|
|
31
|
+
_LOGGER.debug(f"XSRF token: {self._xsrfToken}")
|
|
32
|
+
self._bearerToken = await self.getAuth("POST", "/login")
|
|
33
|
+
self._headers["Authorization"] = self._bearerToken
|
|
34
|
+
succ = {'success': 'True'}
|
|
35
|
+
if succ['success'] == False:
|
|
36
|
+
raise WrongCredentialsException()
|
|
37
|
+
|
|
38
|
+
async def send(self, method, url, json=None):
|
|
39
|
+
# params = {'api_key': self.email, 'api_secret': self.password}
|
|
40
|
+
async with httpx.AsyncClient(
|
|
41
|
+
headers=self._headers, timeout=self.timeout
|
|
42
|
+
) as httpclient:
|
|
43
|
+
# theUrl = apiBaseUrl + url
|
|
44
|
+
theUrl = url
|
|
45
|
+
try:
|
|
46
|
+
_LOGGER.debug(f"{method} {url} {theUrl}")
|
|
47
|
+
response = await httpclient.request(method, theUrl, json=json)
|
|
48
|
+
except httpx.ReadTimeout:
|
|
49
|
+
raise TimeoutException()
|
|
50
|
+
else:
|
|
51
|
+
if response.status_code == 200:
|
|
52
|
+
return response.json()
|
|
53
|
+
elif response.status_code == 202:
|
|
54
|
+
return True
|
|
55
|
+
elif response.status_code == 401:
|
|
56
|
+
raise WrongCredentialsException()
|
|
57
|
+
raise V2HException(response.status_code)
|
|
58
|
+
|
|
59
|
+
async def get(self, url, data=None):
|
|
60
|
+
url = apiBaseUrl + url
|
|
61
|
+
return await self.send("GET", url, data)
|
|
62
|
+
|
|
63
|
+
async def post(self, url, data=None):
|
|
64
|
+
url = apiBaseUrl + url
|
|
65
|
+
return await self.send("POST", url, data)
|
|
66
|
+
|
|
67
|
+
async def getXsrf(self, url, method = "GET"):
|
|
68
|
+
# loginResponse = await self.send("GET", url)
|
|
69
|
+
async with httpx.AsyncClient(
|
|
70
|
+
headers=self._headers, timeout=self.timeout
|
|
71
|
+
) as httpclient:
|
|
72
|
+
theUrl = url
|
|
73
|
+
try:
|
|
74
|
+
_LOGGER.debug(f"{method} {url} {theUrl}")
|
|
75
|
+
response = await httpclient.request(method, theUrl)
|
|
76
|
+
except httpx.ReadTimeout:
|
|
77
|
+
raise TimeoutException()
|
|
78
|
+
else:
|
|
79
|
+
if response.status_code == 200:
|
|
80
|
+
htmlBody = response.content
|
|
81
|
+
self._cookies = response.cookies
|
|
82
|
+
soup = bs(htmlBody, "lxml")
|
|
83
|
+
return soup.find('input', {"name": "__RequestVerificationToken"})['value']
|
|
84
|
+
elif response.status_code == 401:
|
|
85
|
+
raise WrongCredentialsException()
|
|
86
|
+
raise V2HException(response.status_code)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def getAuth(self, method, url, json=None):
|
|
90
|
+
data = {'user_email': self.email, 'user_password': self.password, '__RequestVerificationToken': self._xsrfToken}
|
|
91
|
+
async with httpx.AsyncClient(
|
|
92
|
+
headers=self._headers, timeout=self.timeout
|
|
93
|
+
) as httpclient:
|
|
94
|
+
theUrl = loginUrl + url
|
|
95
|
+
try:
|
|
96
|
+
_LOGGER.debug(f"{method} {url} {theUrl}")
|
|
97
|
+
response = await httpclient.post(theUrl, data=data, cookies=self._cookies, follow_redirects=True)
|
|
98
|
+
except httpx.ReadTimeout:
|
|
99
|
+
raise TimeoutException()
|
|
100
|
+
else:
|
|
101
|
+
if response.status_code == 200:
|
|
102
|
+
htmlBody = response.content
|
|
103
|
+
self._cookies = response.cookies
|
|
104
|
+
soup = bs(htmlBody, "lxml")
|
|
105
|
+
jwtToken = soup.find('input', {"name": "JWTToken"})
|
|
106
|
+
if jwtToken:
|
|
107
|
+
return jwtToken['value']
|
|
108
|
+
else:
|
|
109
|
+
raise WrongCredentialsException()
|
|
110
|
+
|
|
111
|
+
# _LOGGER.debug(f"RESPONSE: {response.text}")
|
|
112
|
+
# return response.json()
|
|
113
|
+
elif response.status_code == 401:
|
|
114
|
+
raise WrongCredentialsException()
|
|
115
|
+
raise V2HException(response.status_code)
|
pyindrav2h/exceptions.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
_LOGGER = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
class V2HException(Exception):
|
|
6
|
+
def __init__(self, code=None, *args, **kwargs):
|
|
7
|
+
self.message = ""
|
|
8
|
+
super().__init__(*args, **kwargs)
|
|
9
|
+
if code is not None:
|
|
10
|
+
self.code = code
|
|
11
|
+
if isinstance(code, str):
|
|
12
|
+
self.message = self.code
|
|
13
|
+
return
|
|
14
|
+
if self.code == 401:
|
|
15
|
+
self.message = "UNAUTHORIZED"
|
|
16
|
+
elif self.code == 404:
|
|
17
|
+
self.message = "NOT_FOUND"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WrongCredentialsException(V2HException):
|
|
21
|
+
"""Class of exceptions for incomplete credentials."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TimeoutException(V2HException):
|
|
26
|
+
"""Class of exceptions for incomplete credentials."""
|
|
27
|
+
pass
|
pyindrav2h/v2hclient.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from .connection import Connection
|
|
3
|
+
from .v2hdevice import v2hDevice
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class v2hClient:
|
|
8
|
+
def __init__(
|
|
9
|
+
self, connection: Connection
|
|
10
|
+
) -> None:
|
|
11
|
+
self._connection = connection
|
|
12
|
+
self._device = None
|
|
13
|
+
|
|
14
|
+
async def refresh(self):
|
|
15
|
+
if self._device is None:
|
|
16
|
+
self._device = v2hDevice(self._connection)
|
|
17
|
+
|
|
18
|
+
await self._device.refresh_device_info()
|
|
19
|
+
await self._device.refresh_stats()
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def device(self):
|
|
23
|
+
return self._device
|
pyindrav2h/v2hdevice.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .connection import Connection
|
|
4
|
+
from . import V2H_MODES
|
|
5
|
+
|
|
6
|
+
_LOGGER = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class v2hDevice:
|
|
9
|
+
def __init__(self, connection: Connection) -> None:
|
|
10
|
+
self.connection = connection
|
|
11
|
+
self.data = {}
|
|
12
|
+
self.stats = {}
|
|
13
|
+
self.active = {}
|
|
14
|
+
|
|
15
|
+
async def refresh_device_info(self):
|
|
16
|
+
d = await self.connection.get("/authorize/validate")
|
|
17
|
+
self.data = d
|
|
18
|
+
|
|
19
|
+
async def __set_mode(self, mode):
|
|
20
|
+
s = await self.connection.post("/transactions/" + self.id +
|
|
21
|
+
"/interrupt/" + mode)
|
|
22
|
+
return s
|
|
23
|
+
|
|
24
|
+
async def load_match(self):
|
|
25
|
+
return await self.__set_mode(V2H_MODES['LOAD_MATCH'])
|
|
26
|
+
|
|
27
|
+
async def idle(self):
|
|
28
|
+
return await self.__set_mode(V2H_MODES['IDLE'])
|
|
29
|
+
|
|
30
|
+
async def schedule(self):
|
|
31
|
+
return await self.__set_mode(V2H_MODES['SCHEDULE'])
|
|
32
|
+
|
|
33
|
+
async def refresh_stats(self):
|
|
34
|
+
s = await self.connection.get("/telemetry/devices/" + self.serial +
|
|
35
|
+
"/latest")
|
|
36
|
+
self.stats = s
|
|
37
|
+
_LOGGER.debug(f"Stats: {s}")
|
|
38
|
+
a = await self.connection.get("/transactions/" + self.serial +
|
|
39
|
+
"/00000000-0000-0000-0000-000000000000/active")
|
|
40
|
+
self.active = a
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def id(self):
|
|
44
|
+
return self.active["id"]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def serial(self):
|
|
48
|
+
return self.data["devices"][0]["deviceUID"]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def lastOn(self):
|
|
52
|
+
return self.data["lastOn"]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def isActive(self):
|
|
56
|
+
return self.data["devices"][0]["active"]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def updateTime(self):
|
|
60
|
+
return self.stats["time"]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def isBoosting(self):
|
|
64
|
+
return self.stats["isBoosting"]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def mode(self):
|
|
68
|
+
return self.stats["mode"]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def state(self):
|
|
72
|
+
return self.stats["state"]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def activeEnergyFromEv(self):
|
|
76
|
+
return self.stats["data"]["activeEnergyFromEv"]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def activeEnergyToEv(self):
|
|
80
|
+
return self.stats["data"]["activeEnergyToEv"]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def powerToEv(self):
|
|
84
|
+
return self.stats["data"]["powerToEv"]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def houseLoad(self):
|
|
88
|
+
return self.stats["data"]["ctClamp"]
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def current(self):
|
|
92
|
+
return self.stats["data"]["current"]
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def voltage(self):
|
|
96
|
+
return self.stats["data"]["voltage"]
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def freq(self):
|
|
100
|
+
return self.stats["data"]["freq"]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def temperature(self):
|
|
104
|
+
return self.stats["data"]["temp"]
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def soc(self):
|
|
108
|
+
return self.stats["data"]["soc"]
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def isInterrupted(self):
|
|
112
|
+
return self.active["isInterrupted"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def showDevice(self):
|
|
116
|
+
ret = ""
|
|
117
|
+
|
|
118
|
+
ret = ret + "--- Device info ---\n"
|
|
119
|
+
ret = ret + f"Device UID: {self.serial}\n"
|
|
120
|
+
ret = ret + f"Last On date: {self.lastOn}\n"
|
|
121
|
+
ret = ret + f"Device active: {self.isActive}"
|
|
122
|
+
|
|
123
|
+
return ret
|
|
124
|
+
|
|
125
|
+
def showStats(self):
|
|
126
|
+
ret = ""
|
|
127
|
+
|
|
128
|
+
ret = ret + "--- Statistics ---\n"
|
|
129
|
+
ret = ret + f"Update time: {self.updateTime}\n"
|
|
130
|
+
ret = ret + f"Boost mode on?: {self.isBoosting}\n"
|
|
131
|
+
ret = ret + f"Mode: {self.mode}\n"
|
|
132
|
+
ret = ret + f"State: {self.state}\n"
|
|
133
|
+
ret = ret + f"Active Energy from EV: {self.activeEnergyFromEv}\n"
|
|
134
|
+
ret = ret + f"Active Energy to EV: {self.activeEnergyToEv}\n"
|
|
135
|
+
ret = ret + f"EV load + / discharge - (W): {self.powerToEv}\n"
|
|
136
|
+
ret = ret + f"House load + / Export - (W): {self.houseLoad}\n"
|
|
137
|
+
ret = ret + f"Current: {self.current}\n"
|
|
138
|
+
ret = ret + f"Voltage: {self.voltage}\n"
|
|
139
|
+
ret = ret + f"Frequency: {self.freq}\n"
|
|
140
|
+
ret = ret + f"Temperature: {self.temperature}\n"
|
|
141
|
+
ret = ret + f"Vehicle SoC: {self.soc}\n"
|
|
142
|
+
ret = ret + f"Schedule active?: {not self.isInterrupted}\n"
|
|
143
|
+
return ret
|
|
144
|
+
|
|
145
|
+
def showAll(self):
|
|
146
|
+
return self.showDevice() + "\n\n" + self.showStats()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 creatingwake
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyindrav2h
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: API client and example CLI to interact with Indra V2H Chargers
|
|
5
|
+
Author-email: Paul Morris <paul@creatingwake.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 creatingwake
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/creatingwake/pyindrav2h
|
|
28
|
+
Keywords: v2h,indra,v2g,v2x
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Programming Language :: Python
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: httpx
|
|
36
|
+
Requires-Dist: bs4
|
|
37
|
+
Requires-Dist: lxml
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# pyindrav2h
|
|
41
|
+
|
|
42
|
+
A basic API client and example CLI to interact with Indra V2H Chargers. Required by Home Assistant.
|
|
43
|
+
|
|
44
|
+
This is a very early Alpha release, and functionality may change very rapidly.
|
|
45
|
+
|
|
46
|
+
NOTE: Indra Renewable Technologies Limited are aware of this integration. However, this is an unofficial integration and Indra are not able to provide support for direct API integrations. The Indra API will likely change in future which may result in functionality provided by this integration failing at any time.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Install pyindrav2h with pip. Using venv is recommended.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pyindrav2h
|
|
56
|
+
```
|
|
57
|
+
to update pyindrav2h
|
|
58
|
+
```bash
|
|
59
|
+
pip install pyindrav2h -U
|
|
60
|
+
```
|
|
61
|
+
On installation a CLI will become available: ```indracli```
|
|
62
|
+
## Usage/Examples
|
|
63
|
+
|
|
64
|
+
### CLI
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
usage: indracli [-h] [-u EMAIL] [-p PASSWORD] [-d]
|
|
68
|
+
{statistics,device,all,loadmatch,idle,schedule} ...
|
|
69
|
+
|
|
70
|
+
Indra V2H CLI
|
|
71
|
+
|
|
72
|
+
positional arguments:
|
|
73
|
+
{statistics,device,all,loadmatch,idle,schedule}
|
|
74
|
+
statistics show device statistics
|
|
75
|
+
device show device info
|
|
76
|
+
all show all info
|
|
77
|
+
loadmatch set mode to load matching
|
|
78
|
+
idle set mode to IDLE
|
|
79
|
+
schedule return to scheuduled mode
|
|
80
|
+
|
|
81
|
+
options:
|
|
82
|
+
-h, --help show this help message and exit
|
|
83
|
+
-u EMAIL, --email EMAIL
|
|
84
|
+
-p PASSWORD, --password PASSWORD
|
|
85
|
+
-d, --debug
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
It is possible to provide a configuration file to provide Indra Smart Portal credentials. If no username/email or password is provided it will be retrieved from ```./.indra.cfg``` or ```~/.indra.cfg```
|
|
89
|
+
|
|
90
|
+
#### Example .indra.cfg Configuration file
|
|
91
|
+
```
|
|
92
|
+
[indra-account]
|
|
93
|
+
email=useremail@email.com
|
|
94
|
+
password=yourindrapassword
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Library Usage
|
|
98
|
+
|
|
99
|
+
Intended for use with Indra V2H Home Assistant integration.
|
|
100
|
+
Documentation to follow.
|
|
101
|
+
|
|
102
|
+
## Support
|
|
103
|
+
|
|
104
|
+
This is a community project that lacks formal support.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
For support from the community please join the Indra V2H trial support community: https://indra.v2h.zendesk.com/hc/en-gb/community/topics
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
For bugs or feature requests please create a GitHub Issue: https://github.com/creatingwake/pyindrav2h/issues
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
#### NOTE: Please do not contact Indra Support. Indra are unable to assist with unofficial API integrations.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Acknowledgements
|
|
118
|
+
|
|
119
|
+
- [trizmark](https://github.com/trizmark) for help with home assistant API integration examples
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyindrav2h/__init__.py,sha256=8lYuQZCRvR83cx9sx2LnwHztWfwzwe_hiBNHeC2Cwes,114
|
|
2
|
+
pyindrav2h/cli.py,sha256=qL_dzmbzxdG9bMMvnykaMBFp9RaWsFTVnfwEy-bSsVQ,2451
|
|
3
|
+
pyindrav2h/connection.py,sha256=dlz4QBbTsHdGm-jaMSJ2DnYLef3Srw_L_KQLsufIfR0,4634
|
|
4
|
+
pyindrav2h/exceptions.py,sha256=lTYAIIuKK9WOydQSetXghMl-y_3ajbV3T8WU5yXrAYM,746
|
|
5
|
+
pyindrav2h/v2hclient.py,sha256=Qr4yjfJQ0QNv6GOO693ZMOOWOFPcDEUqvNFN8fksXQM,551
|
|
6
|
+
pyindrav2h/v2hdevice.py,sha256=sBMPMkgECAVZk5L6KjHZ19N5aloZzmdjRg2r5QSa_p8,4066
|
|
7
|
+
pyindrav2h-0.0.2.dist-info/LICENSE,sha256=oHutMahDk37Dtf6mzS7bfjn4KOF6e_sSB3-S2vns3vk,1068
|
|
8
|
+
pyindrav2h-0.0.2.dist-info/METADATA,sha256=Xt3wUGD9lJLy27YngRFU7hGAZGOg1MzbTdSN0oTir7M,4183
|
|
9
|
+
pyindrav2h-0.0.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10
|
+
pyindrav2h-0.0.2.dist-info/entry_points.txt,sha256=htV_PSdnt-lyUBephTyTHnw0ztGpxKra_kffUYhPqVw,48
|
|
11
|
+
pyindrav2h-0.0.2.dist-info/top_level.txt,sha256=9L6PkzDDQdqBnXPubaAtGsrkWs4GTVDQ-dvX4je0lzs,11
|
|
12
|
+
pyindrav2h-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyindrav2h
|