ics_2000_python 0.1.1__tar.gz → 0.1.3__tar.gz

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.
Files changed (22) hide show
  1. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/PKG-INFO +10 -4
  2. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/README.md +7 -1
  3. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/command.py +2 -2
  4. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/encryption.py +6 -4
  5. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/color_temperature_device.py +15 -1
  6. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/device.py +9 -1
  7. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/dim_device.py +25 -1
  8. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/entity.py +12 -3
  9. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/switch_device.py +27 -0
  10. ics_2000_python-0.1.3/ics_2000/exceptions.py +12 -0
  11. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/hub.py +60 -24
  12. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/pyproject.toml +2 -2
  13. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/LICENSE +0 -0
  14. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/__init__.py +0 -0
  15. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/config.py +0 -0
  16. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/entities/__init__.py +0 -0
  17. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/__init__.py +0 -0
  18. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/device_config.py +0 -0
  19. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/device_data.py +0 -0
  20. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/entity_type.py +0 -0
  21. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/smart_meter_data.py +0 -0
  22. {ics_2000_python-0.1.1 → ics_2000_python-0.1.3}/ics_2000/model/smart_meter_data_current.py +0 -0
@@ -1,14 +1,14 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: ics_2000_python
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary:
5
- Home-page: https://github.com/zefanjajobse/ics_2000_python
6
5
  Keywords: ics_2000,ics-2000,klikaanklikuit
7
6
  Author: iiTzArcur
8
7
  Author-email: zefanjaj@gmail.com
9
8
  Requires-Python: >=3.12,<4.0
10
9
  Classifier: Programming Language :: Python :: 3
11
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
12
  Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
13
13
  Requires-Dist: requests (>=2.32.3,<3.0.0)
14
14
  Project-URL: Repository, https://github.com/zefanjajobse/ics_2000_python
@@ -19,8 +19,14 @@ This library gives methods to control a ICS-2000 from KlikAanKlikUit. It is made
19
19
 
20
20
  ## Usage example:
21
21
  ```py
22
+ from ics_2000.entities.dim_device import DimDevice
23
+ from ics_2000.entities.switch_device import SwitchDevice
24
+ from ics_2000.hub import Hub
25
+
22
26
  hub = Hub("example@email.com", "password")
23
- hub.login() # Authenticate
27
+ homes = hub.login() # Authenticate
28
+ print(homes) # returns a dictionary of homes, ex: {"0": "test home}
29
+ hub.select_home("132829") # Select the right home by id
24
30
  hub.get_devices() # request devices
25
31
  for device in hub.devices:
26
32
  if type(device) is SwitchDevice or type(device) is DimDevice:
@@ -3,8 +3,14 @@ This library gives methods to control a ICS-2000 from KlikAanKlikUit. It is made
3
3
 
4
4
  ## Usage example:
5
5
  ```py
6
+ from ics_2000.entities.dim_device import DimDevice
7
+ from ics_2000.entities.switch_device import SwitchDevice
8
+ from ics_2000.hub import Hub
9
+
6
10
  hub = Hub("example@email.com", "password")
7
- hub.login() # Authenticate
11
+ homes = hub.login() # Authenticate
12
+ print(homes) # returns a dictionary of homes, ex: {"0": "test home}
13
+ hub.select_home("132829") # Select the right home by id
8
14
  hub.get_devices() # request devices
9
15
  for device in hub.devices:
10
16
  if type(device) is SwitchDevice or type(device) is DimDevice:
@@ -13,10 +13,10 @@ class Command:
13
13
  hub_mac: str,
14
14
  device_id: int,
15
15
  device_function: int,
16
- value: int,
16
+ value: int | float,
17
17
  aes_key: str,
18
18
  entity_type: Entity_Type,
19
- device_functions: List[int] = [],
19
+ device_functions: List[int | float] = [],
20
20
  ):
21
21
  self.hub_mac = hub_mac
22
22
  self.device_id = device_id
@@ -1,13 +1,15 @@
1
1
  import base64
2
2
  from Crypto.Cipher import AES
