Skip to content

Commit 09ba903

Browse files
authored
Merge pull request #47 from CoMPaTech/improvements
Improvements
2 parents d838c97 + c09dc79 commit 09ba903

18 files changed

+1601
-46
lines changed

.github/workflows/merge.yml

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,44 @@ env:
44
CACHE_VERSION: 1
55
DEFAULT_PYTHON: "3.13"
66

7-
# Only run on merges
87
on:
98
pull_request:
109
types: closed
1110
branches:
1211
- main
1312

1413
jobs:
15-
publishing:
16-
name: Build and publish Python 🐍 distributions 📦 to PyPI
14+
determine_version:
15+
name: Determine Package Version
1716
runs-on: ubuntu-latest
18-
environment: pypi
17+
outputs:
18+
package_version: ${{ steps.get_version.outputs.package_version }}
19+
should_publish: ${{ steps.scheck_pypi.outputs.should_publish }}
1920
permissions:
20-
contents: read # Required by actions/checkout
21-
id-token: write # Needed for OIDC-based Trusted Publishing
22-
# Only trigger on merges, not just closes
21+
contents: read
2322
if: github.event.pull_request.merged == true
2423
steps:
2524
- name: Check out committed code
2625
uses: actions/checkout@v4
27-
- name: Prepare uv
28-
run: |
29-
pip install uv
30-
uv venv --seed venv
31-
. venv/bin/activate
32-
uv pip install toml
33-
- name: Check for existing package on PyPI
34-
id: check_package
26+
- name: Get Package Version from pyproject.toml
27+
id: get_version
3528
run: |
36-
. venv/bin/activate
29+
# Install toml to parse pyproject.toml
30+
pip install toml
3731
PACKAGE_VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['version'])")
3832
PACKAGE_NAME=$(python -c "import toml; print(toml.load('pyproject.toml')['project']['name'])")
3933
40-
echo "Checking for package: $PACKAGE_NAME==$PACKAGE_VERSION"
34+
# Set outputs for the next jobs
35+
echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
36+
echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
37+
echo "Package name and version: $PACKAGE_NAME==$PACKAGE_VERSION"
38+
39+
- name: Check for existing package on PyPI
40+
id: scheck_pypi
41+
run: |
42+
# Using the package name and version from the previous step
43+
PACKAGE_VERSION=${{ steps.get_version.outputs.package_version }}
44+
PACKAGE_NAME=${{ steps.get_version.outputs.package_name }}
4145
4246
if curl -s "https://pypi.org/pypi/$PACKAGE_NAME/json" | jq -r '.releases | keys[]' | grep -q "^$PACKAGE_VERSION$"; then
4347
echo "Package version already exists. Skipping upload."
@@ -46,13 +50,68 @@ jobs:
4650
echo "Package version does not exist. Proceeding with upload."
4751
echo "should_publish=true" >> $GITHUB_OUTPUT
4852
fi
53+
54+
publishing:
55+
name: Build and publish Python 🐍 distributions 📦 to PyPI
56+
runs-on: ubuntu-latest
57+
needs: determine_version
58+
if: needs.determine_version.outputs.should_publish == 'true'
59+
environment: pypi
60+
permissions:
61+
contents: read
62+
id-token: write
63+
steps:
64+
- name: Check out committed code
65+
uses: actions/checkout@v4
66+
- name: Prepare uv
67+
run: |
68+
pip install uv
69+
uv venv --seed venv
70+
. venv/bin/activate
4971
- name: Build
50-
if: steps.check_package.outputs.should_publish == 'true'
5172
run: |
5273
. venv/bin/activate
5374
uv build
5475
- name: Publish distribution 📦 to PyPI
55-
if: steps.check_package.outputs.should_publish == 'true'
5676
run: |
5777
. venv/bin/activate
5878
uv publish
79+
80+
create_tag:
81+
name: Create Git Tag for Release
82+
runs-on: ubuntu-latest
83+
needs: [determine_version, publishing]
84+
if: always() && needs.publishing.result == 'success' && needs.determine_version.outputs.should_publish == 'true'
85+
permissions:
86+
contents: write
87+
steps:
88+
- name: Check out committed code
89+
uses: actions/checkout@v4
90+
- name: Create release tag
91+
id: tag_release
92+
uses: actions/github-script@v6
93+
with:
94+
script: |
95+
const version = process.env.PACKAGE_VERSION;
96+
const tagName = `v${version}`;
97+
98+
console.log(`Attempting to create tag: ${tagName}`);
99+
100+
try {
101+
await github.rest.git.createRef({
102+
owner: context.repo.owner,
103+
repo: context.repo.repo,
104+
ref: `refs/tags/${tagName}`,
105+
sha: context.sha,
106+
});
107+
console.log(`Successfully created tag: ${tagName}`);
108+
} catch (error) {
109+
console.error(`Failed to create tag: ${error.message}`);
110+
if (error.status === 422) {
111+
console.log(`Tag ${tagName} already exists. Skipping creation.`);
112+
} else {
113+
throw error;
114+
}
115+
}
116+
env:
117+
PACKAGE_VERSION: ${{ needs.determine_version.outputs.package_version }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ tests/__pycache__
1111
.coverage
1212
tmp
1313
todo
14+
.DS_Store

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.2.5] - 2025-08-05
6+
7+
### Added
8+
9+
- Added booleans determining station/accesspoint and PTP/PTMP in derived subclass
10+
511
## [0.2.4] - 2025-08-03
612

