pfau-occupancy 0.1.0__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.
- pfau_occupancy-0.1.0/PKG-INFO +62 -0
- pfau_occupancy-0.1.0/README.md +49 -0
- pfau_occupancy-0.1.0/pfau_occupancy/__init__.py +20 -0
- pfau_occupancy-0.1.0/pfau_occupancy/client.py +167 -0
- pfau_occupancy-0.1.0/pfau_occupancy/exceptions.py +18 -0
- pfau_occupancy-0.1.0/pfau_occupancy/models.py +51 -0
- pfau_occupancy-0.1.0/pfau_occupancy.egg-info/PKG-INFO +62 -0
- pfau_occupancy-0.1.0/pfau_occupancy.egg-info/SOURCES.txt +11 -0
- pfau_occupancy-0.1.0/pfau_occupancy.egg-info/dependency_links.txt +1 -0
- pfau_occupancy-0.1.0/pfau_occupancy.egg-info/requires.txt +1 -0
- pfau_occupancy-0.1.0/pfau_occupancy.egg-info/top_level.txt +1 -0
- pfau_occupancy-0.1.0/pyproject.toml +21 -0
- pfau_occupancy-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pfau-occupancy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client for Planet Fitness AU (PerfectGym) club occupancy
|
|
5
|
+
Author: Dan Morgan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dancmorgan/pfau-occupancy
|
|
8
|
+
Project-URL: Issues, https://github.com/dancmorgan/pfau-occupancy/issues
|
|
9
|
+
Keywords: planet fitness,perfectgym,home assistant,occupancy
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: aiohttp>=3.9
|
|
13
|
+
|
|
14
|
+
# pfau-occupancy
|
|
15
|
+
|
|
16
|
+
Async Python client for Planet Fitness Australia club occupancy, backing a Home
|
|
17
|
+
Assistant integration. Data comes from the member portal (PerfectGym
|
|
18
|
+
ClientPortal2), which exposes a live "members currently in club" count for every
|
|
19
|
+
club in a single authenticated call.
|
|
20
|
+
|
|
21
|
+
This library deliberately does only the API layer: login, session handling, and
|
|
22
|
+
fetching occupancy. All Home Assistant concerns (entities, config flow,
|
|
23
|
+
coordinator) live in the [pfau-occupancy-ha](https://github.com/dancmorgan/pfau-occupancy-ha)
|
|
24
|
+
integration that depends on this package.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pfau-occupancy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from pfau_occupancy import PlanetFitnessClient
|
|
37
|
+
|
|
38
|
+
async def main():
|
|
39
|
+
async with PlanetFitnessClient("you@example.com", "password") as client:
|
|
40
|
+
clubs = await client.async_get_clubs()
|
|
41
|
+
for club in clubs:
|
|
42
|
+
print(club.name, club.occupancy)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The client logs in automatically on first fetch and re-authenticates once if the
|
|
48
|
+
session expires, so a long-running poller keeps working without intervention.
|
|
49
|
+
|
|
50
|
+
## Data notes
|
|
51
|
+
|
|
52
|
+
The occupancy endpoint returns every club in one response. Each club carries a
|
|
53
|
+
name, address, a currently-in-club count, and a capacity limit (often null). The
|
|
54
|
+
response has no club ID, so the (slugified) club name is the stable identity used
|
|
55
|
+
for entity `unique_id`s. A club being renamed therefore looks like one club
|
|
56
|
+
disappearing and another appearing.
|
|
57
|
+
|
|
58
|
+
## Disclaimer
|
|
59
|
+
|
|
60
|
+
This uses the member portal's internal endpoints, which are undocumented and may
|
|
61
|
+
change or break without notice. It requires your own valid membership
|
|
62
|
+
credentials. Use it with your own account and keep polling gentle.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# pfau-occupancy
|
|
2
|
+
|
|
3
|
+
Async Python client for Planet Fitness Australia club occupancy, backing a Home
|
|
4
|
+
Assistant integration. Data comes from the member portal (PerfectGym
|
|
5
|
+
ClientPortal2), which exposes a live "members currently in club" count for every
|
|
6
|
+
club in a single authenticated call.
|
|
7
|
+
|
|
8
|
+
This library deliberately does only the API layer: login, session handling, and
|
|
9
|
+
fetching occupancy. All Home Assistant concerns (entities, config flow,
|
|
10
|
+
coordinator) live in the [pfau-occupancy-ha](https://github.com/dancmorgan/pfau-occupancy-ha)
|
|
11
|
+
integration that depends on this package.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pfau-occupancy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from pfau_occupancy import PlanetFitnessClient
|
|
24
|
+
|
|
25
|
+
async def main():
|
|
26
|
+
async with PlanetFitnessClient("you@example.com", "password") as client:
|
|
27
|
+
clubs = await client.async_get_clubs()
|
|
28
|
+
for club in clubs:
|
|
29
|
+
print(club.name, club.occupancy)
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The client logs in automatically on first fetch and re-authenticates once if the
|
|
35
|
+
session expires, so a long-running poller keeps working without intervention.
|
|
36
|
+
|
|
37
|
+
## Data notes
|
|
38
|
+
|
|
39
|
+
The occupancy endpoint returns every club in one response. Each club carries a
|
|
40
|
+
name, address, a currently-in-club count, and a capacity limit (often null). The
|
|
41
|
+
response has no club ID, so the (slugified) club name is the stable identity used
|
|
42
|
+
for entity `unique_id`s. A club being renamed therefore looks like one club
|
|
43
|
+
disappearing and another appearing.
|
|
44
|
+
|
|
45
|
+
## Disclaimer
|
|
46
|
+
|
|
47
|
+
This uses the member portal's internal endpoints, which are undocumented and may
|
|
48
|
+
change or break without notice. It requires your own valid membership
|
|
49
|
+
credentials. Use it with your own account and keep polling gentle.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""pfau_occupancy: async client for Planet Fitness AU club occupancy."""
|
|
2
|
+
from .client import PlanetFitnessClient, DEFAULT_BASE_URL
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
PlanetFitnessError,
|
|
5
|
+
PlanetFitnessAuthError,
|
|
6
|
+
PlanetFitnessConnectionError,
|
|
7
|
+
)
|
|
8
|
+
from .models import Club, slugify_club
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"PlanetFitnessClient",
|
|
14
|
+
"DEFAULT_BASE_URL",
|
|
15
|
+
"PlanetFitnessError",
|
|
16
|
+
"PlanetFitnessAuthError",
|
|
17
|
+
"PlanetFitnessConnectionError",
|
|
18
|
+
"Club",
|
|
19
|
+
"slugify_club",
|
|
20
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Async client for the Planet Fitness AU (PerfectGym ClientPortal2) portal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
PlanetFitnessAuthError,
|
|
11
|
+
PlanetFitnessConnectionError,
|
|
12
|
+
)
|
|
13
|
+
from .models import Club
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://planetfitness.perfectgym.com.au/clientportal2"
|
|
18
|
+
|
|
19
|
+
# Sent on every XHR by the browser. The backend checks x-requested-with;
|
|
20
|
+
# cp-lang / cp-mode are constants the portal expects.
|
|
21
|
+
_BASE_HEADERS = {
|
|
22
|
+
"x-requested-with": "XMLHttpRequest",
|
|
23
|
+
"cp-lang": "en",
|
|
24
|
+
"cp-mode": "desktop",
|
|
25
|
+
"accept": "application/json, text/plain, */*",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PlanetFitnessClient:
|
|
30
|
+
"""Talks to the member portal: login, then fetch club occupancy.
|
|
31
|
+
|
|
32
|
+
Holds one aiohttp session so the auth cookie persists across calls. Credentials
|
|
33
|
+
are kept so the client can transparently re-login when the session expires,
|
|
34
|
+
which for a long-running poller it eventually will.
|
|
35
|
+
|
|
36
|
+
The caller owns the aiohttp session lifecycle by passing one in (this is what
|
|
37
|
+
Home Assistant wants: it supplies its shared session). If none is given, one is
|
|
38
|
+
created and closed by `async with` / `close()`.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
email: str,
|
|
44
|
+
password: str,
|
|
45
|
+
session: aiohttp.ClientSession | None = None,
|
|
46
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._email = email
|
|
49
|
+
self._password = password
|
|
50
|
+
self._base_url = base_url.rstrip("/")
|
|
51
|
+
self._session = session
|
|
52
|
+
self._owns_session = session is None
|
|
53
|
+
self._authenticated = False
|
|
54
|
+
|
|
55
|
+
async def __aenter__(self) -> "PlanetFitnessClient":
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
59
|
+
await self.close()
|
|
60
|
+
|
|
61
|
+
async def close(self) -> None:
|
|
62
|
+
if self._owns_session and self._session is not None:
|
|
63
|
+
await self._session.close()
|
|
64
|
+
|
|
65
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
66
|
+
if self._session is None:
|
|
67
|
+
# cookie_jar persists the session cookie across requests.
|
|
68
|
+
self._session = aiohttp.ClientSession()
|
|
69
|
+
return self._session
|
|
70
|
+
|
|
71
|
+
def _headers(self) -> dict[str, str]:
|
|
72
|
+
return {
|
|
73
|
+
**_BASE_HEADERS,
|
|
74
|
+
"origin": "https://planetfitness.perfectgym.com.au",
|
|
75
|
+
"referer": self._base_url + "/",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async def async_login(self) -> dict[str, Any]:
|
|
79
|
+
"""Authenticate and return the member record.
|
|
80
|
+
|
|
81
|
+
Raises PlanetFitnessAuthError on bad credentials, PlanetFitnessConnectionError
|
|
82
|
+
on transport problems.
|
|
83
|
+
"""
|
|
84
|
+
session = self._get_session()
|
|
85
|
+
payload = {"RememberMe": True, "Login": self._email, "Password": self._password}
|
|
86
|
+
try:
|
|
87
|
+
async with session.post(
|
|
88
|
+
f"{self._base_url}/Auth/Login",
|
|
89
|
+
json=payload,
|
|
90
|
+
headers=self._headers(),
|
|
91
|
+
timeout=aiohttp.ClientTimeout(total=20),
|
|
92
|
+
) as resp:
|
|
93
|
+
if resp.status in (400, 401, 403):
|
|
94
|
+
raise PlanetFitnessAuthError(
|
|
95
|
+
f"Login rejected (HTTP {resp.status}); check credentials."
|
|
96
|
+
)
|
|
97
|
+
if resp.status != 200:
|
|
98
|
+
raise PlanetFitnessConnectionError(
|
|
99
|
+
f"Unexpected login status HTTP {resp.status}."
|
|
100
|
+
)
|
|
101
|
+
data = await resp.json()
|
|
102
|
+
except aiohttp.ClientError as err:
|
|
103
|
+
raise PlanetFitnessConnectionError(f"Login request failed: {err}") from err
|
|
104
|
+
|
|
105
|
+
member = _extract_member(data)
|
|
106
|
+
if not member:
|
|
107
|
+
# 200 but no member usually means the session cookie was not set.
|
|
108
|
+
raise PlanetFitnessAuthError("Login succeeded but no member returned.")
|
|
109
|
+
self._authenticated = True
|
|
110
|
+
_LOGGER.debug("Authenticated as member %s", member.get("Id"))
|
|
111
|
+
return member
|
|
112
|
+
|
|
113
|
+
async def async_get_clubs(self) -> list[Club]:
|
|
114
|
+
"""Fetch occupancy for all clubs.
|
|
115
|
+
|
|
116
|
+
Logs in automatically if not yet authenticated, and re-logs-in once if the
|
|
117
|
+
session has expired (detected as an auth failure mid-fetch).
|
|
118
|
+
"""
|
|
119
|
+
if not self._authenticated:
|
|
120
|
+
await self.async_login()
|
|
121
|
+
try:
|
|
122
|
+
return await self._fetch_clubs()
|
|
123
|
+
except PlanetFitnessAuthError:
|
|
124
|
+
# Session likely expired. Re-login once, then retry.
|
|
125
|
+
_LOGGER.debug("Session expired; re-authenticating.")
|
|
126
|
+
self._authenticated = False
|
|
127
|
+
await self.async_login()
|
|
128
|
+
return await self._fetch_clubs()
|
|
129
|
+
|
|
130
|
+
async def _fetch_clubs(self) -> list[Club]:
|
|
131
|
+
session = self._get_session()
|
|
132
|
+
try:
|
|
133
|
+
async with session.post(
|
|
134
|
+
f"{self._base_url}/Clubs/Clubs/GetMembersInClubs",
|
|
135
|
+
headers=self._headers(),
|
|
136
|
+
timeout=aiohttp.ClientTimeout(total=20),
|
|
137
|
+
) as resp:
|
|
138
|
+
if resp.status in (401, 403):
|
|
139
|
+
raise PlanetFitnessAuthError("Not authenticated for club fetch.")
|
|
140
|
+
if resp.status != 200:
|
|
141
|
+
raise PlanetFitnessConnectionError(
|
|
142
|
+
f"Club fetch failed HTTP {resp.status}."
|
|
143
|
+
)
|
|
144
|
+
# An expired session can also return the login page (HTML) with a
|
|
145
|
+
# 200. Guard against that so we don't parse HTML as JSON.
|
|
146
|
+
ctype = resp.headers.get("content-type", "")
|
|
147
|
+
if "application/json" not in ctype:
|
|
148
|
+
raise PlanetFitnessAuthError(
|
|
149
|
+
"Club fetch returned non-JSON; session likely expired."
|
|
150
|
+
)
|
|
151
|
+
data = await resp.json()
|
|
152
|
+
except aiohttp.ClientError as err:
|
|
153
|
+
raise PlanetFitnessConnectionError(f"Club fetch failed: {err}") from err
|
|
154
|
+
|
|
155
|
+
raw_list = data.get("UsersInClubList") or []
|
|
156
|
+
return [Club.from_api(item) for item in raw_list]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _extract_member(data: dict) -> dict:
|
|
160
|
+
"""Login returns {"User": {"Member": {...}}}; Identity returns {"Member": {...}}.
|
|
161
|
+
|
|
162
|
+
Handle both so either response shape confirms authentication.
|
|
163
|
+
"""
|
|
164
|
+
user = data.get("User")
|
|
165
|
+
if isinstance(user, dict) and user.get("Member"):
|
|
166
|
+
return user["Member"]
|
|
167
|
+
return data.get("Member") or {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Exceptions for pfau_occupancy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PlanetFitnessError(Exception):
|
|
5
|
+
"""Base error for all pfau_occupancy failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlanetFitnessAuthError(PlanetFitnessError):
|
|
9
|
+
"""Raised when login fails or the session is no longer authenticated.
|
|
10
|
+
|
|
11
|
+
The Home Assistant coordinator treats this specially: it triggers a
|
|
12
|
+
re-login (and, if that fails, a reauth flow) rather than marking sensors
|
|
13
|
+
unavailable, so a dropped session is never confused with an empty club.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlanetFitnessConnectionError(PlanetFitnessError):
|
|
18
|
+
"""Raised on network/transport problems talking to the portal."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Data models for pfau_occupancy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import unicodedata
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def slugify_club(name: str) -> str:
|
|
10
|
+
"""Produce a stable, filesystem/entity-safe key from a club name.
|
|
11
|
+
|
|
12
|
+
Used to build entity unique_ids. Since the occupancy endpoint exposes no
|
|
13
|
+
club ID, the (slugified) name is the only stable identity we have. Two
|
|
14
|
+
different clubs must not collide here, so we keep it close to the original:
|
|
15
|
+
lowercased, accents stripped, non-alphanumerics collapsed to underscores.
|
|
16
|
+
"""
|
|
17
|
+
normalized = unicodedata.normalize("NFKD", name)
|
|
18
|
+
ascii_name = normalized.encode("ascii", "ignore").decode("ascii")
|
|
19
|
+
slug = re.sub(r"[^a-z0-9]+", "_", ascii_name.lower()).strip("_")
|
|
20
|
+
return slug or "club"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Club:
|
|
25
|
+
"""A single club's occupancy snapshot."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
address: str | None
|
|
29
|
+
occupancy: int | None
|
|
30
|
+
limit: int | None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def key(self) -> str:
|
|
34
|
+
"""Stable identity for this club (used for the entity unique_id)."""
|
|
35
|
+
return slugify_club(self.name)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def percent_full(self) -> float | None:
|
|
39
|
+
"""Occupancy as a percentage of the club's stated limit, if known."""
|
|
40
|
+
if not self.limit or self.occupancy is None:
|
|
41
|
+
return None
|
|
42
|
+
return round(self.occupancy / self.limit * 100, 1)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_api(cls, raw: dict) -> "Club":
|
|
46
|
+
return cls(
|
|
47
|
+
name=raw["ClubName"],
|
|
48
|
+
address=raw.get("ClubAddress"),
|
|
49
|
+
occupancy=raw.get("UsersCountCurrentlyInClub"),
|
|
50
|
+
limit=raw.get("UsersLimit"),
|
|
51
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pfau-occupancy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client for Planet Fitness AU (PerfectGym) club occupancy
|
|
5
|
+
Author: Dan Morgan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dancmorgan/pfau-occupancy
|
|
8
|
+
Project-URL: Issues, https://github.com/dancmorgan/pfau-occupancy/issues
|
|
9
|
+
Keywords: planet fitness,perfectgym,home assistant,occupancy
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: aiohttp>=3.9
|
|
13
|
+
|
|
14
|
+
# pfau-occupancy
|
|
15
|
+
|
|
16
|
+
Async Python client for Planet Fitness Australia club occupancy, backing a Home
|
|
17
|
+
Assistant integration. Data comes from the member portal (PerfectGym
|
|
18
|
+
ClientPortal2), which exposes a live "members currently in club" count for every
|
|
19
|
+
club in a single authenticated call.
|
|
20
|
+
|
|
21
|
+
This library deliberately does only the API layer: login, session handling, and
|
|
22
|
+
fetching occupancy. All Home Assistant concerns (entities, config flow,
|
|
23
|
+
coordinator) live in the [pfau-occupancy-ha](https://github.com/dancmorgan/pfau-occupancy-ha)
|
|
24
|
+
integration that depends on this package.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pfau-occupancy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from pfau_occupancy import PlanetFitnessClient
|
|
37
|
+
|
|
38
|
+
async def main():
|
|
39
|
+
async with PlanetFitnessClient("you@example.com", "password") as client:
|
|
40
|
+
clubs = await client.async_get_clubs()
|
|
41
|
+
for club in clubs:
|
|
42
|
+
print(club.name, club.occupancy)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The client logs in automatically on first fetch and re-authenticates once if the
|
|
48
|
+
session expires, so a long-running poller keeps working without intervention.
|
|
49
|
+
|
|
50
|
+
## Data notes
|
|
51
|
+
|
|
52
|
+
The occupancy endpoint returns every club in one response. Each club carries a
|
|
53
|
+
name, address, a currently-in-club count, and a capacity limit (often null). The
|
|
54
|
+
response has no club ID, so the (slugified) club name is the stable identity used
|
|
55
|
+
for entity `unique_id`s. A club being renamed therefore looks like one club
|
|
56
|
+
disappearing and another appearing.
|
|
57
|
+
|
|
58
|
+
## Disclaimer
|
|
59
|
+
|
|
60
|
+
This uses the member portal's internal endpoints, which are undocumented and may
|
|
61
|
+
change or break without notice. It requires your own valid membership
|
|
62
|
+
credentials. Use it with your own account and keep polling gentle.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pfau_occupancy/__init__.py
|
|
4
|
+
pfau_occupancy/client.py
|
|
5
|
+
pfau_occupancy/exceptions.py
|
|
6
|
+
pfau_occupancy/models.py
|
|
7
|
+
pfau_occupancy.egg-info/PKG-INFO
|
|
8
|
+
pfau_occupancy.egg-info/SOURCES.txt
|
|
9
|
+
pfau_occupancy.egg-info/dependency_links.txt
|
|
10
|
+
pfau_occupancy.egg-info/requires.txt
|
|
11
|
+
pfau_occupancy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiohttp>=3.9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pfau_occupancy
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pfau-occupancy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async client for Planet Fitness AU (PerfectGym) club occupancy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Dan Morgan" }]
|
|
13
|
+
keywords = ["planet fitness", "perfectgym", "home assistant", "occupancy"]
|
|
14
|
+
dependencies = ["aiohttp>=3.9"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/dancmorgan/pfau-occupancy"
|
|
18
|
+
Issues = "https://github.com/dancmorgan/pfau-occupancy/issues"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["pfau_occupancy*"]
|