3
3
  from Crypto.Util.Padding import unpad
4
- from Crypto.Random import get_random_bytes
5
4
 
6
5
  encoding = "utf-8"
7
6
  block_size = 16
8
- pad = lambda s: s + (block_size - len(s) % block_size) * chr(
9
- block_size - len(s) % block_size
10
- )
7
+
8
+
9
+ def pad(s):
10
+ return s + (block_size - len(s) % block_size) * chr(
11
+ block_size - len(s) % block_size
12
+ )
11
13
 
12
14
 
13
15
  def decrypt(string: str, aes: str) -> str:
@@ -1,10 +1,18 @@
1
+ from typing import TYPE_CHECKING
1
2
  from ..model.device_data import DeviceData
2
3
  from .dim_device import DimDevice
3
4
  from ..model.device_config import DeviceConfig
4
5
 
6
+ if TYPE_CHECKING:
7
+ from ..hub import Hub
8
+
5
9
 
6
10
  class ColorTemperatureDevice(DimDevice):
7
- def __init__(self, hub, device_data: DeviceData, device_config: DeviceConfig):
11
+ """Class used for color changeable, dimmable devices"""
12
+
13
+ def __init__(
14
+ self, hub: "Hub", device_data: DeviceData, device_config: DeviceConfig
15
+ ):
8
16
  super().__init__(hub, device_data, device_config)
9
17
 
10
18
  if device_config.color_temperature_function is None:
@@ -15,6 +23,9 @@ class ColorTemperatureDevice(DimDevice):
15
23
  def change_color_temperature(
16
24
  self, color_temperature: float, send_local: bool = True
17
25
  ) -> None:
26
+ if self.device_config.color_temperature_function is None:
27
+ return
28
+
18
29
  return self.get_hub().change_color_temperature(
19
30
  self.entity_id,
20
31
  self.device_config.color_temperature_function,
@@ -24,5 +35,8 @@ class ColorTemperatureDevice(DimDevice):
24
35
  )
25
36
 
26
37
  def get_color_temperature(self) -> float:
38
+ if self.device_config.color_temperature_function is None:
39
+ return 0
40
+
27
41
  status = self.get_hub().get_device_status(self.entity_id)
28
42
  return status[self.device_config.color_temperature_function]
@@ -1,14 +1,22 @@
1
+ from typing import TYPE_CHECKING
1
2
  from .entity import Entity
2
3
  from ..model.device_data import DeviceData
3
4
  from ..model.device_config import DeviceConfig
4
5
 
6
+ if TYPE_CHECKING:
7
+ from ..hub import Hub
8
+
5
9
 
6
10
  class Device(Entity):
7
- def __init__(self, hub, device_data: DeviceData, device_config: DeviceConfig):
11
+ def __init__(
12
+ self, hub: "Hub", device_data: DeviceData, device_config: DeviceConfig
13
+ ):
8
14
  super().__init__(
9
15
  hub, device_data, "group" in device_data.data and "group" or "module"
10
16
  )
11
17
  self.device_config = device_config
18
+ """List of device functions"""
12
19
  self.disabled = (
13
20
  device_config.disabled if device_config.disabled is not None else False
14
21
  )
22
+ """If the device can be changed"""
@@ -1,16 +1,33 @@
1
+ from typing import TYPE_CHECKING
1
2
  from ..model.device_data import DeviceData
2
3
  from .switch_device import SwitchDevice
3
4
  from ..model.device_config import DeviceConfig
4
5
 
6
+ if TYPE_CHECKING:
7
+ from ..hub import Hub
8
+
5
9
 
6
10
  class DimDevice(SwitchDevice):
7
- def __init__(self, hub, device_data: DeviceData, device_config: DeviceConfig):
11
+ """Class used for all dimmable devices"""
12
+
13
+ def __init__(
14
+ self, hub: "Hub", device_data: DeviceData, device_config: DeviceConfig
15
+ ):
8
16
  super().__init__(hub, device_data, device_config)
9
17
 
10
18
  if device_config.dim_function is None:
11
19
  raise ValueError(f"Dim function not defined for '{self.device_data.name}'")
