File: crownstones.py

package info (click to toggle)
python-crownstone-cloud 1.4.11-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 240 kB
  • sloc: python: 1,126; makefile: 4
file content (213 lines) | stat: -rw-r--r-- 7,309 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
"""Crownstone handler for Crownstone cloud data"""
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Iterator

from crownstone_cloud.const import DIMMING_ABILITY
from crownstone_cloud.exceptions import AbilityError, CrownstoneAbilityError

if TYPE_CHECKING:
    from crownstone_cloud.cloud import CrownstoneCloud


class Crownstones:
    """Handler for the crownstones of a sphere."""

    def __init__(self, cloud: CrownstoneCloud, sphere_id: str) -> None:
        """Initialization."""
        self.cloud = cloud
        self.sphere_id = sphere_id
        self.data: dict[str, Crownstone] = {}

    def __iter__(self) -> Iterator[Crownstone]:
        """Iterate over crownstones."""
        return iter(self.data.values())

    async def async_update_crownstone_data(self) -> None:
        """Get the crownstones data from the cloud."""
        # include abilities and current switch state in the request
        data_filter = {"include": ["currentSwitchState", {"abilities": "properties"}]}
        # request data
        cloud_data: list[dict[str, Any]] = await self.cloud.request_handler.get(
            "Spheres", "ownedStones", data_filter=data_filter, model_id=self.sphere_id
        )
        # process items
        removed_items: list[str] = []
        new_items: list[str] = []
        for crownstone in cloud_data:
            crownstone_id: str = crownstone["id"]
            exists = self.data.get(crownstone_id)
            # check if the crownstone already exists
            # it is important that we don't throw away existing objects,
            # as they need to remain functional
            if exists:
                # update data and update abilities
                self.data[crownstone_id].data = crownstone
            else:
                # add new Crownstone
                self.data[crownstone_id] = Crownstone(self.cloud, crownstone)

            # update the abilities of the Crownstone from the data
            self.data[crownstone_id].update_abilities()

            # generate list with new id's to check with the existing id's
            new_items.append(crownstone_id)

        # check for removed items
        for crownstone_id in self.data:
            if crownstone_id not in new_items:
                removed_items.append(crownstone_id)

        # remove items from dict
        for crownstone_id in removed_items:
            del self.data[crownstone_id]

    def find(self, crownstone_name: str) -> Crownstone | None:
        """Search for a crownstone by name and return crownstone object if found."""
        for crownstone in self.data.values():
            if crownstone_name == crownstone.name:
                return crownstone

        return None

    def find_by_id(self, crownstone_id: str) -> Crownstone | None:
        """Search for a crownstone by id and return crownstone object if found."""
        return self.data.get(crownstone_id)

    def find_by_uid(self, crownstone_uid: int) -> Crownstone | None:
        """Search for a crownstone by uid and return crownstone object if found."""
        for crownstone in self.data.values():
            if crownstone_uid == crownstone.unique_id:
                return crownstone

        return None


class CrownstoneAbility:
    """Represents a Crownstone Ability"""

    def __init__(self, data: dict[str, Any]) -> None:
        """Initialization"""
        self.data = data
        self.is_enabled: bool = self.data["enabled"]

    @property
    def type(self) -> str:
        """Return the ability type."""
        return str(self.data["type"])

    @property
    def ability_id(self) -> str:
        """Return the ability id."""
        return str(self.data["id"])

    @property
    def crownstone_id(self) -> str:
        """Return the Crownstone id."""
        return str(self.data["stoneId"])


class Crownstone:
    """Represents a Crownstone"""

    def __init__(self, cloud: CrownstoneCloud, data: dict[str, Any]) -> None:
        """Initialization."""
        self.cloud = cloud
        self.data = data
        self.abilities: dict[str, CrownstoneAbility] = {}
        # Not cloud data, store your own data here (from Crownstone USB)
        self.power_usage = 0
        self.energy_usage = 0

    @property
    def name(self) -> str:
        """Return the name of this Crownstone."""
        return str(self.data["name"])

    @property
    def unique_id(self) -> int:
        """Return the unique_id of this Crownstone."""
        return int(self.data["uid"])

    @property
    def cloud_id(self) -> str:
        """Return the cloud id of this Crownstone."""
        return str(self.data["id"])

    @property
    def type(self) -> str:
        """Return the Crownstone type."""
        return str(self.data["type"])

    @property
    def sw_version(self) -> str:
        """Return the firmware version of this Crownstone."""
        return str(self.data["firmwareVersion"])

    @property
    def icon(self) -> str:
        """Return the icon of this Crownstone."""
        return str(self.data["icon"])

    @property
    def state(self) -> int:
        """Return the last reported switch state (0 - 100) of this Crownstone."""
        return int(self.data["currentSwitchState"]["switchState"])

    @state.setter
    def state(self, value: int) -> None:
        """
        Set a new switch state (0 - 100) of this Crownstone.

        Only updates state in cloud, does not send a switch command to the actual Crownstone.
        """
        self.data["currentSwitchState"]["switchState"] = value

    def update_abilities(self) -> None:
        """Add/update the abilities for this Crownstone."""
        for ability in self.data["abilities"]:
            self.abilities[ability["type"]] = CrownstoneAbility(ability)

    async def async_turn_on(self) -> None:
        """
        Turn this crownstone on.

        This method is a coroutine.
        """
        # send a command to the cloud to turn the Crownstone on.
        await self.cloud.request_handler.post(
            "Stones", "switch", model_id=self.cloud_id, json={"type": "TURN_ON"}
        )

    async def async_turn_off(self) -> None:
        """
        Turn this crownstone off.

        This method is a coroutine.
        """
        # send a command to the cloud to turn the Crownstone off.
        await self.cloud.request_handler.post(
            "Stones", "switch", model_id=self.cloud_id, json={"type": "TURN_OFF"}
        )

    async def async_set_brightness(self, brightness: int) -> None:
        """
        Set the brightness of this crownstone, if dimming enabled.

        :param brightness: brightness value between (0 - 100)

        This method is a coroutine.
        """
        # check dimming availability & value, and send a command to the cloud to dim the Crownstone.
        if self.abilities[DIMMING_ABILITY].is_enabled:
            if brightness < 0 or brightness > 100:
                raise ValueError("Enter a value between 0 and 100")

            await self.cloud.request_handler.post(
                "Stones",
                "switch",
                model_id=self.cloud_id,
                json={"type": "PERCENTAGE", "percentage": brightness},
            )
        else:
            raise CrownstoneAbilityError(AbilityError["NOT_ENABLED"])