gshock-api 2.0.24__tar.gz → 2.0.26__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 (47) hide show
  1. {gshock_api-2.0.24/src/gshock_api.egg-info → gshock_api-2.0.26}/PKG-INFO +1 -1
  2. {gshock_api-2.0.24 → gshock_api-2.0.26}/pyproject.toml +1 -1
  3. {gshock_api-2.0.24 → gshock_api-2.0.26}/setup.py +1 -1
  4. gshock_api-2.0.26/src/gshock_api/always_connected_watch_filter.py +40 -0
  5. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/connection.py +5 -5
  6. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/gshock_api.py +3 -0
  7. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/dst_watch_state_io.py +0 -1
  8. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/scanner.py +3 -5
  9. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/watch_info.py +96 -45
  10. {gshock_api-2.0.24 → gshock_api-2.0.26/src/gshock_api.egg-info}/PKG-INFO +1 -1
  11. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api.egg-info/SOURCES.txt +1 -0
  12. {gshock_api-2.0.24 → gshock_api-2.0.26}/LICENSE.txt +0 -0
  13. {gshock_api-2.0.24 → gshock_api-2.0.26}/README.rst +0 -0
  14. {gshock_api-2.0.24 → gshock_api-2.0.26}/setup.cfg +0 -0
  15. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/__init__.py +0 -0
  16. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/alarms.py +0 -0
  17. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/app_notification.py +0 -0
  18. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/cancelable_result.py +0 -0
  19. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/casio_constants.py +0 -0
  20. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/event.py +0 -0
  21. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/exceptions.py +0 -0
  22. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/__init__.py +0 -0
  23. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/alarms_io.py +0 -0
  24. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/app_info_io.py +0 -0
  25. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/app_notification_io.py +0 -0
  26. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/button_pressed_io.py +0 -0
  27. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/dst_for_world_cities_io.py +0 -0
  28. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/error_io.py +0 -0
  29. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/events_io.py +2 -2
  30. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/settings_io.py +0 -0
  31. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/time_adjustement_io.py +0 -0
  32. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/time_io.py +0 -0
  33. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/timer_io.py +0 -0
  34. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/unknown_io.py +0 -0
  35. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/watch_condition_io.py +0 -0
  36. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/watch_name_io.py +0 -0
  37. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/iolib/world_cities_io.py +0 -0
  38. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/logger.py +0 -0
  39. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/mailsener.py +0 -0
  40. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/message_dispatcher.py +0 -0
  41. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/settings.py +0 -0
  42. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api/utils.py +0 -0
  43. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api.egg-info/dependency_links.txt +0 -0
  44. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api.egg-info/not-zip-safe +0 -0
  45. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api.egg-info/requires.txt +0 -0
  46. {gshock_api-2.0.24 → gshock_api-2.0.26}/src/gshock_api.egg-info/top_level.txt +0 -0
  47. {gshock_api-2.0.24 → gshock_api-2.0.26}/tests/test_code.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gshock_api
3
- Version: 2.0.24
3
+ Version: 2.0.26
4
4
  Summary: Pythgon API for GShock Watches using BLE
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: Ivo Zivkov
@@ -5,7 +5,7 @@ requires = ["setuptools>=61.0"]
5
5
 
6
6
  [project]
7
7
  name = "gshock_api" # Change to your project name
8
- version = "2.0.24"
8
+ version = "2.0.26"
9
9
  description = "Pythgon API for GShock Watches using BLE"
10
10
  readme = "README.rst"
11
11
  authors = [{ name = "Ivo Zivkov", email = "izivkov@gmail.com" }]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='gshock_api',
5
- version='2.0.24',
5
+ version='2.0.26',
6
6
  package_dir={'': 'src'},
7
7
  packages=find_packages(where='src'),