12
20
 
13
21
  def dim(self, dim_level: int, send_local: bool = True) -> None:
22
+ """Change the dim level of the device.
23
+
24
+ Args:
25
+ dim_level: new dim level (0-255).
26
+ send_local: Use the ip_address set on the hub to talk to the device
27
+ """
28
+ if self.device_config.dim_function is None:
29
+ return
30
+
14
31
  return self.get_hub().dim_device(
15
32
  self.entity_id,
16
33
  self.device_config.dim_function,
@@ -20,5 +37,12 @@ class DimDevice(SwitchDevice):
20
37
  )
21
38
 
22
39
  def get_dim_level(self) -> int:
40
+ """Get the dim level of a device
41
+
42
+ Returns:
43
+ Current dim level (0-255)."""
44
+ if self.device_config.dim_function is None:
45
+ return 0
46
+
23
47
  status = self.get_hub().get_device_status(self.entity_id)
24
48
  return status[self.device_config.dim_function]
@@ -1,13 +1,22 @@
1
- from typing import List
1
+ from typing import List, TYPE_CHECKING
2
+
2
3
  from ..model.device_data import DeviceData
3
4
  from ..model.entity_type import EntityType
4
5
 
6
+ if TYPE_CHECKING:
7
+ from ..hub import Hub
8
+
5
9
 
6
10
  class Entity:
7
- def __init__(self, hub, device_data: DeviceData, entity_type: EntityType):
11
+ """Class used for all devices"""
12
+
13
+ def __init__(self, hub: "Hub", device_data: DeviceData, entity_type: EntityType):
8
14
  self._hub = hub
15
+ """Main hub"""
9
16
  self.entity_id = int(device_data.id)
17
+ """ID"""
10
18
  self.name = device_data.data[entity_type].get("name", "")
19
+ """Name of the device"""
11
20
  self.device_type = (
12
21
  device_data.data[entity_type].get("device", "")
13
22
  if "device" in device_data.data[entity_type]
@@ -19,7 +28,7 @@ class Entity:
19
28
  def get_status(self) -> List[int]:
20
29
  return self._hub.get_device_status(self.entity_id)
21
30
 
22
- def get_hub(self):
31
+ def get_hub(self) -> "Hub":
23
32
  return self._hub
24
33
 
25
34
  def change_status(self, device_function: int, value: int, send_local: bool) -> None:
@@ -4,6 +4,8 @@ from .device import Device
4
4
 
5
5
 
6
6
  class SwitchDevice(Device):
7
+ """Class used for all switch devices"""
8
+
7
9
  def __init__(self, hub, device_data: DeviceData, device_config: DeviceConfig):
8
10
  super().__init__(hub, device_data, device_config)
9
11
 
@@ -13,6 +15,14 @@ class SwitchDevice(Device):
13
15
  )
14
16
 
15
17
  def turn_on_off(self, on: bool, send_local: bool = True) -> None:
18
+ """Change the state of the device.
19
+
20
+ Args:
21
+ on: new state of the device, True == on.
22
+ send_local: Use the ip_address set on the hub to talk to the device
23
+ """
24
+ if self.device_config.on_off_function is None:
25
+ return
16
26
  self.get_hub().turn_device_on_off(
17
27
  self.entity_id,
18
28
  on,
@@ -22,6 +32,13 @@ class SwitchDevice(Device):
22
32
  )
23
33
 
24
34
  def turn_on(self, send_local: bool = True) -> None:
35
+ """Turn on the device.
36
+
37
+ Args:
38
+ send_local: Use the ip_address set on the hub to talk to the device
39
+ """
40
+ if self.device_config.on_off_function is None:
41
+ return
25
42
  self.get_hub().turn_device_on_off(
26
43
  self.entity_id,
27
44
  True,
@@ -31,6 +48,13 @@ class SwitchDevice(Device):
31
48
  )
32
49
 
33
50
  def turn_off(self, send_local: bool = True) -> None:
