Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor pysmartthings to be more typesafe #131

Draft
wants to merge 62 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f7f2ac3
Make typesafe
joostlek Feb 7, 2025
21da453
Make typesafe
joostlek Feb 7, 2025
ceb0ddf
Make typesafe
joostlek Feb 7, 2025
6fb866f
Make typesafe
joostlek Feb 7, 2025
8ce1ad3
Make typesafe
joostlek Feb 7, 2025
8efa1dc
Make typesafe
joostlek Feb 7, 2025
a2f779f
Make typesafe
joostlek Feb 7, 2025
a71bcd9
Make typesafe
joostlek Feb 7, 2025
ae33153
Make typesafe
joostlek Feb 8, 2025
9b2ee0a
Make typesafe
joostlek Feb 8, 2025
779221b
Make typesafe
joostlek Feb 8, 2025
a2e827d
Make typesafe
joostlek Feb 8, 2025
677e1c1
Make typesafe
joostlek Feb 8, 2025
d8353e3
Make typesafe
joostlek Feb 8, 2025
e0fe482
Make typesafe
joostlek Feb 8, 2025
755e83e
Make typesafe
joostlek Feb 9, 2025
4774549
Make typesafe
joostlek Feb 9, 2025
a803b2e
Make typesafe
joostlek Feb 9, 2025
598572d
Make typesafe
joostlek Feb 9, 2025
614ef05
Make typesafe
joostlek Feb 9, 2025
4236d82
Make typesafe
joostlek Feb 9, 2025
56cecdc
Make typesafe
joostlek Feb 9, 2025
82f215d
Make typesafe
joostlek Feb 9, 2025
8bd8e59
Make typesafe
joostlek Feb 9, 2025
7769aee
Make typesafe
joostlek Feb 9, 2025
5f58d86
Make typesafe
joostlek Feb 9, 2025
c871d37
Make typesafe
joostlek Feb 9, 2025
c625d2b
Make typesafe
joostlek Feb 9, 2025
5363eb7
Make typesafe
joostlek Feb 9, 2025
fece016
Make typesafe
joostlek Feb 9, 2025
e7901b1
Make typesafe
joostlek Feb 9, 2025
3fdfa90
Make typesafe
joostlek Feb 9, 2025
8ebc720
Make typesafe
joostlek Feb 9, 2025
0fe3c00
Make typesafe
joostlek Feb 9, 2025
6c5fa4a
Add Arlo Pro 3
joostlek Feb 9, 2025
198b88d
Add switch level thing
joostlek Feb 9, 2025
cc05bc1
Add centralite
joostlek Feb 9, 2025
e651771
ZOOOOOZ
joostlek Feb 9, 2025
8de85bc
Add yale and arlo and multipurpose_sensor
joostlek Feb 9, 2025
2ad40e3
Add 27 smart monitor
joostlek Feb 9, 2025
9cf2707
Add more devices
joostlek Feb 9, 2025
71b0cb0
Fix formatting
joostlek Feb 9, 2025
6578c7b
Add devices
joostlek Feb 9, 2025
b7ca4ac
prettier
joostlek Feb 9, 2025
4709b65
Add more devices
joostlek Feb 9, 2025
c72e484
Add command
joostlek Feb 9, 2025
8f1a5ca
Add command
joostlek Feb 9, 2025
8104f52
Add command
joostlek Feb 9, 2025
6751cff
Add command
joostlek Feb 9, 2025
c41f13a
Add command
joostlek Feb 9, 2025
658401d
Add device soundbar_hw_q80_r (#1)
PiotrMachowski Feb 10, 2025
f85816d
Add command
joostlek Feb 10, 2025
4ff80c7
Add command
joostlek Feb 10, 2025
07f1ebd
Add command
joostlek Feb 10, 2025
5bc0eb1
Add devices
joostlek Feb 10, 2025
0271607
Add devices
joostlek Feb 10, 2025
fb5afc7
Add devices
joostlek Feb 10, 2025
0c24580
Add devices
joostlek Feb 10, 2025
34e68f1
Add devices
joostlek Feb 10, 2025
bd550e4
Add devices
joostlek Feb 10, 2025
63418b6
Add devices
joostlek Feb 10, 2025
9087f3d
Add devices
joostlek Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ repos:
types: [text]
exclude: ^poetry\.lock$
entry: poetry run codespell
args:
- --ignore-words-list=unx
- id: detect-private-key
name: 🕵️ Detect Private Keys
language: system
Expand Down
204 changes: 191 additions & 13 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ packages = [
python = "^3.11"
aiohttp = ">=3.0.0"
yarl = ">=1.6.0"
mashumaro = "^3.11"
orjson = "^3.9.10"

[tool.poetry.group.dev.dependencies]
codespell = "2.4.1"
Expand All @@ -44,6 +46,15 @@ pre-commit = "4.1.0"
pre-commit-hooks = "5.0.0"
yamllint = "1.35.1"
ruff = "0.9.5"
syrupy = "^4.8.1"
aioresponses = "^0.7.8"

[tool.poetry.group.cli]
optional = true

[tool.poetry.group.cli.dependencies]
treelib = "^1.7.0"
pyperclip = "^1.9.0"

[tool.poetry.urls]
"Bug Tracker" = "https://github.com/pySmartThings/pysmartthings/issues"
Expand Down Expand Up @@ -120,6 +131,7 @@ asyncio_mode = "auto"
ignore = [
"ANN401", # Opinioated warning on disallowing dynamically typed expressions
"DTZ005",
"ERA001",
"PLR0913",
"TRY003",
"FBT002",
Expand Down
1 change: 1 addition & 0 deletions script/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Script module."""
19 changes: 0 additions & 19 deletions script/generate_attributes.py

This file was deleted.

19 changes: 0 additions & 19 deletions script/generate_capabilities.py

This file was deleted.

90 changes: 90 additions & 0 deletions script/process_device_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Process the device status JSON file to generate a tree of a device status."""

import json
from pathlib import Path
import re
import sys

from treelib import Tree

from pysmartthings.models import CAPABILITY_ATTRIBUTES, Attribute, Capability


def main() -> int: # noqa: PLR0912
"""Run the script."""
if len(sys.argv) != 2:
print("Usage: python process_device_status.py <filename>")
return 1
filename = sys.argv[1]
print(f"Processing {filename}")
with Path(filename).open(encoding="utf-8") as file:
data = json.load(file)
components = data["components"]
tree = Tree()
found_capabilities = {}
found_attributes = {}
missing_attribute_mapping = {}
tree.create_node(filename, "root")
for component_name, capabilities in components.items(): # pylint: disable=too-many-nested-blocks
tree.create_node(component_name, component_name, parent="root")
for capability_name, attributes in capabilities.items():
if capability_name not in Capability:
found_capabilities[capability_name] = (
re.sub(r"(?<!^)(?=[A-Z])", "_", capability_name)
.upper()
.replace(".", "_")
.replace("SAMSUNGCE", "SAMSUNG_CE")
)
missing_attribute_mapping[capability_name] = []
tree.create_node(
capability_name,
f"{component_name}-{capability_name}",
parent=component_name,
)
for attribute in attributes:
attribute_name: str
if attribute not in Attribute:
found_attributes[attribute] = attribute_name = re.sub(
r"(?<!^)(?=[A-Z])", "_", attribute
).upper()
else:
attribute_name = Attribute(attribute).name
attribute_name = f"Attribute.{attribute_name}"
if capability_name in CAPABILITY_ATTRIBUTES:
if attribute not in CAPABILITY_ATTRIBUTES[capability_name]:
if capability_name not in missing_attribute_mapping:
missing_attribute_mapping[capability_name] = []
if (
attribute_name
not in missing_attribute_mapping[capability_name]
):
missing_attribute_mapping[capability_name].append(
attribute_name
)
else:
if capability_name not in missing_attribute_mapping:
missing_attribute_mapping[capability_name] = []
if attribute_name not in missing_attribute_mapping[capability_name]:
missing_attribute_mapping[capability_name].append(
attribute_name
)
tree.create_node(
attribute,
f"{component_name}-{capability_name}-{attribute}",
parent=f"{component_name}-{capability_name}",
)
print(tree.show(stdout=False))
if found_capabilities:
print("\nFound capabilities:")
for capability_name, slug in found_capabilities.items():
print(f'{slug} = "{capability_name}"')
if found_attributes:
print("\nFound attributes:")
for attribute_name, slug in found_attributes.items():
print(f'{slug} = "{attribute_name}"')
print(json.dumps(missing_attribute_mapping, indent=4))
return 0


if __name__ == "__main__":
sys.exit(main())
22 changes: 22 additions & 0 deletions script/sort_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Sort the Attribute enum."""

import pyperclip

from pysmartthings.models import Attribute


def main() -> int:
"""Run the script."""
attributes = {attr.name: attr for attr in Attribute}
attributes = dict(sorted(attributes.items()))
result = "class Attribute(StrEnum):"
result += '\n """Attribute model."""\n\n'
for name, attribute in attributes.items():
result += f' {name} = "{attribute.value}"\n'
pyperclip.copy(result)
print(result)
return 0


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions script/sort_capability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Script to sort the capability constants."""

import pyperclip

from pysmartthings.models import Capability


def main() -> int:
"""Run the script."""
capabilities = {}
dot_capabilities = {}
for capability in Capability:
if "." in capability.value:
category = capability.value.split(".")[0]
if category not in dot_capabilities:
dot_capabilities[category] = {}
dot_capabilities[category][capability.value] = capability
else:
capabilities[capability.value] = capability
capabilities = dict(sorted(capabilities.items()))
result = "class Capability(StrEnum):"
result += '\n """Capability model."""\n\n'
for name, capability in capabilities.items():
result += f' {capability.name} = "{name}"\n'
for category_capabilities in dot_capabilities.values():
result += "\n"
capabilities = dict(sorted(category_capabilities.items()))
for name, capability in capabilities.items():
result += f' {capability.name} = "{name}"\n'
print(result)
pyperclip.copy(result)
return 0


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions src/pysmartthings/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

__title__ = "pysmartthings"
__version__ = "0.7.8"

API_BASE = "api.smartthings.com"
2 changes: 1 addition & 1 deletion src/pysmartthings/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def apply_data(self, data: dict) -> None:
self._device_type_name = dth.get("deviceTypeName")
self._device_type_network = dth.get("deviceNetworkType")

def get_capability(self, *capabilities: list[dict[str, Any]]) -> str | None:
def get_capability(self, *capabilities: str) -> str | None:
"""Return the first capability held by the device."""
for capability in capabilities:
if capability in self._capabilities:
Expand Down
21 changes: 21 additions & 0 deletions src/pysmartthings/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Asynchronous Python client for SmartThings."""


class SmartThingsError(Exception):
"""Generic exception."""


class SmartThingsConnectionError(SmartThingsError):
"""SmartThings connection exception."""


class SmartThingsAuthenticationFailedError(SmartThingsError):
"""SmartThings authentication failed exception."""


class SmartThingsNotFoundError(SmartThingsError):
"""SmartThings not found exception."""


class SmartThingsRateLimitError(SmartThingsError):
"""SmartThings rate limit exception."""
Loading