-
-
Notifications
You must be signed in to change notification settings - Fork 54
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
Add hotspot vouchers #554
Add hotspot vouchers #554
Changes from 51 commits
5b1131f
3086a3e
b88738a
54d1fe4
45e328f
89065c7
5e17d97
759190f
2ac284f
8ed62ca
d79d090
d08890b
15ec646
06d9956
04db869
5e2d896
b66e569
31acc29
81e897c
4fc8417
e8b2933
76ed566
a973d1a
5c3b32d
0a98af2
d8e1525
47174da
19b5280
8c35b4b
fe0532e
310dde4
3a9d6fa
de385c9
ea5bfb1
a8dfd1c
0ccbe77
64fab7b
4eb795b
d266d63
350927e
f82d9fe
6e7770c
bdad257
e1f4174
4e6741a
b6ead16
2bc1922
77d9bc0
47e13c2
43e7f78
632b764
e960312
c5d3e1a
137e644
29d99e2
688eb20
b5de641
eefe2c1
88105a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,46 @@ | ||||||||
"""Hotspot vouchers as part of a UniFi network.""" | ||||||||
|
||||||||
from ..models.api import TypedApiResponse | ||||||||
from ..models.message import MessageKey | ||||||||
from ..models.voucher import ( | ||||||||
Voucher, | ||||||||
VoucherCreateRequest, | ||||||||
VoucherDeleteRequest, | ||||||||
VoucherListRequest, | ||||||||
) | ||||||||
from .api_handlers import APIHandler | ||||||||
|
||||||||
|
||||||||
class Vouchers(APIHandler[Voucher]): | ||||||||
"""Represents Hotspot vouchers.""" | ||||||||
|
||||||||
obj_id_key = "_id" | ||||||||
item_cls = Voucher | ||||||||
process_messages = (MessageKey.VOUCHER_CREATED,) | ||||||||
remove_messages = (MessageKey.VOUCHER_DELETED,) | ||||||||
api_request = VoucherListRequest.create() | ||||||||
|
||||||||
async def create(self, voucher: Voucher) -> TypedApiResponse: | ||||||||
"""Create voucher on controller.""" | ||||||||
return await self.controller.request( | ||||||||
VoucherCreateRequest.create( | ||||||||
number=1, | ||||||||
quota=voucher.quota, | ||||||||
expire_number=int( | ||||||||
voucher.duration.total_seconds() / 60 # Get minutes. | ||||||||
), | ||||||||
expire_unit=1, | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the default value is 1 for expire unit would this be ok, or is there a specific reason this is 1
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exire_unit is not an attribute of Voucher. 1 = minute, because expire_number came from voucher.duration in Seconds / 60 = Minutes The attributes for vouchers of the return values of the API are called differently than those for creating. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, yes indeed, thanks, as it has the default value assigned this can just be removed.
Suggested change
|
||||||||
usage_quota=voucher.qos_usage_quota, | ||||||||
rate_max_up=voucher.qos_rate_max_up, | ||||||||
rate_max_down=voucher.qos_rate_max_down, | ||||||||
note=voucher.note, | ||||||||
) | ||||||||
) | ||||||||
|
||||||||
async def delete(self, voucher: Voucher) -> TypedApiResponse: | ||||||||
"""Delete voucher from controller.""" | ||||||||
return await self.controller.request( | ||||||||
VoucherDeleteRequest.create( | ||||||||
obj_id=voucher.id, | ||||||||
) | ||||||||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
"""Hotspot vouchers as part of a UniFi network.""" | ||
|
||
from dataclasses import dataclass | ||
from datetime import datetime, timedelta | ||
from typing import NotRequired, Self, TypedDict | ||
|
||
from .api import ApiItem, ApiRequest | ||
|
||
|
||
class TypedVoucher(TypedDict): | ||
"""Voucher type definition.""" | ||
|
||
_id: str | ||
site_id: str | ||
note: NotRequired[str] | ||
code: str | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
quota: int | ||
duration: float | ||
qos_overwrite: NotRequired[bool] | ||
qos_usage_quota: NotRequired[str] | ||
qos_rate_max_up: NotRequired[int] | ||
qos_rate_max_down: NotRequired[int] | ||
used: int | ||
create_time: float | ||
start_time: NotRequired[float] | ||
end_time: NotRequired[float] | ||
for_hotspot: NotRequired[bool] | ||
admin_name: str | ||
status: str | ||
status_expires: float | ||
|
||
|
||
@dataclass | ||
class VoucherListRequest(ApiRequest): | ||
"""Request object for voucher list.""" | ||
|
||
@classmethod | ||
def create(cls) -> Self: | ||
"""Create voucher list request.""" | ||
return cls( | ||
method="get", | ||
path="/stat/voucher", | ||
) | ||
|
||
|
||
@dataclass | ||
class VoucherCreateRequest(ApiRequest): | ||
"""Request object for voucher create.""" | ||
|
||
@classmethod | ||
def create( | ||
cls, | ||
number: int, | ||
quota: int, | ||
expire_number: int, | ||
Kane610 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
expire_unit: int = 1, | ||
usage_quota: int | None = None, | ||
rate_max_up: int | None = None, | ||
rate_max_down: int | None = None, | ||
note: str | None = None, | ||
) -> Self: | ||
"""Create voucher create request. | ||
|
||
:param number: number of vouchers | ||
:param quota: number of using; 0 = unlimited | ||
:param expire_number: expiration of voucher per expire_unit | ||
:param expire_unit: scale of expire_number, 1 = minute, 60 = hour, 3600 = day | ||
:param usage_quota: quantity of bytes allowed in MB | ||
:param rate_max_up: up speed allowed in kbps | ||
:param rate_max_down: down speed allowed in kbps | ||
:param note: description | ||
""" | ||
data = { | ||
"cmd": "create-voucher", | ||
"n": number, | ||
"quota": quota, | ||
"expire_number": expire_number, | ||
"expire_unit": expire_unit, | ||
} | ||
if usage_quota: | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data["bytes"] = usage_quota | ||
if rate_max_up: | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data["up"] = rate_max_up | ||
if rate_max_down: | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data["down"] = rate_max_down | ||
if note: | ||
data["note"] = note | ||
|
||
return cls( | ||
method="post", | ||
path="/cmd/hotspot", | ||
data=data, | ||
) | ||
|
||
|
||
@dataclass | ||
class VoucherDeleteRequest(ApiRequest): | ||
"""Request object for voucher delete.""" | ||
|
||
@classmethod | ||
def create( | ||
cls, | ||
obj_id: str, | ||
) -> Self: | ||
"""Create voucher delete request.""" | ||
data = { | ||
"cmd": "delete-voucher", | ||
"_id": obj_id, | ||
} | ||
return cls( | ||
method="post", | ||
path="/cmd/hotspot", | ||
data=data, | ||
) | ||
|
||
|
||
class Voucher(ApiItem): | ||
"""Represents a voucher.""" | ||
|
||
raw: TypedVoucher | ||
|
||
@property | ||
def id(self) -> str: | ||
"""ID of voucher.""" | ||
return self.raw["_id"] | ||
|
||
@property | ||
def site_id(self) -> str: | ||
"""Site ID.""" | ||
return self.raw["site_id"] | ||
|
||
@property | ||
def note(self) -> str: | ||
"""Note describing voucher.""" | ||
return self.raw.get("note") or "" | ||
|
||
@property | ||
def code(self) -> str: | ||
"""Code in known format 00000-00000.""" | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if len(c := self.raw.get("code", "")) > 5: | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# API returns the code without a hyphen. But this is necessary. Separate the API string after the fifth digit. | ||
return f"{c[:5]}-{c[5:]}" | ||
return c | ||
|
||
@property | ||
def quota(self) -> int: | ||
"""Allowed usages (0 = unlimited).""" | ||
return self.raw["quota"] | ||
|
||
@property | ||
def duration(self) -> timedelta: | ||
"""Expiration of voucher.""" | ||
return timedelta(minutes=self.raw["duration"]) | ||
|
||
@property | ||
def qos_overwrite(self) -> bool: | ||
"""QoS defaults overwritten.""" | ||
return self.raw.get("qos_overwrite", False) | ||
|
||
@property | ||
def qos_usage_quota(self) -> int: | ||
"""Quantity of bytes allowed in MB.""" | ||
return int(self.raw.get("qos_usage_quota", 0)) | ||
ufozone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@property | ||
def qos_rate_max_up(self) -> int: | ||
"""Up speed allowed in kbps.""" | ||
return self.raw.get("qos_rate_max_up", 0) | ||
|
||
@property | ||
def qos_rate_max_down(self) -> int: | ||
"""Down speed allowed in kbps.""" | ||
return self.raw.get("qos_rate_max_down", 0) | ||
|
||
@property | ||
def used(self) -> int: | ||
"""Number of usages.""" | ||
return self.raw["used"] | ||
|
||
@property | ||
def create_time(self) -> datetime: | ||
"""Create datetime.""" | ||
return datetime.fromtimestamp(self.raw["create_time"]) | ||
|
||
@property | ||
def start_time(self) -> datetime | None: | ||
"""Start datetime of first usage.""" | ||
if "start_time" in self.raw: | ||
return datetime.fromtimestamp(self.raw["start_time"]) | ||
return None | ||
|
||
@property | ||
def end_time(self) -> datetime | None: | ||
"""End datetime of latest usage.""" | ||
if "end_time" in self.raw: | ||
return datetime.fromtimestamp(self.raw["end_time"]) | ||
return None | ||
|
||
@property | ||
def for_hotspot(self) -> bool: | ||
"""For hotspot.""" | ||
return self.raw.get("for_hotspot", False) | ||
|
||
@property | ||
def admin_name(self) -> str: | ||
"""Admin name.""" | ||
return self.raw["admin_name"] | ||
|
||
@property | ||
def status(self) -> str: | ||
"""Status.""" | ||
return self.raw["status"] | ||
|
||
@property | ||
def status_expires(self) -> timedelta | None: | ||
"""Status expires in seconds.""" | ||
if (status_expiry := self.raw["status_expires"]) > 0: | ||
return timedelta(seconds=status_expiry) | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Number is a required item to create an instance of Voucher, is there a reason 1 is hardcoded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Number is not an attribute of Voucher. Number is the number of vouchers to be create in one request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I would just add the default value to the create request and remove this instead.