pysunsynkweb 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.
- pysunsynkweb/__about__.py +6 -0
- pysunsynkweb/__init__.py +4 -0
- pysunsynkweb/battery.py +6 -0
- pysunsynkweb/const.py +10 -0
- pysunsynkweb/exceptions.py +5 -0
- pysunsynkweb/inverter.py +72 -0
- pysunsynkweb/model.py +172 -0
- pysunsynkweb/pvstring.py +41 -0
- pysunsynkweb/session.py +77 -0
- pysunsynkweb-0.0.2.dist-info/METADATA +43 -0
- pysunsynkweb-0.0.2.dist-info/RECORD +13 -0
- pysunsynkweb-0.0.2.dist-info/WHEEL +4 -0
- pysunsynkweb-0.0.2.dist-info/licenses/LICENSE.txt +9 -0
pysunsynkweb/__init__.py
ADDED
pysunsynkweb/battery.py
ADDED
pysunsynkweb/const.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Constants for pysunsynkweb."""
|
|
2
|
+
|
|
3
|
+
BASE_URL = "https://api.sunsynk.net"
|
|
4
|
+
BASE_API = BASE_URL + "/api/v1"
|
|
5
|
+
BASE_HEADERS = {
|
|
6
|
+
"accept": "application/json",
|
|
7
|
+
"content-type": "application/json",
|
|
8
|
+
"accept-language": "en-US,en;q=0.5",
|
|
9
|
+
"accept-encoding": "gzip, deflate, br",
|
|
10
|
+
}
|
pysunsynkweb/inverter.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import decimal
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from pysunsynkweb.const import BASE_API
|
|
6
|
+
from pysunsynkweb.pvstring import PVString
|
|
7
|
+
from pysunsynkweb.session import SunsynkwebSession
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Inverter:
|
|
12
|
+
sn: int
|
|
13
|
+
session: Union[SunsynkwebSession, None] = None
|
|
14
|
+
acc_pv: decimal.Decimal = decimal.Decimal(0)
|
|
15
|
+
acc_grid_export: decimal.Decimal = decimal.Decimal(0)
|
|
16
|
+
acc_grid_import: decimal.Decimal = decimal.Decimal(0)
|
|
17
|
+
acc_battery_discharge: decimal.Decimal = decimal.Decimal(0)
|
|
18
|
+
acc_battery_charge: decimal.Decimal = decimal.Decimal(0)
|
|
19
|
+
acc_load: decimal.Decimal = decimal.Decimal(0)
|
|
20
|
+
pv_strings: dict = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
async def _get_total_grid(self):
|
|
23
|
+
returned = await self.session.get(
|
|
24
|
+
BASE_API + f"/inverter/grid/{self.sn}/realtime",
|
|
25
|
+
params={"lan": "en"},
|
|
26
|
+
)
|
|
27
|
+
self.acc_grid_export = decimal.Decimal(returned["data"]["etotalTo"])
|
|
28
|
+
self.acc_grid_import = decimal.Decimal(returned["data"]["etotalFrom"])
|
|
29
|
+
|
|
30
|
+
async def _get_total_battery(self):
|
|
31
|
+
returned = await self.session.get(
|
|
32
|
+
BASE_API + f"/inverter/battery/{self.sn}/realtime",
|
|
33
|
+
params={"lan": "en"},
|
|
34
|
+
)
|
|
35
|
+
self.acc_battery_charge = decimal.Decimal(returned["data"]["etotalChg"])
|
|
36
|
+
self.acc_battery_discharge = decimal.Decimal(returned["data"]["etotalDischg"])
|
|
37
|
+
|
|
38
|
+
async def _get_total_pv(self):
|
|
39
|
+
returned = await self.session.get(
|
|
40
|
+
BASE_API + f"/inverter/{self.sn}/total",
|
|
41
|
+
params={"lan": "en"},
|
|
42
|
+
)
|
|
43
|
+
self.acc_pv = sum(
|
|
44
|
+
[
|
|
45
|
+
decimal.Decimal(i["value"])
|
|
46
|
+
for i in returned["data"]["infos"][0]["records"]
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def _get_total_load(self):
|
|
51
|
+
returned = await self.session.get(
|
|
52
|
+
BASE_API + f"/inverter/load/{self.sn}/realtime",
|
|
53
|
+
params={"lan": "en"},
|
|
54
|
+
)
|
|
55
|
+
self.acc_load = decimal.Decimal(returned["data"]["totalUsed"])
|
|
56
|
+
|
|
57
|
+
async def _update_strings(self):
|
|
58
|
+
returned = await self.session.get(
|
|
59
|
+
BASE_API + f"/inverter/{self.sn}/realtime/input"
|
|
60
|
+
)
|
|
61
|
+
strings_raw = returned["data"]["pvIV"]
|
|
62
|
+
for string in strings_raw:
|
|
63
|
+
self.pv_strings.setdefault(
|
|
64
|
+
string["pvNo"], PVString(id=string["pvNo"])
|
|
65
|
+
).update_from_inv(string)
|
|
66
|
+
|
|
67
|
+
async def update(self):
|
|
68
|
+
await self._get_total_pv()
|
|
69
|
+
await self._get_total_grid()
|
|
70
|
+
await self._get_total_battery()
|
|
71
|
+
await self._get_total_load()
|
|
72
|
+
await self._update_strings()
|
pysunsynkweb/model.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Top level data model for the sunsynk web api."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import decimal
|
|
5
|
+
import logging
|
|
6
|
+
import pprint
|
|
7
|
+
from typing import List, Union
|
|
8
|
+
|
|
9
|
+
from pysunsynkweb.inverter import Inverter
|
|
10
|
+
|
|
11
|
+
from .const import BASE_API
|
|
12
|
+
from .session import SunsynkwebSession
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Plant:
|
|
19
|
+
"""Proxy for the 'Plant' object in sunsynk web api.
|
|
20
|
+
|
|
21
|
+
A plant can host multiple inverters and other devices. Our plant object
|
|
22
|
+
carries a summary of the data from all inverters in the plant.
|
|
23
|
+
|
|
24
|
+
In the limited sample i've got, there is one plant per inverter.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
id: int
|
|
29
|
+
master_id: int
|
|
30
|
+
name: str
|
|
31
|
+
status: int
|
|
32
|
+
battery_power: int = 0
|
|
33
|
+
state_of_charge: float = 0
|
|
34
|
+
load_power: int = 0
|
|
35
|
+
grid_power: int = 0
|
|
36
|
+
pv_power: int = 0
|
|
37
|
+
session: Union[SunsynkwebSession, None] = None
|
|
38
|
+
inverters: List[Inverter] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
"""Summary of the plant"""
|
|
42
|
+
return f"""Plant {id}
|
|
43
|
+
|
|
44
|
+
{self.name}
|
|
45
|
+
Battery power {self.battery_power} W ({self.state_of_charge})%
|
|
46
|
+
Grid Power: {self.grid_power} W
|
|
47
|
+
PV Power: {self.pv_power} W
|
|
48
|
+
Accumulated PV Energy {self.acc_pv} KWh
|
|
49
|
+
Accumulated Grid export {self.acc_grid_export} KWh
|
|
50
|
+
Accumulated Grid import {self.acc_grid_import} KWh
|
|
51
|
+
Accumulated Load {self.acc_load} KWh
|
|
52
|
+
Accumulated Battery discharge {self.acc_battery_discharge} KWh
|
|
53
|
+
Accumulated Battery charge {self.acc_battery_charge} KWh
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def ismaster(self):
|
|
58
|
+
"""Is the plant a master plant.
|
|
59
|
+
|
|
60
|
+
Unused at the moment, but required when introducing read-write calls
|
|
61
|
+
(for instance to command to charge batteries from the grid).
|
|
62
|
+
"""
|
|
63
|
+
return self.master_id == self.id
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_api(cls, api_return, session):
|
|
67
|
+
"""Create the plant from the return of the web api."""
|
|
68
|
+
return cls(
|
|
69
|
+
name=api_return["name"],
|
|
70
|
+
id=api_return["id"],
|
|
71
|
+
master_id=api_return["masterId"],
|
|
72
|
+
status=api_return["status"],
|
|
73
|
+
session=session,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def enrich_inverters(self):
|
|
77
|
+
"""Populate inverters' serial numbers.
|
|
78
|
+
|
|
79
|
+
The plant summary doesn't contain the inverters, so we have a
|
|
80
|
+
separate call to populate inverter's serial numbers.
|
|
81
|
+
"""
|
|
82
|
+
returned = await self.session.get(
|
|
83
|
+
BASE_API + f"/plant/{self.id}/inverters",
|
|
84
|
+
params={"page": 1, "limit": 20, "type": -1, "status": -1},
|
|
85
|
+
)
|
|
86
|
+
self.inverters = [
|
|
87
|
+
Inverter(k["sn"], session=self.session) for k in returned["data"]["infos"]
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
async def _get_instantaneous_data(self):
|
|
91
|
+
"""Populate instantaneous data.
|
|
92
|
+
|
|
93
|
+
Instantaneous data is conveniently summarized in the 'flow' api end point.
|
|
94
|
+
"""
|
|
95
|
+
returned = await self.session.get(
|
|
96
|
+
BASE_API + f"/plant/energy/{self.id}/flow",
|
|
97
|
+
params={"page": 1, "limit": 20},
|
|
98
|
+
)
|
|
99
|
+
_LOGGER.debug("Flow Api returned %s", pprint.pformat(returned))
|
|
100
|
+
|
|
101
|
+
self.battery_power = returned["data"]["battPower"]
|
|
102
|
+
if returned["data"]["toBat"]:
|
|
103
|
+
self.battery_power *= -1
|
|
104
|
+
self.state_of_charge = returned["data"]["soc"]
|
|
105
|
+
self.load_power = returned["data"]["loadOrEpsPower"]
|
|
106
|
+
self.grid_power = returned["data"]["gridOrMeterPower"]
|
|
107
|
+
if returned["data"]["toGrid"]:
|
|
108
|
+
self.grid_power *= -1
|
|
109
|
+
self.pv_power = returned["data"]["pvPower"]
|
|
110
|
+
|
|
111
|
+
async def update(self):
|
|
112
|
+
"""Update all sensors."""
|
|
113
|
+
await self._get_instantaneous_data()
|
|
114
|
+
for inverter in self.inverters:
|
|
115
|
+
await inverter.update()
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def acc_pv(self):
|
|
119
|
+
return sum(i.acc_pv for i in self.inverters)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def acc_grid_export(self):
|
|
123
|
+
return max(i.acc_grid_export for i in self.inverters)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def acc_grid_import(self):
|
|
127
|
+
return max(i.acc_grid_import for i in self.inverters)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def acc_battery_discharge(self):
|
|
131
|
+
return sum(i.acc_battery_discharge for i in self.inverters)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def acc_battery_charge(self):
|
|
135
|
+
return sum(i.acc_battery_charge for i in self.inverters)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def acc_load(self):
|
|
139
|
+
return sum(i.acc_load for i in self.inverters)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class Installation:
|
|
144
|
+
"""An installation is a series of plants.
|
|
145
|
+
|
|
146
|
+
This integration presents the plants as a single entity.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
plants: List[Plant]
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_api(cls, api_return, session):
|
|
153
|
+
"""Create the installation from the sunsynk web api."""
|
|
154
|
+
assert "data" in api_return
|
|
155
|
+
assert api_return["msg"] == "Success"
|
|
156
|
+
return cls(
|
|
157
|
+
plants=[Plant.from_api(ret, session) for ret in api_return["data"]["infos"]]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async def update(self):
|
|
161
|
+
"""Update all the plants. They will in turn update their sensors."""
|
|
162
|
+
for plant in self.plants:
|
|
163
|
+
await plant.update()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def get_plants(session: SunsynkwebSession):
|
|
167
|
+
"""Start walking the plant composition."""
|
|
168
|
+
returned = await session.get(BASE_API + "/plants", params={"page": 1, "limit": 20})
|
|
169
|
+
installation = Installation.from_api(returned, session)
|
|
170
|
+
for plant in installation.plants:
|
|
171
|
+
await plant.enrich_inverters()
|
|
172
|
+
return installation
|
pysunsynkweb/pvstring.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import datetime
|
|
3
|
+
import decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class PVString:
|
|
8
|
+
"""A PV 'string'.
|
|
9
|
+
|
|
10
|
+
PV 'strings' are cables from a set of solar panels. Typically, your
|
|
11
|
+
installer will group the panels on string based on common orientation
|
|
12
|
+
or exposure to the sun.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
id: int
|
|
17
|
+
voltage: decimal.Decimal = decimal.Decimal(0)
|
|
18
|
+
power: decimal.Decimal = decimal.Decimal(0)
|
|
19
|
+
amperage: decimal.Decimal = decimal.Decimal(0)
|
|
20
|
+
last_update: datetime.datetime = None
|
|
21
|
+
|
|
22
|
+
def update_from_inv(self, data):
|
|
23
|
+
"""Update from invertor 'input' request.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
{
|
|
27
|
+
"id": null,
|
|
28
|
+
"pvNo": 1,
|
|
29
|
+
"vpv": "212.9",
|
|
30
|
+
"ipv": "1.0",
|
|
31
|
+
"ppv": "222.0",
|
|
32
|
+
"todayPv": "0.0",
|
|
33
|
+
"sn": "2211166856",
|
|
34
|
+
"time": "2024-06-13 07:54:05"
|
|
35
|
+
},
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
self.last_update = datetime.datetime.strptime(data["time"], "%Y-%m-%d %H:%M:%S")
|
|
39
|
+
self.power = decimal.Decimal(data["ppv"])
|
|
40
|
+
self.voltage = decimal.Decimal(data["vpv"])
|
|
41
|
+
self.amperage = decimal.Decimal(data["ipv"])
|
pysunsynkweb/session.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""A session object
|
|
2
|
+
|
|
3
|
+
Maintaining the authentication with the sunsynk api and
|
|
4
|
+
wrapping repetitive things
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import pprint
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from .const import BASE_HEADERS, BASE_URL
|
|
13
|
+
from .exceptions import AuthenticationFailed
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SunsynkwebSession:
|
|
19
|
+
"""the main entry point to sunsynk api.
|
|
20
|
+
|
|
21
|
+
Maintains http headers,
|
|
22
|
+
authentication token, etc.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, session: aiohttp.ClientSession, username: str, password: str) -> None:
|
|
26
|
+
"""Pass an aiohttp client session and authentication items"""
|
|
27
|
+
self.session = session
|
|
28
|
+
self.bearer = None
|
|
29
|
+
self.username = username
|
|
30
|
+
self.password = password
|
|
31
|
+
|
|
32
|
+
async def get(self, *args, **kwargs):
|
|
33
|
+
"""Run a GET query against the sunsynk api"""
|
|
34
|
+
if self.bearer is None:
|
|
35
|
+
await self._get_bearer_token()
|
|
36
|
+
headers = BASE_HEADERS.copy()
|
|
37
|
+
headers.update({"Authorization": f"Bearer{self.bearer}"})
|
|
38
|
+
kwargs["headers"] = headers
|
|
39
|
+
result = await self.session.get(*args, **kwargs)
|
|
40
|
+
result = await result.json()
|
|
41
|
+
if result.get("msg") != "Success" and result.get("code") == 401:
|
|
42
|
+
# expired token
|
|
43
|
+
await self._get_bearer_token()
|
|
44
|
+
result = await self.get(*args, **kwargs)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
async def _get_bearer_token(self):
|
|
48
|
+
"""Get the bearer token for the sunsynk api."""
|
|
49
|
+
params = {
|
|
50
|
+
"username": self.username,
|
|
51
|
+
"password": self.password,
|
|
52
|
+
"grant_type": "password",
|
|
53
|
+
"client_id": "csp-web",
|
|
54
|
+
"source": "sunsynk",
|
|
55
|
+
"areaCode": "sunsynk",
|
|
56
|
+
}
|
|
57
|
+
returned = await self.session.post(BASE_URL + "/oauth/token", json=params, headers=BASE_HEADERS)
|
|
58
|
+
|
|
59
|
+
returned = await returned.json()
|
|
60
|
+
_LOGGER.debug("authentication attempt returned %s", pprint.pformat(returned))
|
|
61
|
+
# returned data looks like the below
|
|
62
|
+
# {
|
|
63
|
+
# "code": 0,
|
|
64
|
+
# "msg": "Success",
|
|
65
|
+
# "data": {
|
|
66
|
+
# "access_token": "VALUE",
|
|
67
|
+
# "token_type": "bearer",
|
|
68
|
+
# "refresh_token": "VALUE",
|
|
69
|
+
# "expires_in": 604799,
|
|
70
|
+
# "scope": "all",
|
|
71
|
+
# },
|
|
72
|
+
# "success": True,
|
|
73
|
+
# }
|
|
74
|
+
try:
|
|
75
|
+
self.bearer = returned["data"]["access_token"]
|
|
76
|
+
except KeyError as exc:
|
|
77
|
+
raise AuthenticationFailed from exc
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pysunsynkweb
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Project-URL: Documentation, https://github.com/francoisverbeek/pysunsynkweb
|
|
5
|
+
Project-URL: Issues, https://github.com/francoisverbeek/pysunsynkweb/issues
|
|
6
|
+
Project-URL: Source, https://github.com/francoisverbeek/pysunsynkweb
|
|
7
|
+
Author-email: Francois <f@flve.uk>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Requires-Dist: aiohttp
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Sunsynk Web
|
|
24
|
+
|
|
25
|
+
[](https://pypi.org/project/pysunsynkweb)
|
|
26
|
+
[](https://pypi.org/project/pysunsynkweb)
|
|
27
|
+
|
|
28
|
+
-----
|
|
29
|
+
|
|
30
|
+
## Table of Contents
|
|
31
|
+
|
|
32
|
+
- [Installation](#installation)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
pip install pysunsynkweb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
`pysunsynkweb` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pysunsynkweb/__about__.py,sha256=Iug5BLKo1Q_htAJ3DfAWjoemeuWjZ-BTsD9zs-if9J4,157
|
|
2
|
+
pysunsynkweb/__init__.py,sha256=W-7fTovQ_YrLkagXsWhaSSMdEbmaNHMU72Hmi2ul8jI,162
|
|
3
|
+
pysunsynkweb/battery.py,sha256=MRF4vtUxOuaJrsiEi4761FqtNE8a1Y038dAkV9VpZPo,74
|
|
4
|
+
pysunsynkweb/const.py,sha256=N2PnrEETMhNF6k2vqxzqDlmA9BKyfOSPA2VFsXMAu08,282
|
|
5
|
+
pysunsynkweb/exceptions.py,sha256=3XiZihDBmFM6aCV3MAl3sC3D8XsajL382ah7EAiwe3M,109
|
|
6
|
+
pysunsynkweb/inverter.py,sha256=JeTTylP2R41XU07E2u6OkoKeyAHDlFxb6BiHlqY4FW0,2596
|
|
7
|
+
pysunsynkweb/model.py,sha256=qIwcJ_VSjx7CY3XSAO3tqFIStO5vsEfFcIGPeEnORU0,5370
|
|
8
|
+
pysunsynkweb/pvstring.py,sha256=hJEofwkxqc3EOxvsQMpGdCT5QUgtjISKDCN-fLyEVP4,1250
|
|
9
|
+
pysunsynkweb/session.py,sha256=OP81Kyl1AnnP-8h3x4-mhGORRa4D4BrAu8HfVwv-QjY,2465
|
|
10
|
+
pysunsynkweb-0.0.2.dist-info/METADATA,sha256=SEUl3CuWhCsfCW2PCYk4HGbFSwusLZDw0mNb3bCsHow,1433
|
|
11
|
+
pysunsynkweb-0.0.2.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
|
12
|
+
pysunsynkweb-0.0.2.dist-info/licenses/LICENSE.txt,sha256=FlpRiuOvoN51zmppyHedCkDPsMo1ZB3PYxd-mV8X7t0,1085
|
|
13
|
+
pysunsynkweb-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Francois <f@flve.uk>
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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.
|