713
### Added

CONTRIBUTE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Contributing
2+
3+
It would be very helpful if you would share your configuration data to this project. This way we can make sure that the data returned by your airOS devices is processed correctly.
4+
5+
## Current fixtures
6+
7+
We currently have data on
8+
9+
- Nanostation 5AC (LOCO5AC) - PTP - both AP and Station output of `/status.cgi` present (by @CoMPaTech)
10+
11+
## Secure your data
12+
13+
The best way to share your data is to remove any data that is not necessary for processing. To ensure you don't share any data by accident please follow the following
14+
15+
- Log in to your device
16+
- Note down the following options set
17+
- Is it a station or access point
18+
- Is it PTP (or PTMP)
19+
- Channel width in Mhz
20+
- Manually update the url to `/status.cgi`, e.g. `https://192.168.1.10/status.cgi`
21+
- Store the output (for instance in an editor, even notepad would be fine) for processing
22+
23+
**NOTE**: when redacting, redact in a meaningful way by changing parameters, don't put text in number fields or vice-versa!
24+
25+
- First and foremost: find any `lat` and `lon` information and redact these but keep them as floats. (I.e. a value with decimals)!
26+
- There are potentially multiple of these (so keep searching)
27+
- If you are unsure, apply them as `(...)"lat":52.379894,"lon":4.901608,(...)` to point to Amsterdam
28+
- Redact your IP addresses, especially public IPs (if present),
29+
- Search/replace your range, say your AP is at `192.168.1.10` then search for `192.168.1.` and replace with `127.0.0.`
30+
- Set them to `127.0.0.xxx` leaving the actual last octet what it was, just so devices are still different from **and** coheren to each other.
31+
- Redact your SSID, just name it WirelessABC (as long as it's still coherent)
32+
- Make sure your `hostname`s don't disclose unwanted information
33+
- You may redact your MAC addresses (hwaddr, mac, etc) just make sure they are still valid (`00:11:22:33:44:55`) and coherent (so make sure local and remote(s) still differ)
34+
35+
### Examples
36+
37+
See `fixtures/userdata` for examples of shared information

airos/airos8.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,30 @@ def derived_data(
196196
self, response: dict[str, Any] | None = None
197197
) -> dict[str, Any] | None:
198198
"""Add derived data to the device response."""
199+
derived = {
200+
"station": False,
201+
"access_point": False,
202+
"ptp": False,
203+
"ptmp": False,
204+
}
205+
206+
# Access Point / Station vs PTP/PtMP
207+
wireless_mode = response.get("wireless", {}).get("mode", "")
208+
match wireless_mode:
209+
case "ap-ptmp":
210+
derived["access_point"] = True
211+
derived["ptmp"] = True
212+
case "sta-ptmp":
213+
derived["station"] = True
214+
derived["ptmp"] = True
215+
case "ap-ptp":
216+
derived["access_point"] = True
217+
derived["ptp"] = True
218+
case "sta-ptp":
219+
derived["station"] = True
220+
derived["ptp"] = True
221+
222+
# INTERFACES
199223
addresses = {}
200224
interface_order = ["br0", "eth0", "ath0"]
201225

@@ -209,19 +233,18 @@ def derived_data(
209233
if interface["enabled"]: # Only consider if enabled
210234
addresses[interface["ifname"]] = interface["hwaddr"]
211235

236+
# Fallback take fist alternate interface found
237+
derived["mac"] = interfaces[0]["hwaddr"]
238+
derived["mac_interface"] = interfaces[0]["ifname"]
239+
212240
for interface in interface_order:
213241
if interface in addresses:
214-
response["derived"] = {
215-
"mac": addresses[interface],
216-
"mac_interface": interface,
217-
}
218-
return response
242+
derived["mac"] = addresses[interface]
243+
derived["mac_interface"] = interface
244+
break
245+
246+
response["derived"] = derived
219247

220-
# Fallback take fist alternate interface found
221-
response["derived"] = {
222-
"mac": interfaces[0]["hwaddr"],
223-
"mac_interface": interfaces[0]["ifname"],
224-
}
225248
return response
226249

227250
async def status(self) -> AirOSData:

airos/data.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ class IeeeMode(Enum):
4444
class WirelessMode(Enum):
4545
"""Enum definition."""
4646

47-
PTP_ACCESSPOINT = "ap-ptp"
4847
PTMP_ACCESSPOINT = "ap-ptmp"
48+
PTMP_STATION = "sta-ptmp"
49+
PTP_ACCESSPOINT = "ap-ptp"
4950
PTP_STATION = "sta-ptp"
5051
# More to be added when known
5152

@@ -437,6 +438,14 @@ class Derived:
437438
mac: str # Base device MAC address (i.e. eth0)
438439
mac_interface: str # Interface derived from
439440

441+
# Split for WirelessMode
442+
station: bool
443+
access_point: bool
444+
445+
# Split for WirelessMode
446+
ptp: bool
447+
ptmp: bool
448+
440449

441450
@dataclass
442451
class AirOS8Data(DataClassDictMixin):

airos/discovery.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,13 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
175175
offset += 2
176176

177177
if tlv_length > (len(data) - offset):
178-
log = f"TLV type {tlv_type:#x} length {tlv_length} exceeds remaining data "
179-
_LOGGER.warning(log)
180-
log = f"({len(data) - offset} bytes left). Packet malformed. "
181-
_LOGGER.warning(log)
182-
log = f"Data from TLV start: {data[offset - 3 :].hex()}"
178+
log = (
179+
f"TLV type {tlv_type:#x} length {tlv_length} exceeds remaining data "
180+
f"({len(data) - offset} bytes left). Packet malformed. "
181+
f"Data from TLV start: {data[offset - 3 :].hex()}"
182+
)
183183
_LOGGER.warning(log)
184-
log = f"Malformed packet: {log}"
185-
raise AirOSEndpointError(log)
184+
raise AirOSEndpointError(f"Malformed packet: {log}")
186185

187186
tlv_value: bytes = data[offset : offset + tlv_length]
188187

@@ -195,6 +194,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
195194
else:
196195
log = f"Unexpected length for 0x02 TLV (MAC+IP). Expected 10, got {tlv_length}. Value: {tlv_value.hex()}"
197196
_LOGGER.warning(log)
197+
raise AirOSEndpointError(f"Malformed packet: {log}")
198198

199199
elif tlv_type == 0x03:
200200
parsed_info["firmware_version"] = tlv_value.decode(
@@ -213,6 +213,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None
213213
else:
214214
log = f"Unexpected length for Uptime (Type 0x0A): {tlv_length}. Value: {tlv_value.hex()}"
215215
_LOGGER.warning(log)
216+
raise AirOSEndpointError(f"Malformed packet: {log}")
216217

217218
elif tlv_type == 0x0B:
218219
parsed_info["hostname"] = tlv_value.decode(

fixtures/airos_ap-ptp.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)