Skip to content
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

Update anonymize data #118

Merged
merged 2 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
__pycache__
.idea/
custom_components/hubspace/dump_*.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ the following setup steps must be run:
```
* Download requirements
* ```sh
python -m pip install requests "hubspace_async>=0.0.5" click
python -m pip install requests "hubspace_async>=0.0.5" asyncclick
```
* Goto your home directory
* Unix
Expand Down
181 changes: 124 additions & 57 deletions custom_components/hubspace/anonomyize_data.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import asyncio
__all__ = ["generate_anon_data", "generate_raw_data"]


import json
import logging
from dataclasses import asdict
from uuid import uuid4

import aiofiles
from hubspace_async import HubSpaceConnection, HubSpaceDevice, HubSpaceState

logger = logging.getLogger(__name__)
Expand All @@ -12,32 +15,63 @@
FNAME_IND = 0


async def generate_anon_data(conn: HubSpaceConnection):
async def generate_anon_data(
conn: HubSpaceConnection,
anon_name: bool = False,
) -> dict:
devices = await conn.devices
fake_devices = []
parents = {}
parents = await generate_parent_mapping(devices)
device_links = {}
for dev in devices.values():
fake_devices.append(anonymize_device(dev, parents))
fake_devices.append(
await anonymize_device(dev, parents, device_links, anon_name)
)
return fake_devices


def get_devices(conn: HubSpaceConnection) -> dict:
loop = asyncio.get_event_loop()
loop.run_until_complete(conn.populate_data())
async def generate_raw_data(conn: HubSpaceConnection) -> dict:
devices = await conn.devices
fake_devs = []
for dev in devices.values():
fake_dev = asdict(dev)
fake_dev["states"] = []
for state in dev.states:
fake_dev["states"].append(await anonymize_state(state, only_geo=True))
fake_devs.append(fake_dev)
return fake_devs


async def generate_parent_mapping(devices: list[HubSpaceDevice]) -> dict:
mapping = {}
for device in devices.values():
if device.children:
device.id = str(uuid4())
new_children = []
for child_id in device.children:
new_uuid = str(uuid4())
mapping[child_id] = {"parent": device.id, "new": new_uuid}
new_children.append(new_uuid)
device.children = new_children
return mapping


async def get_devices(conn: HubSpaceConnection) -> dict:
await conn.populate_data()
return conn._devices


def generate_name(dev: HubSpaceDevice) -> str:
async def generate_name(dev: HubSpaceDevice) -> str:
return f"{dev.device_class}-{dev.model}"


def output_devices(conn: HubSpaceConnection):
async def output_devices(conn: HubSpaceConnection):
"""Output all devices associated with the Account

This should only be run when testing as it modified the
log level.
"""
devices = get_devices(conn)
devices = await get_devices(conn)
for child_id in devices:
dev = devices[child_id]
logger.info("Friendly Name: %s", dev.friendly_name)
Expand All @@ -46,8 +80,11 @@ def output_devices(conn: HubSpaceConnection):
logger.info("\tDevice Class: %s", dev.device_class)


def anonymize_device_lookup(
conn: HubSpaceConnection, friendly_name: str = None, child_id: str = None
async def anonymize_device_lookup(
conn: HubSpaceConnection,
friendly_name: str = None,
child_id: str = None,
anon_name: bool = False,
):
"""Output anonymized device data for a device

Expand All @@ -56,7 +93,7 @@ def anonymize_device_lookup(
three entries, one of the fan, one of the light, and one for the
overall.
"""
devices = get_devices(conn)
devices = await get_devices(conn)
matched_dev = None
related = []
# Find the device match
Expand All @@ -77,77 +114,89 @@ def anonymize_device_lookup(
if dev.device_id == matched_dev.device_id:
related.append(dev)
anon_data = []
mapping = {}
parents = await generate_parent_mapping(devices)
device_links = {}
for dev in related:
anon_data.append(anonymize_device(dev, mapping))
identifier = f"{generate_name(related[0])}.json"
with open(identifier, "w") as f:
json.dump(anon_data, f, indent=4)
anon_data.append(await anonymize_device(dev, parents, device_links, anon_name))
identifier = f"dump_{await generate_name(related[0])}.json"
async with aiofiles.open(identifier, "w") as f:
await f.write(json.dumps(anon_data, indent=4))
return anon_data


def anonymize_device(dev: HubSpaceDevice, parent_mapping: dict):
async def anonymize_device(
dev: HubSpaceDevice,
parent_mapping: dict,
device_links: dict,
anon_name,
):
fake_dev = asdict(dev)
global FNAME_IND
# Modify the name
fake_dev["friendly_name"] = f"friendly-device-{FNAME_IND}"
FNAME_IND += 1
if anon_name:
global FNAME_IND
# Modify the name
fake_dev["friendly_name"] = f"friendly-device-{FNAME_IND}"
FNAME_IND += 1
# Modify the id
fake_dev["id"] = str(uuid4())
# Remove parent id
parent = dev.device_id
if parent not in parent_mapping:
parent_mapping[parent] = str(uuid4())
fake_dev["device_id"] = parent_mapping[parent]
if dev.id in parent_mapping:
fake_dev["id"] = parent_mapping[dev.id]["new"]
else:
fake_dev["id"] = str(uuid4())
# Generate a custom device_id link
dev_link = dev.device_id
if dev_link not in device_links:
device_links[dev_link] = str(uuid4())
fake_dev["device_id"] = device_links[dev_link]
fake_dev["states"] = []
for state in dev.states:
fake_dev["states"].append(anonymize_state(state))
fake_dev["states"].append(await anonymize_state(state))
return fake_dev


def anonymize_state(state: HubSpaceState):
async def anonymize_state(state: HubSpaceState, only_geo: bool = False):
fake_state = asdict(state)
fake_state["lastUpdateTime"] = 0
if fake_state["functionClass"] == "wifi-ssid":
fake_state["value"] = str(uuid4())
elif fake_state["functionClass"] == "geo-coordinates":
if fake_state["functionClass"] == "geo-coordinates":
fake_state["value"] = {"geo-coordinates": {"latitude": "0", "longitude": "0"}}
elif isinstance(state.value, str):
if "mac" in state.functionClass:
elif not only_geo:
if fake_state["functionClass"] == "wifi-ssid":
fake_state["value"] = str(uuid4())
elif isinstance(state.value, str):
if "mac" in state.functionClass:
fake_state["value"] = str(uuid4())
return fake_state


def get_states(
async def get_states(
conn: HubSpaceConnection, friendly_name: str = None, child_id: str = None
):
"""Get all states associated to a specific ID"""
states = []
devices = get_devices(conn)
devices = await get_devices(conn)
for dev in devices.values():
if friendly_name and dev.friendly_name != friendly_name:
continue
if child_id and dev.id != child_id:
continue
for state in dev.states:
states.append(anonymize_state(state))
with open(f"{generate_name(dev)}.json", "w") as f:
json.dump(states, f, indent=4)
states.append(await anonymize_state(state))
async with aiofiles.open(f"dump_{await generate_name(dev)}.json", "w") as f:
await f.write(json.dumps(states, indent=4))
return states


try:
import click
import asyncclick as click
import hubspace_async

user = click.option("--username", required=True, help="HubSpace Username")
pwd = click.option("--password", required=True, help="HubSpace password")
aname = click.option("--anon-name", default=False, help="Anonymize name")

@click.group()
@user
@pwd
@click.pass_context
def cli(ctx, username, password):
async def cli(ctx, username, password):
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
logging.getLogger("asyncio").setLevel(logging.WARNING)
Expand All @@ -156,40 +205,58 @@ def cli(ctx, username, password):

@cli.command()
@click.pass_context
def get_devs(ctx):
async def get_devs(ctx):
"""Output all devices associated with the Account"""
click.echo(output_devices(ctx.obj["conn"]))
click.echo(await output_devices(ctx.obj["conn"]))

@cli.command()
@click.option("--fn", required=True, help="Friendly Name")
@aname
@click.pass_context
def friendly_name(ctx, fn):
async def friendly_name(ctx, fn, anon_name):
"""Output all devices associated to the name"""
click.echo(anonymize_device_lookup(ctx.obj["conn"], friendly_name=fn))
click.echo(
await anonymize_device_lookup(
ctx.obj["conn"], friendly_name=fn, anon_name=anon_name
)
)

@cli.command()
@click.option("--child_id", required=True, help="Child ID")
@aname
@click.pass_context
def child_id(ctx, child_id):
async def child_id(ctx, child_id, anon_name):
"""Output all devices associated to the child_id"""
click.echo(anonymize_device_lookup(ctx.obj["conn"], child_id=child_id))
click.echo(
await anonymize_device_lookup(
ctx.obj["conn"], child_id=child_id, anon_name=anon_name
)
)

@cli.command()
@click.option("--child_id", required=True, help="Child ID")
@click.pass_context
def states(ctx, child_id):
async def states(ctx, child_id):
"""Output all devices associated to the child_id"""
click.echo(get_states(ctx.obj["conn"], child_id=child_id))
click.echo(await get_states(ctx.obj["conn"], child_id=child_id))

@cli.command()
@click.pass_context
def raw_data(ctx):
async def raw_data(ctx):
"""Get the raw output. This data is not anonymized"""
get_devices(ctx.obj["conn"])
with open("raw-data.json", "w") as f:
import json
await get_devices(ctx.obj["conn"])
async with aiofiles.open("dump_raw-data.json", "w") as f:
data = await generate_raw_data(ctx.obj["conn"])
await f.write(json.dumps(data, indent=4))

json.dump(ctx.obj["conn"].raw_devices, f, indent=4)
@cli.command()
@aname
@click.pass_context
async def anon_data(ctx, anon_name):
"""Get the raw output. This data is not anonymized"""
async with aiofiles.open("dump_anon-data.json", "w") as f:
data = await generate_anon_data(ctx.obj["conn"], anon_name=anon_name)
await f.write(json.dumps(data, indent=4))

except: # noqa
pass
Expand All @@ -198,4 +265,4 @@ def raw_data(ctx):
try:
cli()
except NameError:
logger.warning("Click is not installed")
logger.exception("Click is not installed")
3 changes: 2 additions & 1 deletion custom_components/hubspace/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ async def _async_update_data(
await fh.write(json.dumps(data, indent=4))
dev_raw = os.path.join(curr_directory, "_dump_hs_raw.json")
_LOGGER.debug("Writing out raw device data to %s", dev_raw)
data = await anonomyize_data.generate_raw_data(self.conn)
async with aiofiles.open(dev_raw, "w") as fh:
await fh.write(json.dumps(self.conn.raw_devices, indent=4))
await fh.write(json.dumps(data, indent=4))
return await self.process_tracked_devices()

async def hs_data_update(self) -> None:
Expand Down
6 changes: 6 additions & 0 deletions tests/device_dumps/fan-ZandraFan.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@
]
}
],
"children": [],
"states": [
{
"functionClass": "toggle",
Expand Down Expand Up @@ -624,6 +625,7 @@
]
}
],
"children": [],
"states": [
{
"functionClass": "power",
Expand Down Expand Up @@ -820,6 +822,10 @@
]
}
],
"children": [
"066c0e38-c49b-4f60-b805-486dc07cab74",
"3a0c5015-c19d-417f-8e08-e71cd5bc221b"
],
"states": [
{
"functionClass": "error-flag",
Expand Down
Loading