Skip to content
Closed
123 changes: 123 additions & 0 deletions card.triangulate.req.notecard.api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json",
"title": "card.triangulate Request Application Programming Interface (API) Schema",
"description": "Enables or disables a behavior by which the Notecard gathers information about surrounding cell towers and/or Wi-Fi access points with each new Notehub session.",
"type": "object",
"skus": [
"CELL",
"CELL+WIFI",
"WIFI"
],
"version": "0.2.1",
"apiVersion": "9.1.1",
"properties": {
"cmd": {
"description": "Command for the Notecard (no response)",
"const": "card.triangulate"
},
"req": {
"description": "Request for the Notecard (expects response)",
"const": "card.triangulate"
},
"mode": {
"description": "The triangulation approach to use for determining the Notecard location. The following keywords can be used separately or together in a comma-delimited list, in any order.",
"type": "string",
"pattern": "^(cell(,wifi)?|wifi(,cell)?|-)$",
"sub-descriptions": [
{
"const": "cell",
"description": "Enables cell tower scanning to determine the position of the Device."
},
{
"const": "wifi",
"description": "Enables the use of nearby Wi-Fi access points to determine the position of the Device. To leverage this feature, the host will need to provide access point information to the Notecard via the `text` argument in subsequent requests."
},
{
"const": "-",
"description": "Clear the currently-set triangulation mode."
}
]
},
"on": {
"description": "`true` to instruct the Notecard to triangulate even if the module has not moved. Only takes effect when `set` is `true`.",
Copy link

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description references a dependency on the 'set' parameter, but according to the PR description, the 'on' parameter should work independently to enable/disable triangulation mode. This creates confusion about the parameter's behavior.

Copilot generated this review using guidance from repository custom instructions.
"type": "boolean",
"default": false
},
"usb": {
"description": "`true` to perform triangulation only when the Notecard is connected to USB power. Only takes effect when `set` is `true`.",
"type": "boolean",
"default": false
},
"set": {
"description": "`true` to instruct the module to use the state of the `on` and `usb` arguments.",
"type": "boolean",
"default": false
},
"minutes": {
"description": "Minimum delay, in minutes, between triangulation attempts. Use `0` for no time-based suppression.",
"type": "integer",
"default": 0,
"minimum": -1
},
"text": {
"description": "When using Wi-Fi triangulation, a newline-terminated list of Wi-Fi access points obtained by the external module. Format should follow the ESP32's [AT+CWLAP command output](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Wi-Fi_AT_Commands.html#cmd-lap).",
"type": "string"
},
"time": {
"description": "When passed with `text`, records the time that the Wi-Fi access point scan was performed. _If not provided, Notecard time is used._",
"type": "integer"
}
},
"oneOf": [
{
"required": [
"req"
],
"properties": {
"req": {
"const": "card.triangulate"
}
}
},
{
"required": [
"cmd"
],
"properties": {
"cmd": {
"const": "card.triangulate"
}
}
}
],
"additionalProperties": false,
"samples": [
{
"title": "Single Mode",
"description": "Enable triangulation using cell towers only.",
"json": "{\"req\":\"card.triangulate\",\"mode\":\"cell\",\"on\":true,\"set\":true}"
},
{
"title": "Dual Mode",
"description": "Enable triangulation using both Wi-Fi and cell towers when connected to USB power.",
"json": "{\"req\":\"card.triangulate\",\"mode\":\"wifi,cell\",\"on\":true,\"usb\":true,\"set\":true}"
},
{
"title": "Send Wi-Fi AP Data",
"description": "Send a newline-terminated list of Wi-Fi access points to the Notecard for triangulation.",
"json": "{\"req\":\"card.triangulate\",\"text\":\"+CWLAP:(4,\\\"Blues\\\",-51,\\\"74:ac:b9:12:12:f8\\\",1)\\n+CWLAP:(3,\\\"AAAA-62DD\\\",-70,\\\"6c:55:e8:91:62:e1\\\",11)\\n+CWLAP:(4,\\\"Blues\\\",-81,\\\"74:ac:b9:11:12:23\\\",1)\\n+CWLAP:(4,\\\"Blues\\\",-82,\\\"74:ac:a9:12:19:48\\\",11)\\n+CWLAP:(4,\\\"Free Parking\\\",-83,\\\"02:18:4a:11:60:31\\\",6)\\n+CWLAP:(5,\\\"GO\\\",-84,\\\"01:13:6a:13:90:30\\\",6)\\n+CWLAP:(4,\\\"AAAA-5C62-2.4\\\",-85,\\\"d8:97:ba:7b:fd:60\\\",1)\\n+CWLAP:(3,\\\"DIRECT-a5-HP MLP50\\\",-86,\\\"fa:da:0c:1b:16:a5\\\",6)\\n+CWLAP:(3,\\\"DIRECT-c6-HP M182 LaserJet\\\",-88,\\\"da:12:65:44:31:c6\\\",6)\\n\\n\"}"
},
{
"title": "Disable Triangulation",
"description": "Disable triangulation mode.",
"json": "{\"req\":\"card.triangulate\",\"mode\":\"-\"}"
}
],
"annotations": [
{
"title": "note",
"description": "See [Using Cell Tower & Wi-Fi Triangulation](https://dev.blues.io/guides-and-tutorials/collecting-sensor-data/notecard-guides/using-cell-tower-wi-fi-triangulation/) for more information."
}
]
}
45 changes: 45 additions & 0 deletions card.triangulate.rsp.notecard.api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.rsp.notecard.api.json",
"title": "card.triangulate Response Application Programming Interface (API) Schema",
"description": "Response containing triangulated location information and triangulation settings.",
"type": "object",
"version": "0.1.2",
"apiVersion": "9.1.1",
"properties": {
"motion": {
"description": "UNIX Epoch time of last detected Notecard movement.",
"type": "integer",
"minimum": 0
},
"time": {
"description": "UNIX Epoch time of last triangulation scan.",
"type": "integer",
"minimum": 0
},
"mode": {
"description": "A comma-separated list indicating the active triangulation modes.",
"type": "string"
},
"on": {
"description": "`true` if triangulation scans will be performed even if the device has not moved.",
"type": "boolean"
},
"usb": {
"description": "`true` if triangulation scans will be performed only when the device is USB-powered.",
"type": "boolean"
},
"length": {
"description": "The length of the `text` buffer provided in the current or a previous request.",
Copy link

Copilot AI Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for the 'length' field appears to be copied from another API and doesn't align with the card.triangulate API functionality. This field should describe the length related to triangulation data, not a generic text buffer.

Copilot generated this review using guidance from repository custom instructions.
"type": "integer",
"minimum": 0
}
},
"samples": [
{
"title": "Example Response",
"description": "Response showing triangulation configuration and status.",
"json": "{\"usb\":true,\"mode\":\"wifi,cell\",\"length\":443,\"on\":true,\"time\":1606755042,\"motion\":1606757487}"
}
]
}
5 changes: 4 additions & 1 deletion notecard.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
{
"$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.transport.req.notecard.api.json"
},
{
"$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.triangulate.req.notecard.api.json"
},
{
"$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/card.usage.get.req.notecard.api.json"
},
Expand Down Expand Up @@ -220,4 +223,4 @@
"$ref": "https://raw.githubusercontent.com/blues/notecard-schema/master/web.req.notecard.api.json"
}
]
}
}
84 changes: 84 additions & 0 deletions tests/test_card_triangulate_req.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pytest
import jsonschema
import json