8
8
  install_requires=[
@@ -0,0 +1,40 @@
1
+ import time
2
+ from gshock_api.watch_info import watch_info
3
+
4
+ class AlwaysConnectedWatchFilter:
5
+ """
6
+ For always-connected watches, limit the connection frequency to once every 6 hours.
7
+ Otherwise, they may block other watches from connecting.
8
+ """
9
+
10
+ def __init__(self):
11
+ self.last_connected_times = {}
12
+
13
+ def connection_filter(self, watch_name):
14
+ watch = watch_info.lookup_watch_info(watch_name)
15
+
16
+ if not watch["alwaysConnected"]:
17
+ # not always connected - allow...
18
+ return True
19
+
20
+ last_time = self.last_connected_times.get(watch_name)
21
+ now = time.time()
22
+
23
+ if last_time is None:
24
+ # connected for the first time - allow...
25
+ self.update_connection_time(watch_name=watch_name)
26
+ return True
27
+
28
+ elapsed = now - last_time
29
+ if elapsed > 6 * 3600:
30
+ # last connected more than 6 hours ago - allow...
31
+ self.update_connection_time(watch_name=watch_name)
32
+ return True
33
+
34
+ # last connected less than 6 hours ago - deny...
35
+ return False
36
+
37
+ def update_connection_time(self, watch_name):
38
+ self.last_connected_times[watch_name.strip()] = time.time()
39
+
40
+ always_connected_watch_filter = AlwaysConnectedWatchFilter()
@@ -33,15 +33,13 @@ class Connection:
33
33
  for char in service.characteristics:
34
34
  self.characteristics_map[char.uuid] = char.uuid # Store in map
35
35
 
36
- logger.info(f"Characteristics map initialized: {self.characteristics_map}")
37
-
38
- async def connect(self, excluded_watches: list[str] | None = None) -> bool:
36
+ async def connect(self, watch_filter=None) -> bool:
39
37
  try:
40
38
  # Scan for device if address not provided
41
39
  if self.address == None:
42
40
  device = await scanner.scan(
43
41
  device_address=self.address if self.address else None,
44
- excluded_watches=excluded_watches
42
+ watch_filter=watch_filter
45
43
  )
46
44
  if device is None:
47
45
  logger.info("No G-Shock device found or name matches excluded watches.")
@@ -90,8 +88,10 @@ class Connection:
90
88
  )
91
89
  return
92
90
 
91
+ responseType = True if handle == 0x0E else False
92
+
93
93
  await self.client.write_gatt_char(
94
- uuid, to_casio_cmd(data)
94
+ uuid, to_casio_cmd(data), response=responseType
95
95
  )
96
96
 
97
97
  except Exception as e:
@@ -455,3 +455,6 @@ class GshockAPI:
455
455
  encrypted_buffer = AppNotificationIO.xor_encode_buffer(encoded_buffer)
456
456
  await self.connection.write(0xD, encrypted_buffer)
457
457
 
458
+ # Note: replace all logging calls with 'print' or minimal logger.
459
+ # Ensure your async calls and message_dispatcher methods are ported or stubbed for MicroPython.
460
+
@@ -8,7 +8,6 @@ from gshock_api.casio_constants import CasioConstants
8
8
 
9
9
  CHARACTERISTICS = CasioConstants.CHARACTERISTICS
10
10
 
11
-
12
11
  class DtsState(IntEnum):
13
12
  ZERO = 0
14
13
  TWO = 2
@@ -17,11 +17,10 @@ class Scanner:
17
17
  async def scan(
18
18
  self,
19
19
  device_address: str | None = None,
20
- excluded_watches: list[str] | None = None,
20
+ watch_filter = None,
21
21
  max_retries: int = 60, # Optional: to prevent infinite loops
22
22
  ) -> BLEDevice | None:
23
23
  scanner = BleakScanner()
24
- excluded_watches = excluded_watches or []
25
24
 
26
25
  retries = 0
27
26
  device = None
@@ -39,9 +38,8 @@ class Scanner:
39
38
  return False
40
39
 
41
40
  is_casio = parts[0].lower() == "casio"
