pax_global_header00006660000000000000000000000064147502624700014521gustar00rootroot0000000000000052 comment=dfd2a5e92526b2d3293e47e0d8af0f14c4cf0024 lacrosse_view-1.1.1/000077500000000000000000000000001475026247000143665ustar00rootroot00000000000000lacrosse_view-1.1.1/.github/000077500000000000000000000000001475026247000157265ustar00rootroot00000000000000lacrosse_view-1.1.1/.github/workflows/000077500000000000000000000000001475026247000177635ustar00rootroot00000000000000lacrosse_view-1.1.1/.github/workflows/python-publish.yml000066400000000000000000000021031475026247000234670ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} lacrosse_view-1.1.1/.gitignore000066400000000000000000000015371475026247000163640ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg tmp*.pem # appears during build # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # Mypy .mypy_cache/ # Basic test, nothing special src/test.py # Output from various tests *.txt # Snyk .dccache # Venv .venvlacrosse_view-1.1.1/LICENSE000066400000000000000000000020501475026247000153700ustar00rootroot00000000000000MIT License Copyright (c) 2022 IceBotYT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.lacrosse_view-1.1.1/README.md000066400000000000000000000040631475026247000156500ustar00rootroot00000000000000> [!IMPORTANT] > This project is no longer being updated. I no longer own a LaCrosse View device and I have moved on to other projects. > I will only do general fixes required by Home Assistant. If you would like to to take over this repo and maintain it, please let me know. # La Crosse View A library for retrieving data from [La Crosse View-connected sensors](https://www.lacrossetechnology.com/collections/lacrosse-view-connected). ## Disclaimer This library is **NOT** for the Jeelink LaCrosse sensors. You can find that library [here](https://pypi.org/project/pylacrosse/). There is also a [Home Assistant integration](https://home-assistant.io/integrations/lacrosse) for the Jeelink LaCrosse sensors. ## Installation Run this in your terminal: ``` pip install lacrosse_view ``` ## Usage This example shows how to get the latest data from every sensor connected to the first location on your account. ```python from lacrosse_view import LaCrosse import asyncio from datetime import datetime, timedelta import time async def get_data(): api = LaCrosse() # Log in to your LaCrosse View account await api.login('username', 'password') # Get the sensors from the first location on the account locations = await api.get_locations() startTime = datetime.now() - timedelta(minutes=1) endTime = datetime.now() startTimeUnix = time.mktime(startTime.timetuple()) endTimeUnix = time.mktime(endTime.timetuple()) sensors = await api.get_sensors(locations[0], tz="America/Vancouver", start=startTimeUnix, end=endTimeUnix) for sensor in sensors: for field in sensor.sensor_field_names: # Each value is a dictionary with keys "s" and "u". "s" is the value and "u" is the Unix timestamp for it. value = sensor.data[field]["values"][-1]["s"] print( f"{sensor.name} {field}: {value}" ) await api.logout() asyncio.run(get_data()) ``` ## Questions? If you have any questions, please, don't hesitate to [open an issue](https://github.com/IceBotYT/lacrosse_view/issues/new). lacrosse_view-1.1.1/pyproject.toml000066400000000000000000000001471475026247000173040ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=42", "wheel" ] build-backend = "setuptools.build_meta"lacrosse_view-1.1.1/setup.cfg000066400000000000000000000014041475026247000162060ustar00rootroot00000000000000[metadata] name = lacrosse_view version = 1.1.1 author = IceBotYT author_email = icebotyt@outlook.com description = Client for retrieving data from the La Crosse View cloud long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/IceBotYT/lacrosse_view project_urls = Bug Tracker = https://github.com/IceBotYT/lacrosse_view/issues classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: MIT License Operating System :: OS Independent [options] package_dir = = src packages = find: python_requires = >=3.6 install_requires = aiohttp>=3.8.1 pydantic>=1.9.0,<3.0.0 aiozoneinfo>=0.2.1 [options.packages.find] where = src [options.package_data] lacrosse_view = *.typedlacrosse_view-1.1.1/src/000077500000000000000000000000001475026247000151555ustar00rootroot00000000000000lacrosse_view-1.1.1/src/lacrosse_view/000077500000000000000000000000001475026247000200225ustar00rootroot00000000000000lacrosse_view-1.1.1/src/lacrosse_view/__init__.py000066400000000000000000000203111475026247000221300ustar00rootroot00000000000000"""LaCrosse View library.""" # Thanks to Keith Prickett for the original code: https://github.com/keithprickett/lacrosse_weather from __future__ import annotations from typing import Any import aiohttp from pydantic import BaseModel from aiozoneinfo import async_get_time_zone import datetime from .const import DEVICE_URL, LOGIN_URL, SENSORS_URL, LOCATIONS_URL, STATUS_URL from .util import request class LaCrosse: """Basic class to hold the LaCrosse data.""" token: str = "" websession: aiohttp.ClientSession | None = None def __init__(self, websession: aiohttp.ClientSession | None = None): self.websession = websession async def login(self, email: str, password: str) -> bool: """Login to the LaCrosse API.""" payload = {"email": email, "password": password, "returnSecureToken": True} response, data = await request(LOGIN_URL, "POST", self.websession, json=payload) try: self.token: str = data["idToken"] except KeyError as e: raise LoginError("Invalid credentials.", data) from e if self.token is None: raise LoginError("Login failed. Check credentials and try again.") return True async def get_locations(self) -> list[Location]: """Get all locations.""" if self.token == "": raise LoginError("Login first.") headers = {"Authorization": f"Bearer {self.token}"} response, data = await request( LOCATIONS_URL, "GET", self.websession, headers=headers ) if response.status != 200: raise HTTPError( f"Failed to get locations, status code: {str(response.status)}", data, ) return [ Location(id=location["id"], name=location["name"]) for location in data["items"] ] async def get_sensors( self, location: Location, tz: str = "America/New_York", start: str = "", end: str = "", ) -> list[Sensor]: """Get all sensors.""" if self.token == "": raise LoginError("Login first.") # Validate the timezone await async_get_time_zone(tz) # Check if the start and end times are valid if start != "" and end != "": # Check if it is a valid Unix timestamp try: datetime.datetime.fromtimestamp(int(start)) datetime.datetime.fromtimestamp(int(end)) except ValueError as e: raise ValueError("Invalid start or end time.") from e if start > end: raise ValueError("Start time cannot be after end time.") headers = {"Authorization": f"Bearer {self.token}"} sensors_url = SENSORS_URL.format(location_id=str(location.id)) response, data = await request( sensors_url, "GET", self.websession, headers=headers ) if response.status != 200: raise HTTPError( f"Failed to get location, status code: {str(response.status)}", data, ) aggregates = "ai.ticks.1" devices = [] for device in data.get("items"): sensor = device.get("sensor") device = { "name": device.get("name"), "device_id": device.get("id"), "type": sensor.get("type").get("name"), "sensor_id": sensor.get("id"), "sensor_field_names": [ x for x in sensor.get("fields") if x.lower() != "notsupported" ], "location": location, "permissions": sensor.get("permissions"), "model": sensor.get("type").get("name"), } fields_str = ( ",".join(device["sensor_field_names"]) if device["sensor_field_names"] else None ) url = DEVICE_URL.format( id=device["device_id"], fields=fields_str, tz=tz, _from=start, _to=end, agg=aggregates, ) headers = {"Authorization": f"Bearer {self.token}"} response, data = await request(url, "GET", self.websession, headers=headers) if response.status != 200: raise HTTPError( f"Failed to get sensor, status code: {str(response.status)}", data ) device["data"] = data.get("ref.user-device." + device["device_id"])[ "ai.ticks.1" ]["fields"] device = Sensor(**device) devices.append(device) return devices async def get_devices(self, location: Location) -> list[Sensor]: """Get all devices.""" if self.token == "": raise LoginError("Login first.") headers = {"Authorization": f"Bearer {self.token}"} response, data = await request( SENSORS_URL.format(location_id=str(location.id)), "GET", self.websession, headers=headers, ) if response.status != 200: raise HTTPError( f"Failed to get devices, status code: {str(response.status)}", data, ) # convert to sensor models devices = [] for device in data.get("items"): sensor = device.get("sensor") device = { "name": device.get("name"), "device_id": device.get("id"), "type": sensor.get("type").get("name"), "sensor_id": sensor.get("id"), "sensor_field_names": [ x for x in sensor.get("fields") if x.lower() != "notsupported" ], "location": location, "permissions": sensor.get("permissions"), "model": sensor.get("type").get("name"), } devices.append(Sensor(**device)) return devices async def get_sensor_status(self, sensor: Sensor, tz: str = "America/New_York") -> dict[str, Any]: """Get the status of a sensor.""" if self.token == "": raise LoginError("Login first.") # Validate the timezone await async_get_time_zone(tz) headers = {"Authorization": f"Bearer {self.token}"} url = STATUS_URL.format(id=sensor.device_id, tz=tz) response, data = await request(url, "GET", self.websession, headers=headers) if response.status != 200: raise HTTPError( f"Failed to get sensor status, status code: {str(response.status)}", data, ) return data async def logout(self) -> bool: """Logout from the LaCrosse API.""" url = ( "https://lax-gateway.appspot.com/" "_ah/api/lacrosseClient/v1.1/user/devices" "?prettyPrint=false" ) headers = {"Authorization": f"Bearer {self.token}"} body = { "firebaseRegistrationToken": "fpxASxqXfE_rvyNdMGe2Bd:APA91bH53k_fq0pWNpIwTla9CiOQgx8G1PLrKpp74AfdTHPgwh3g0RZNopQQ-POqmNVyaW_2vT9I7nYz0RdWqY1DU4uNIx4vOzZPQwn7mHD6uvtYH8qxwedB3cLOBmSpOdAOkH2jTN4c" } response, data = await request( url, "DELETE", self.websession, json=body, headers=headers ) if response.status != 200: raise HTTPError(f"Failed to logout, status code: {str(response.status)}") if data["message"] != "Operation Successful": raise HTTPError("Failed to logout, message: " + data["message"]) self.token = "" return True class Location(BaseModel): """Location.""" id: str name: str class Sensor(BaseModel): """Results from get_sensors.""" name: str device_id: str type: str sensor_id: str sensor_field_names: list[str] location: Location permissions: dict[str, bool] model: str data: dict[str, Any] | None = None class LaCrosseError(Exception): """Basic exception class for LaCrosse errors.""" class LoginError(LaCrosseError): """Exception for login errors.""" class HTTPError(LaCrosseError): """Exception for HTTP errors.""" lacrosse_view-1.1.1/src/lacrosse_view/const.py000066400000000000000000000015241475026247000215240ustar00rootroot00000000000000"""Constants for LaCrosse View.""" DEVICE_URL = ( "https://ingv2.lacrossetechnology.com/" "api/v1.1/active-user/device-association/ref.user-device.{id}/" "feed?fields={fields}&" "tz={tz}&" "from={_from}&" "to={_to}&" "aggregates={agg}&" "types=spot" ) SENSORS_URL = "https://lax-gateway.appspot.com/_ah/api/lacrosseClient/v1.1/active-user/location/{location_id}/sensorAssociations?prettyPrint=false" LOCATIONS_URL = ( "https://lax-gateway.appspot.com/" "_ah/api/lacrosseClient/v1.1/active-user/locations" ) LOGIN_URL = ( "https://www.googleapis.com/" "identitytoolkit/v3/relyingparty/verifyPassword?" "key=AIzaSyD-Uo0hkRIeDYJhyyIg-TvAv8HhExARIO4" ) STATUS_URL = ( "https://ingv2.lacrossetechnology.com/" "api/v1.1/active-user/device-association/ref.user-device.{id}/" "status?tz={tz}" ) lacrosse_view-1.1.1/src/lacrosse_view/py.typed000066400000000000000000000000001475026247000215070ustar00rootroot00000000000000lacrosse_view-1.1.1/src/lacrosse_view/util.py000066400000000000000000000017031475026247000213520ustar00rootroot00000000000000"""Utilities for LaCrosse View.""" import aiohttp from typing import Any import logging _LOGGER = logging.getLogger(__name__) async def request( url: str, method: str, websession: aiohttp.ClientSession | None, **kwargs: Any ) -> tuple[aiohttp.ClientResponse, dict[str, Any]]: kwargs.setdefault("headers", {})["User-Agent"] = "okhttp/5.0.0-alpha.11" if not websession: async with aiohttp.ClientSession() as session: async with session.request(method, url, **kwargs) as response: _LOGGER.debug("Request: %s %s", method, url) _LOGGER.debug("Response: %s", await response.text()) data = await response.json() else: async with websession.request(method, url, **kwargs) as response: _LOGGER.debug("Request: %s %s", method, url) _LOGGER.debug("Response: %s", await response.text()) data = await response.json() return response, data