pax_global_header00006660000000000000000000000064147561354150014525gustar00rootroot0000000000000052 comment=27e5baf7f4c29425521ae89c9617eda4d3b8550b pyprosegur-0.0.14/000077500000000000000000000000001475613541500140265ustar00rootroot00000000000000pyprosegur-0.0.14/.github/000077500000000000000000000000001475613541500153665ustar00rootroot00000000000000pyprosegur-0.0.14/.github/workflows/000077500000000000000000000000001475613541500174235ustar00rootroot00000000000000pyprosegur-0.0.14/.github/workflows/python-publish.yml000066400000000000000000000015401475613541500231330ustar00rootroot00000000000000# This workflows 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 name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Create Release run: | python3 setup.py sdist - name: pypi-publish uses: pypa/gh-action-pypi-publish@v1.5.0 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} pyprosegur-0.0.14/.gitignore000066400000000000000000000034071475613541500160220ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ pyprosegur-0.0.14/CHANGES.md000066400000000000000000000012701475613541500154200ustar00rootroot000000000000000.0.14 - Restore status information from /installation endpoint 0.0.13 - Pin User-Agent 0.0.12 - Support for UY clients 0.0.11 - Update API url (in accordance with Prosegur App) - Add panel-status - Change alarm status endpoint from installation to panel-status (in accordance with Prosegur App) 0.0.10 - Support for PY clients 0.0.9 - Support for multiple contracts 0.0.8 - Supports Cameras (get and request images) - Changes required by Home Assistant 0.0.7 - Support for CO clients 0.0.6 (unreleased) - Add last-event object, add support for Argentina 0.0.5 - Add exponential backoff retries 0.0.4 - Support ES clients - Support for MovistarProsegurAlarms 0.0.1 - Initial Release pyprosegur-0.0.14/LICENSE000066400000000000000000000020541475613541500150340ustar00rootroot00000000000000MIT License Copyright (c) 2020 Diogo Gomes 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. pyprosegur-0.0.14/README.md000066400000000000000000000011541475613541500153060ustar00rootroot00000000000000[![PyPI version](https://badge.fury.io/py/pyprosegur.svg)](https://badge.fury.io/py/pyprosegur) # PyProsegur Python library to retrieve information from [Prosegur Residential Alarms](http://www.prosegur.pt) ## Requirements - backoff - aiohttp - aiofile - Click ## Changelog [See CHANGES.md](CHANGES.md) ## NOTES: This project has no relationship with Prosegur (unofficial library). The component uses the API provided by the Web Application. Has been developed/tested in Portugal, but should also work in other countries (please open an Issue and report your success!) ## LICENCE: Code is under MIT licence. pyprosegur-0.0.14/cli.py000066400000000000000000000141761475613541500151600ustar00rootroot00000000000000"""Command Line Interface.""" import pprint import logging import aiohttp import asyncclick as click from pyprosegur.auth import Auth, COUNTRY from pyprosegur.installation import Installation from pyprosegur.exceptions import ProsegurException logging.basicConfig(level=logging.DEBUG) @click.group() @click.argument("username") @click.argument("password") @click.argument("country", type=click.Choice(COUNTRY.keys(), case_sensitive=True)) @click.pass_context async def prosegur(ctx, username, password, country): """Set common arguments.""" ctx.ensure_object(dict) ctx.obj["username"] = username ctx.obj["password"] = password ctx.obj["country"] = country @click.command() @click.pass_context async def list_install(ctx): """Get List of installations.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installations = await Installation.list(auth) pprint.pprint(installations) @click.command() @click.argument("contract") @click.pass_context async def installation(ctx, contract): """Get installation status.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) pprint.pprint(installation.data) pprint.pprint(installation.status) @click.command() @click.argument("contract") @click.pass_context async def status(ctx, contract): """Get alarm status.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.panel_status(auth) print(installation.status) @click.command() @click.argument("contract") @click.pass_context async def arm(ctx, contract): """Arm the Alarm Panel.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.arm(auth) print(r) @click.command() @click.argument("contract") @click.pass_context async def disarm(ctx, contract): """Disarm the Alarm Panel.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.disarm(auth) print(r) @click.command() @click.argument("contract") @click.pass_context async def activity(ctx, contract): """Get Alarm Panel Activity.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.activity(auth) pprint.pprint(r) @click.command() @click.argument("contract") @click.pass_context async def panel_status(ctx, contract): """Get Alarm Panel Panel Status.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.panel_status(auth) pprint.pprint(r) @click.command() @click.argument("contract") @click.pass_context async def last_event(ctx, contract): """Get the last event.""" username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.last_event(auth) pprint.pprint(r) @click.command() @click.pass_context @click.argument("contract") @click.argument("camera") async def get_image(ctx, contract, camera): """Get CAMERA image. CAMERA is the Camera ID to get the image from. """ username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: try: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.get_image(auth, camera, save_to_disk=True) except ProsegurException as err: logging.error("Image doesn't exist: %s", err) @click.command() @click.pass_context @click.argument("contract") @click.argument("camera") async def request_image(ctx, contract, camera): """Request new CAMERA image. CAMERA is the Camera ID to request a new image from. """ username = ctx.obj["username"] password = ctx.obj["password"] country = ctx.obj["country"] async with aiohttp.ClientSession() as session: auth = Auth(session, username, password, country) installation = await Installation.retrieve(auth, contract) r = await installation.request_image(auth, camera) pprint.pprint(r) prosegur.add_command(list_install) prosegur.add_command(installation) prosegur.add_command(arm) prosegur.add_command(disarm) prosegur.add_command(activity) prosegur.add_command(last_event) prosegur.add_command(get_image) prosegur.add_command(request_image) prosegur.add_command(panel_status) prosegur.add_command(status) if __name__ == "__main__": try: prosegur(obj={}) except Exception as err: print(err) pyprosegur-0.0.14/pyprosegur.code-workspace000066400000000000000000000000741475613541500210760ustar00rootroot00000000000000{ "folders": [ { "path": "." } ], "settings": {} }pyprosegur-0.0.14/pyprosegur/000077500000000000000000000000001475613541500162455ustar00rootroot00000000000000pyprosegur-0.0.14/pyprosegur/__init__.py000066400000000000000000000000611475613541500203530ustar00rootroot00000000000000"""pyprosegur library.""" __version__ = "0.0.14" pyprosegur-0.0.14/pyprosegur/auth.py000066400000000000000000000111041475613541500175550ustar00rootroot00000000000000"""Authentication and Request Helper.""" import logging import backoff from aiohttp import ClientSession, ClientResponse from pyprosegur.exceptions import BackendError, NotFound LOGGER = logging.getLogger(__name__) SMART_SERVER_WS = "https://api-smart.prosegur.cloud/smart-server/ws" COUNTRY = { "AR": { "Origin": "https://smart.prosegur.com/smart-individuo", "Referer": "https://smart.prosegur.com/smart-individuo/login.html", "origin": "Web", }, "PY": { "Origin": "https://smart.prosegur.com/smart-individuo", "Referer": "https://smart.prosegur.com/smart-individuo/login.html", "origin": "Web", }, "PT": { "Origin": "https://smart.prosegur.com/smart-individuo", "Referer": "https://smart.prosegur.com/smart-individuo/login.html", "origin": "Web", }, "ES": { "Origin": "https://alarmas.movistarproseguralarmas.es", "Referer": "https://alarmas.movistarproseguralarmas.es/smart-mv/login.html", "origin": "WebM", }, "CO": { "Origin": "https://smart.prosegur.com/smart-individuo", "Referer": "https://smart.prosegur.com/smart-individuo/login.html", "origin": "Web", }, "UY": { "Origin": "https://smart.prosegur.com/smart-individuo", "Referer": "https://smart.prosegur.com/smart-individuo/login.html", "origin": "Web", }, } class Auth: """Class to make authenticated requests.""" def __init__( self, websession: ClientSession, user: str, password: str, country: str ): """Initialize the auth.""" self.websession = websession self.user = user self.password = password self.country = country if country not in COUNTRY: raise ValueError(f"{country} not in {COUNTRY.keys()}") self.smart_token = None self.headers = { "User-Agent": "Smart/1 CFNetwork/3826.400.120 Darwin/24.3.0", #not proud, as we are starting a cat and mouse game "Accept": "application/json, text/plain, */*", "Content-Type": "application/json;charset=UTF-8", "Origin": COUNTRY[self.country]["Origin"], "Referer": COUNTRY[self.country]["Referer"], } async def login(self): """Login, retrieving Smart Token used for all requests.""" data = { "user": self.user, "password": self.password, "language": "en_GB", "origin": COUNTRY[self.country]["origin"], "platform": "smart2", "provider": None, } response = await self.websession.post( f"{SMART_SERVER_WS}/access/login", json=data, headers=self.headers, ) if response.status != 200: LOGGER.error("%s LOGIN FAILED: %s", response.status, response.reason) raise ConnectionRefusedError( f"Could not login: {response.status} {response.reason}" ) login = await response.json() self.headers["X-Smart-Token"] = login["data"]["token"] @backoff.on_exception( backoff.expo, ConnectionRefusedError, max_tries=1, logger=LOGGER ) @backoff.on_exception( backoff.expo, ConnectionError, base=5, max_tries=3, logger=LOGGER ) async def request(self, method: str, path: str, **kwargs) -> ClientResponse: """Make a request.""" if self.websession.closed: raise ConnectionError("websession with smart.prosegur is closed") headers = kwargs.get("headers") if headers is None: headers = self.headers else: headers = {**self.headers, **headers} if "X-Smart-Token" not in headers: LOGGER.debug("No X-Smart-Token, attempting login") await self.login() resp = await self.websession.request( method, f"{SMART_SERVER_WS}{path}", **kwargs, headers=headers, ) LOGGER.debug("Requesting %s %s %s", f"{SMART_SERVER_WS}{path}", kwargs, headers) if 500 <= resp.status <= 600: LOGGER.warning(resp.text) raise BackendError("Prosegur backend is unresponsive") if 404 == resp.status: raise NotFound() if 400 <= resp.status < 500: del self.headers["X-Smart-Token"] raise ConnectionRefusedError() if resp.status != 200: LOGGER.error(resp.text) raise ConnectionError( f"{resp.status} couldn't {method} {path}: {resp.text}" ) return resp pyprosegur-0.0.14/pyprosegur/exceptions.py000066400000000000000000000005111475613541500207750ustar00rootroot00000000000000"""Exceptions raised by pyprosegur.""" class ProsegurException(Exception): """Exception raised py pyprosegur.""" class BackendError(ProsegurException): """Error to indicate backend did not return something usefull.""" class NotFound(ProsegurException): """Error to indicate the request object was not found.""" pyprosegur-0.0.14/pyprosegur/installation.py000066400000000000000000000162111475613541500213210ustar00rootroot00000000000000"""Installation Representation.""" import enum import logging import aiofiles from aiohttp import ClientConnectionError from dataclasses import dataclass from datetime import datetime, timedelta from pyprosegur.auth import Auth from pyprosegur.exceptions import BackendError, NotFound LOGGER = logging.getLogger(__name__) class Status(enum.Enum): """Alarm Panel Status.""" ALARM = "LE" ARMED = "AT" DISARMED = "DA" ERROR = "error" PARTIALLY = "AP" POWER_FAILURE = "FC" POWER_RESTORED = "RFC" IMAGE = "IM" ERROR_DISARMED = "EDA" ERROR_ARMED_TOTAL = "EAT" ERROR_PARTIALLY = "EAP" ERROR_ARMED_TOTAL_COMMUNICATIONS = "EAT-COM" ERROR_DISARMED_COMMUNICATIONS = "EDA-COM" ERROR_PARTIALLY_COMMUNICATIONS = "EAP-COM" ERROR_IMAGE_COMMUNCATIONS = "EIM-COM" @staticmethod def from_str(code): """Convert Status Code to Enum.""" for status in Status: if code == str(status.value): return status raise NotImplementedError(f"'{code}' not an implemented Installation.Status") @dataclass class Event: """Event in a Prosegur Alarm.""" ts: datetime id: str operation: Status by: str @dataclass class Camera: """Prosegur camera.""" id: str description: str class Installation: """Alarm Panel Installation.""" def __init__(self, contractId): """Installation properties.""" self.data = None self.contractId = contractId self.installationId = None self.cameras = [] self._status = Status.ERROR @classmethod async def list(cls, auth: Auth): """Retrieve list of constract associated with user.""" try: resp = await auth.request("GET", "/installation") except ClientConnectionError as err: raise BackendError from err resp_json = await resp.json() if resp_json["result"]["code"] != 200: LOGGER.error(resp_json["result"]) raise BackendError(resp_json["result"]) return [ {"contractId": install["contractId"], "description": install["description"]} for install in resp_json["data"] ] @classmethod async def retrieve(cls, auth: Auth, contractId): """Retrieve an installation object.""" self = Installation(contractId) try: resp = await auth.request("GET", "/installation") except ClientConnectionError as err: raise BackendError from err resp_json = await resp.json() if resp_json["result"]["code"] != 200: LOGGER.error(resp_json["result"]) raise BackendError(resp_json["result"]) for install in resp_json["data"]: if install["contractId"] == contractId: self.data = install if not self.data: raise NotFound(f"Contract {contractId} not found") self.installationId = self.data["installationId"] self._status = Status.from_str(self.data["status"]) for camera in self.data["detectors"]: if camera["type"] == "Camera": self.cameras.append(Camera(camera["id"], camera["description"])) return self @property def contract(self): """Contract Identifier.""" return self.contractId @property def status(self): """Alarm Panel Status.""" return self._status async def arm(self, auth: Auth): """Order Alarm Panel to Arm itself.""" if self.status == Status.ARMED: return True data = {"statusCode": Status.ARMED.value} resp = await auth.request( "PUT", f"/installation/{self.installationId}/status", json=data ) LOGGER.debug("ARM HTTP status: %s\t%s", resp.status, await resp.text()) return resp.status == 200 async def arm_partially(self, auth: Auth): """Order Alarm Panel to Arm Partially itself.""" if self.status == Status.PARTIALLY: return True data = {"statusCode": Status.PARTIALLY.value} resp = await auth.request( "PUT", f"/installation/{self.installationId}/status", json=data ) LOGGER.debug("ARM HTTP status: %s\t%s", resp.status, await resp.text()) return resp.status == 200 async def disarm(self, auth: Auth): """Order Alarm Panel to Disarm itself.""" if self.status == Status.DISARMED: return True data = {"statusCode": Status.DISARMED.value} resp = await auth.request( "PUT", f"/installation/{self.installationId}/status", json=data ) LOGGER.debug("DISARM HTTP status: %s\t%s", resp.status, await resp.text()) return resp.status == 200 async def activity(self, auth: Auth, delta=timedelta(hours=24)): """Retrieve activity events.""" date = datetime.now() - delta ts = int(date.timestamp()) * 1000 resp = await auth.request( "GET", f"/event/installation/{self.installationId}/less?limitDate?{ts}" ) json = await resp.json() LOGGER.debug("Activity: %s", json) return json async def panel_status(self, auth: Auth): """Retrieve Panel Status.""" resp = await auth.request( "GET", f"/installation/{self.installationId}/panel-status" ) json = await resp.json() LOGGER.debug("Panel Status: %s", json) if "data" in json and "status" in json["data"]: self._status = Status.from_str(json["data"]["status"]) else: self._status = Status.ERROR LOGGER.error("Installation Panel Status could not be updated: %s", json) return json async def last_event(self, auth: Auth): """Return Last Event.""" _all = await self.activity(auth) def extract_by(description): if " by " in description: return description.split(" by ")[1] return None if "data" in _all: event = sorted(_all["data"], key=lambda x: x["creationDate"], reverse=True) if len(event): return Event( ts=datetime.fromtimestamp(event[0]["creationDate"] / 1000), id=event[0]["id"], operation=Status.from_str(event[0]["operation"]), by=extract_by(event[0]["description"]), ) return None async def get_image(self, auth: Auth, camera: str, save_to_disk=False): """Retrieve image stored in prosegur backend.""" resp = await auth.request("GET", f"/image/device/{camera}/last") if save_to_disk: f = await aiofiles.open(f"{camera}.jpg", mode="wb") await f.write(await resp.read()) await f.close() else: return await resp.read() async def request_image(self, auth: Auth, camera: str): """Request image update.""" data = [camera] resp = await auth.request( "POST", f"/installation/{self.installationId}/images", json=data ) json = await resp.json() LOGGER.debug("Request Image %s: %s", camera, json) return json pyprosegur-0.0.14/requirements.txt000066400000000000000000000001201475613541500173030ustar00rootroot00000000000000backoff==1.10.0 asyncclick==7.1.2.3 click==7.1.2 aiohttp==3.7.3 aiofiles==0.8.0 pyprosegur-0.0.14/setup.py000066400000000000000000000021211475613541500155340ustar00rootroot00000000000000from setuptools import setup, find_packages import pyprosegur long_description = open("README.md").read() setup( name="pyprosegur", version=pyprosegur.__version__, license="MIT License", url="https://github.com/dgomes/pyprosegur", author="Diogo Gomes", author_email="diogogomes@gmail.com", description="Unofficial Python library to interface with Prosegur Alarmes PT/ES.", long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), include_package_data=True, zip_safe=True, platforms="any", install_requires=[ "backoff", "aiohttp", "click", ], entry_points=""" [console_scripts] pyprosegur=pyprosegur.cli:prosegur """, classifiers=[ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], )