SCHEMA_FILE = "card.triangulate.req.notecard.api.json"

def test_valid_req(schema):
"""Tests a minimal valid request using 'req'."""
instance = {"req": "card.triangulate"}
jsonschema.validate(instance=instance, schema=schema)

def test_valid_cmd(schema):
"""Tests a minimal valid request using 'cmd'."""
instance = {"cmd": "card.triangulate"}
jsonschema.validate(instance=instance, schema=schema)

def test_invalid_empty_object(schema):
"""Tests invalid empty object (needs req or cmd)."""
instance = {}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "is not valid under any of the given schemas" in str(excinfo.value)

def test_invalid_both_req_and_cmd(schema):
"""Tests invalid request having both req and cmd."""
instance = {"req": "card.triangulate", "cmd": "card.triangulate"}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "is valid under each of" in str(excinfo.value)

def test_invalid_additional_property_with_req(schema):
"""Tests invalid request with req and an additional property."""
instance = {"req": "card.triangulate", "extra": "field"}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "Additional properties are not allowed ('extra' was unexpected)" in str(excinfo.value)

def test_invalid_additional_property_with_cmd(schema):
"""Tests invalid request with cmd and an additional property."""
instance = {"cmd": "card.triangulate", "extra": "field"}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "Additional properties are not allowed ('extra' was unexpected)" in str(excinfo.value)

def test_on_field_valid(schema):
"""Tests valid 'on' field values."""
# Valid boolean values
instance = {"req": "card.triangulate", "on": True}
jsonschema.validate(instance=instance, schema=schema)

instance = {"req": "card.triangulate", "on": False}
jsonschema.validate(instance=instance, schema=schema)

def test_on_field_invalid_type(schema):
"""Tests invalid type for 'on' field."""
instance = {"req": "card.triangulate", "on": "true"}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "'true' is not of type 'boolean'" in str(excinfo.value)

def test_on_field_invalid_number(schema):
"""Tests invalid number type for 'on' field."""
instance = {"req": "card.triangulate", "on": 1}
with pytest.raises(jsonschema.ValidationError) as excinfo:
jsonschema.validate(instance=instance, schema=schema)
assert "1 is not of type 'boolean'" in str(excinfo.value)

def test_valid_request_with_cmd_and_on(schema):
"""Tests valid request using 'cmd' with 'on' parameter."""
instance = {"cmd": "card.triangulate", "on": True}
jsonschema.validate(instance=instance, schema=schema)

def test_validate_samples_from_schema(schema, schema_samples):
"""Tests that samples in the schema definition are valid."""
for sample in schema_samples:
sample_json_str = sample.get("json")
if not sample_json_str:
pytest.fail(f"Sample missing 'json' field: {sample.get('description', 'Unnamed sample')}")
try:
instance = json.loads(sample_json_str)
except json.JSONDecodeError as e:
pytest.fail(f"Failed to parse sample JSON: {sample_json_str}\nError: {e}")

jsonschema.validate(instance=instance, schema=schema)
Loading