51
+ """Turn off the device.
52
+
53
+ Args:
54
+ send_local: Use the ip_address set on the hub to talk to the device
55
+ """
56
+ if self.device_config.on_off_function is None:
57
+ return
34
58
  self.get_hub().turn_device_on_off(
35
59
  self.entity_id,
36
60
  False,
@@ -40,5 +64,8 @@ class SwitchDevice(Device):
40
64
  )
41
65
 
42
66
  def get_on_status(self) -> bool:
67
+ """Returns true if device is turned on"""
68
+ if self.device_config.on_off_function is None:
69
+ return False
43
70
  status = self.get_status()
44
71
  return status[self.device_config.on_off_function] == 1
@@ -0,0 +1,12 @@
1
+
2
+ class InvalidAuthException(Exception):
3
+ """When the authentication failed"""
4
+
5
+ class InvalidHomeException(Exception):
6
+ """When selected home does not exist as a option"""
7
+
8
+ class InvalidMacOrAuthException(Exception):
9
+ """When the auth or mac address is not set"""
10
+
11
+ class NoHomeSelectedException(Exception):
12
+ """When no home is selected"""
@@ -1,6 +1,8 @@
1
1
  from datetime import datetime
2
2
  import json
3
3
  import requests
4
+
5
+ from ics_2000.exceptions import InvalidAuthException, InvalidHomeException, InvalidMacOrAuthException, NoHomeSelectedException
4
6
  from .command import Command
5
7
  from .encryption import decrypt
6
8
  from .config import API_URL, device_configs
@@ -14,22 +16,43 @@ from .model.entity_type import Entity_Type
14
16
 
15
17
 
16
18
  class Hub:
19
+ """KlikAanKlikUit ICS-2000 Control Station connection info
20
+
21
+
22
+ Attributes:
23
+ email: Email address of the account connected to the ICS-2000
24
+ password: Password of the account connected to the ICS-2000
25
+ """
26
+
17
27
  def __init__(self, email: str, password: str):
28
+ self.homes = []
18
29
  self.devices: list[
19
30
  Device | DimDevice | SwitchDevice | ColorTemperatureDevice
20
31
  ] = []
32
+ """List of available devices within the authenticated home"""
21
33
  self.aes_key: str | None = None
34
+ """Authentication key"""
22
35
  self.mac: str | None = None
36
+ """MAC-Address of the KlikAanKlikUit ICS-2000"""
23
37
  self.home_name: str | None = None
38
+ """Name of the home given within KlikAanKlikUit"""
24
39
  self.home_id: str | None = None
40
+ """ID of the home"""
25
41
  self.email = email
42
+ """Email address of the account connected to the ICS-2000"""
26
43
  self.password = password
44
+ """Password of the account connected to the ICS-2000"""
27
45
  self.local_address: str | None = None
46
+ """Local IP address when you want to control it locally"""
28
47
  self.device_statuses: dict[int, list[int]] = {}
48
+ """Statuses of the devices"""
29
49
  self.update_date = datetime.min
50
+ """When the device statuses were last updated"""
30
51
 