42
- is_excluded = len(parts) > 1 and parts[1] in excluded_watches
43
-
44
- return is_casio and not is_excluded
41
+ passed = is_casio and (watch_filter is None or watch_filter(device.name))
42
+ return passed
45
43
 
46
44
  device = await scanner.find_device_by_filter(
47
45
  casio_filter,
@@ -1,7 +1,6 @@
1
- from enum import Enum
2
1
  from gshock_api.logger import logger
3
2
 
4
- class WATCH_MODEL(Enum):
3
+ class WATCH_MODEL:
5
4
  GA = 1
6
5
  GW = 2
7
6
  DW = 3
@@ -17,6 +16,7 @@ class WATCH_MODEL(Enum):
17
16
  GB = 13
18
17
  GM = 14
19
18
  ABL = 15
19
+ DW_H = 16
20
20
  UNKNOWN = 20
21
21
 
22
22
  class WatchInfo:
@@ -167,6 +167,7 @@ class WatchInfo:
167
167
  "longLightDuration": "3s",
168
168
  "worldCities": False,
169
169
  "temperature": False,
170
+ "alwaysConnected": True,
170
171
  },
171
172
  {
172
173
  "model": WATCH_MODEL.ECB,
@@ -185,6 +186,23 @@ class WatchInfo:
185
186
  "hasPowerSavingMode": False,
186
187
  "hasDnD": True
187
188
  },