31
- def login(self):
32
- test = requests.post(
52
+ def login(self) -> dict[str, str]:
53
+ """Logs in to the account given"""
54
+ self.homes = []
55
+ response = requests.post(
33
56
  f"{API_URL}/account.php",
34
57
  {
35
58
  "action": "login",
@@ -41,24 +64,28 @@ class Hub:
41
64
  },
42
65
  )
43
66
 
44
- if test.status_code != 200:
45
- if test.status_code == 401:
46
- raise Exception("Incorrect username or password")
47
- raise Exception(test.content)
48
-
49
- response = test.json()
50
- if len(response.get("homes", [])) > 0:
51
- home: dict = response["homes"][0]
52
- self.aes_key = home.get("aes_key", "")
53
- self.mac = home.get("mac", "")
54
- self.home_id = home.get("home_id")
55
- self.home_name = home.get("home_name")
56
- else:
57
- raise Exception("No homes")
67
+ if response.status_code != 200:
68
+ if response.status_code == 401:
69
+ raise InvalidAuthException("Incorrect username or password")
70
+ raise InvalidAuthException(response.content)
71
+
72
+ data = response.json()
73
+ if data is not None:
74
+ self.homes = data.get("homes", [])
75
+ return dict([(home.get("home_id"), home.get("home_name"))for home in self.homes])
76
+
77
+ def select_home(self, home_id: str) -> None:
78
+ home = next(home for home in self.homes if home.get("home_id") == home_id)
79
+ if home is None:
80
+ raise InvalidHomeException()
81
+ self.aes_key = home.get("aes_key", "")
82
+ self.mac = home.get("mac", "")
83
+ self.home_id = home.get("home_id")
84
+ self.home_name = home.get("home_name")
58
85
 
59
86
  def get_raw_devices_data(self, decrypt_data, decrypt_status) -> list:
60
87
  if self.aes_key is None or self.mac is None:
61
- raise Exception("no mac or auth key")
88
+ raise InvalidMacOrAuthException()
62
89
 
63
90
  device_info = requests.post(
64
91
  f"{API_URL}/gateway.php",
@@ -91,9 +118,12 @@ class Hub:
91
118
 
92
119
  return data
93
120
 
94
- def get_devices(self):
121
+ def get_devices(
122
+ self,
123
+ ) -> list[Device | DimDevice | SwitchDevice | ColorTemperatureDevice]:
124
+ """Gets all devices connected to the hub"""
95
125
  if self.home_id is None:
96
- raise Exception("no home")
126
+ raise NoHomeSelectedException()
97
127
 
98
128
  entities = self.get_raw_devices_data(True, False)
99
129
  for device in entities:
@@ -129,14 +159,19 @@ class Hub:
129
159
  self.devices.append(SwitchDevice(self, device_data, device_config))
130
160
  else:
131
161
  self.devices.append(Device(self, device_data, device_config))
162
+ return self.devices
132
163
 
133
164
  def create_command(
134
- self, device_id: int, device_function: int, value: int, entity_type: Entity_Type
165
+ self,
166
+ device_id: int,
167
+ device_function: int,
168
+ value: int | float,
169
+ entity_type: Entity_Type,
135
170
  ) -> Command:
136
171
  if self.aes_key is None or self.mac is None:
137
- raise Exception("no mac or auth key")
172
+ raise InvalidMacOrAuthException()
138
173
 
139
- device_functions: list[int] = []
174
+ device_functions: list[int | float] = []
140
175
 
141
176
  # if entity_type == Entity_Type.Group:
142
177
  # device_functions = self.device_statuses.get(device_id, [])
@@ -159,6 +194,7 @@ class Hub:
159
194
  is_group: bool,
160
195
  send_local: bool,
161
196
  ) -> None:
197
+ """Change to the enabled state of the device"""
162
198
  command = self.create_command(
163
199
  device_id,
164
200
  on_function,
@@ -236,7 +272,7 @@ class Hub:
236
272
  self, data: dict, decrypt_data: bool, decrypt_status: bool
237
273
  ) -> dict:
238
274
  if self.aes_key is None or self.mac is None:
239
- raise Exception("no mac or auth key")
275
+ raise InvalidMacOrAuthException()
240
276
 
241
277
  if decrypt_data:
242
278
  data["data"] = json.loads(decrypt(data.get("data", ""), self.aes_key))
@@ -294,7 +330,7 @@ class Hub:
294
330
  self,
295
331
  device_id: int,
296
332
  color_temp_function: int,
297
- color_temperature: int,
333
+ color_temperature: float,
298
334
  is_group: bool,
299
335
  send_local: bool,
300
336
  ):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ics_2000_python"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = ""
5
5
  authors = ["iiTzArcur <zefanjaj@gmail.com>"]
6
6
  readme = "README.md"
@@ -15,7 +15,7 @@ pycryptodome = "^3.21.0"
15
15
 
16
16
 
17
17
  [tool.poetry.group.test.dependencies]
18
- pytest = "^8.3.3"
18
+ pytest = "^8.3.4"
19
19
  requests-mock = "^1.12.1"
20
20
 
21
21
  [build-system]
File without changes