189
+ {
190
+ "model": WATCH_MODEL.DW_H,
191
+ "worldCitiesCount": 2,
192
+ "dstCount": 1,
193
+ "alarmCount": 5,
194
+ "hasAutoLight": True,
195
+ "hasReminders": False,
196
+ "shortLightDuration": "1.5s",
197
+ "longLightDuration": "3s",
198
+ "worldCities": True,
199
+ "temperature": False,
200
+ "hasBatteryLevel": False,
201
+ "alwaysConnected": True,
202
+ "findButtonUserDefined": True,
203
+ "hasPowerSavingMode": False,
204
+ "hasDnD": True
205
+ },
188
206
  {
189
207
  "model": WATCH_MODEL.UNKNOWN,
190
208
  "worldCitiesCount": 2,
@@ -201,32 +219,63 @@ class WatchInfo:
201
219
  self.model_map = {entry["model"]: entry for entry in self.models}
202
220
 
203
221
  def set_name_and_model(self, name):
204
- self.name = name
205
- self.shortName = None
206
- self.model = WATCH_MODEL.UNKNOWN
222
+ details = self._resolve_watch_details(name)
223
+ if not details:
224
+ return
225
+
226
+ # Persist fields
227
+ self.name = details["name"]
228
+ self.shortName = details["shortName"]
229
+ self.model = details["model"]
230
+ self.hasReminders = details["hasReminders"]
231
+ self.hasAutoLight = details["hasAutoLight"]
232
+ self.alarmCount = details["alarmCount"]
233
+ self.worldCitiesCount = details["worldCitiesCount"]
234
+ self.dstCount = details["dstCount"]
235
+ self.shortLightDuration = details["shortLightDuration"]
236
+ self.longLightDuration = details["longLightDuration"]
237
+ self.weekLanguageSupported = details["weekLanguageSupported"]
238
+ self.worldCities = details["worldCities"]
239
+ self.temperature = details["temperature"]
240
+ self.batteryLevelLowerLimit= details["batteryLevelLowerLimit"]
241
+ self.batteryLevelUpperLimit= details["batteryLevelUpperLimit"]
242
+ self.alwaysConnected = details["alwaysConnected"]
243
+ self.findButtonUserDefined = details["findButtonUserDefined"]
244
+ self.hasPowerSavingMode = details["hasPowerSavingMode"]
245
+ self.hasDnD = details["hasDnD"]
246
+ self.hasBatteryLevel = details["hasBatteryLevel"]
247
+ self.hasWorldCities = details["hasWorldCities"]
207
248
 
208
- parts = self.name.split(" ")
209
- if len(parts) > 1:
210
- self.shortName = parts[1]
249
+ def lookup_watch_info(self, name):
250
+ # Public non-destructive lookup
251
+ return self._resolve_watch_details(name)
211
252
 
212
- if not self.shortName:
213
- return
253
+ def _resolve_watch_details(self, name):
254
+ # Internal method for lookup logic
255
+ shortName = None
256
+ model = WATCH_MODEL.UNKNOWN
257
+
258
+ parts = name.split(" ")
259
+ if len(parts) > 1:
260
+ shortName = parts[1] # instead of `parts`
261
+ if not shortName:
262
+ return None # Could return a dict of defaults or None
214
263
 
215
- # Special case: exact match for ECB models
216
- if self.shortName in {"ECB-10", "ECB-20", "ECB-30"}:
217
- self.model = WATCH_MODEL.ECB
218
- elif self.shortName.startswith("ABL"): # ABL-100WE
219
- self.model = WATCH_MODEL.ABL
220
- elif self.shortName.startswith("GST"): # GST-B100
221
- self.model = WATCH_MODEL.GST
264
+ if shortName in {"ECB-10", "ECB-20", "ECB-30"}:
265
+ model = WATCH_MODEL.ECB
266
+ elif shortName.startswith("ABL"):
267
+ model = WATCH_MODEL.ABL
268
+ elif shortName.startswith("GST"):
269
+ model = WATCH_MODEL.GST
222
270
  else:
223
- # Ordered prefix-to-model mapping (longer prefixes first)
224
271
  prefix_map = [
225
272
  ("MSG", WATCH_MODEL.MSG),
226
273
  ("GPR", WATCH_MODEL.GPR),
274
+ ("GM-B2100", WATCH_MODEL.GA),
227
275
  ("GBM", WATCH_MODEL.GA),
228
276
  ("GBD", WATCH_MODEL.GBD),
229
277
  ("GMW", WATCH_MODEL.GMW),
278
+ ("DW-H", WATCH_MODEL.DW_H),
230
279
  ("DW", WATCH_MODEL.DW),
231
280
  ("GA", WATCH_MODEL.GA),
232
281
  ("GB", WATCH_MODEL.GB),
@@ -235,35 +284,37 @@ class WatchInfo:
235
284
  ("MRG", WATCH_MODEL.MRG),
236
285
  ("ABL", WATCH_MODEL.ABL),
237
286
  ]
238
-
239
- for prefix, model in prefix_map:
240
- if self.shortName.startswith(prefix):
241
- self.model = model
287
+ for prefix, m in prefix_map:
288
+ if shortName.startswith(prefix):
289
+ model = m
242
290
  break
243
291
 
244
- model_info = self.model_map.get(self.model)
245
- if not model_info:
246
- return
247
-
248
- # Set attributes with defaults
249
- self.hasReminders = model_info.get("hasReminders", False)
250
- self.hasAutoLight = model_info.get("hasAutoLight", False)
251
- self.alarmCount = model_info.get("alarmCount", 0)
252
- self.worldCitiesCount = model_info.get("worldCitiesCount", 0)
253
- self.dstCount = model_info.get("dstCount", 0)
254
- self.shortLightDuration = model_info.get("shortLightDuration", 0)
255
- self.longLightDuration = model_info.get("longLightDuration", 0)
256
- self.weekLanguageSupported = model_info.get("weekLanguageSupported", True)
257
- self.worldCities = model_info.get("worldCities", True)
258
- self.temperature = model_info.get("temperature", True)
259
- self.batteryLevelLowerLimit= model_info.get("batteryLevelLowerLimit", 15)
260
- self.batteryLevelUpperLimit= model_info.get("batteryLevelUpperLimit", 20)
261
- self.alwaysConnected = model_info.get("alwaysConnected", False)
262
- self.findButtonUserDefined = model_info.get("findButtonUserDefined", False)
263
- self.hasPowerSavingMode = model_info.get("hasPowerSavingMode", False)
264
- self.hasDnD = model_info.get("hasDnD", False)
265
- self.hasBatteryLevel = model_info.get("hasBatteryLevel", False)
266
- self.hasWorldCities = model_info.get("hasWorldCities", True)
292
+ model_info = self.model_map.get(model, {})
293
+ computed = {
294
+ "name": name,
295
+ "shortName": shortName,
296
+ "model": model,
297
+ # Use model_info for each property
298
+ "hasReminders": model_info.get("hasReminders", False),
299
+ "hasAutoLight": model_info.get("hasAutoLight", False),
300
+ "alarmCount": model_info.get("alarmCount", 0),
301
+ "worldCitiesCount": model_info.get("worldCitiesCount", 0),
302
+ "dstCount": model_info.get("dstCount", 0),
303
+ "shortLightDuration": model_info.get("shortLightDuration", ""),
304
+ "longLightDuration": model_info.get("longLightDuration", ""),
305
+ "weekLanguageSupported": model_info.get("weekLanguageSupported", True),
306
+ "worldCities": model_info.get("worldCities", True),
307
+ "temperature": model_info.get("temperature", True),
308
+ "batteryLevelLowerLimit": model_info.get("batteryLevelLowerLimit", 15),
309
+ "batteryLevelUpperLimit": model_info.get("batteryLevelUpperLimit", 20),
310
+ "alwaysConnected": model_info.get("alwaysConnected", False),
311
+ "findButtonUserDefined": model_info.get("findButtonUserDefined", False),
312
+ "hasPowerSavingMode": model_info.get("hasPowerSavingMode", False),
313
+ "hasDnD": model_info.get("hasDnD", False),
314
+ "hasBatteryLevel": model_info.get("hasBatteryLevel", False),
315
+ "hasWorldCities": model_info.get("hasWorldCities", True),
316
+ }
317
+ return computed
267
318
 
268
319
  def set_address(self, address):
269
320
  self.address = address
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gshock_api
3
- Version: 2.0.24
3
+ Version: 2.0.26
4
4
  Summary: Pythgon API for GShock Watches using BLE
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: Ivo Zivkov
@@ -5,6 +5,7 @@ setup.cfg
5
5
  setup.py
6
6
  src/gshock_api/__init__.py
7
7
  src/gshock_api/alarms.py
8
+ src/gshock_api/always_connected_watch_filter.py
8
9
  src/gshock_api/app_notification.py
9
10
  src/gshock_api/cancelable_result.py
10
11
  src/gshock_api/casio_constants.py
File without changes
File without changes
File without changes
@@ -17,7 +17,6 @@ from gshock_api.utils import (
17
17
 
18
18
  CHARACTERISTICS = CasioConstants.CHARACTERISTICS
19
19
 
20
-
21
20
  class ReminderMasks:
22
21
  YEARLY_MASK = 0b00001000
23
22
  MONTHLY_MASK = 0b00010000
@@ -33,7 +32,6 @@ class ReminderMasks:
33
32
 
34
33
  ENABLED_MASK = 0b00000001
35
34
 
36
-
37
35
  class EventsIO:
38
36
  result: CancelableResult = None
39
37
  connection = None
@@ -184,6 +182,7 @@ class EventsIO:
184
182
  title_byte_arr += bytearray([index + 1])
185
183
  title_byte_arr += title
186
184
  title_byte_arr_to_send = to_compact_string(to_hex_string(title_byte_arr))
185
+
187
186
  await EventsIO.connection.write(0x000E, title_byte_arr_to_send)
188
187
 
189
188
  reminder_time_byte_arr = bytearray([])
@@ -195,6 +194,7 @@ class EventsIO:
195
194
  reminder_time_byte_arr_to_send = to_compact_string(
196
195
  to_hex_string(bytearray(reminder_time_byte_arr))
197
196
  )
197
+
198
198
  await EventsIO.connection.write(0x000E, reminder_time_byte_arr_to_send)
199
199
 
200
200
  @staticmethod