Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jcgoette committed Jun 15, 2021
0 parents commit 1516458
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
paths:
- "**.py"
pull_request:
paths:
- "**.py"

jobs:
lint-isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.8
- uses: jamescurtin/isort-action@master
with:
configuration: "--check-only --diff --profile black"
lint-black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
options: ". --check"
TODO:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@master"
- uses: "alstr/[email protected]"
with:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Python files
*.pyc
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Justin Goette

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.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# baby_buddy_homeassistant

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)

This custom integration provides sensors for Weight Gurus API endpoints within [Home Assistant](https://github.com/home-assistant/core). Thanks to [@MarkWalters-pw](https://github.com/MarkWalters-pw) for the [heavy lifting of API documentation](https://gist.github.com/MarkWalters-pw/08ea0e8737e3e4d11f70427ef8fdc7df).

## Installation

### HACS

1. Go to any of the sections (integrations, frontend, automation).
1. Click on the 3 dots in the top right corner.
1. Select "Custom repositories"
1. Add the URL (i.e., https://github.com/jcgoette/weight_gurus_homeassistant) to the repository.
1. Select the Integration category.
1. Click the "ADD" button.

## Configuration

1. Go to "Configuration".
1. Click the "Integrations" button.
1. Click on the "ADD INTEGRATION" in the bottom right corner.
1. Search for Weight Gurus
1. Enter email and password and click "Submit"
13 changes: 13 additions & 0 deletions custom_components/weight_gurus/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Weight Gurus integration"""
from .const import DOMAIN


async def async_setup_entry(hass, entry):
"""Set up Weight Gurus platform from a ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = entry.data

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True
24 changes: 24 additions & 0 deletions custom_components/weight_gurus/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD

from .const import ATTR_DEFAULT_NAME, DOMAIN


# TODO: better validation of data
# TODO: translations
class WeightGurusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input):
if user_input is not None:
return self.async_create_entry(title=ATTR_DEFAULT_NAME, data=user_input)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
),
)
15 changes: 15 additions & 0 deletions custom_components/weight_gurus/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Weight Gurus integration constants"""
DOMAIN = "weight_gurus"

ATTR_ACCESS_TOKEN = "accessToken"
ATTR_ACCOUNT = "account"
ATTR_DECIMAL_VALUES = "decimalValues"
ATTR_DEFAULT_NAME = "Weight Gurus"
ATTR_EMAIL_REGEX = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
ATTR_ENTRY_TIMESTAMP = "entryTimestamp"
ATTR_FIRST_NAME = "firstName"
ATTR_ICON = "mdi:scale-bathroom"
ATTR_LAST_NAME = "lastName"
ATTR_OPERATIONS = "operations"
ATTR_URL = "https://api.weightgurus.com/v3"
ATTR_WEIGHT = "weight"
11 changes: 11 additions & 0 deletions custom_components/weight_gurus/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"codeowners": ["@jcgoette"],
"config_flow": true,
"documentation": "https://github.com/jcgoette/weight_gurus_homeassistant",
"domain": "weight_gurus",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/jcgoette/weight_gurus_homeassistant/issues",
"name": "Weight Gurus",
"requirements": ["stringcase==1.2.0"],
"version": "v1.0.0"
}
152 changes: 152 additions & 0 deletions custom_components/weight_gurus/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Platform for Weight Gurus sensor integration."""
import logging
from datetime import timedelta

import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
import requests
import stringcase
import voluptuous as vol
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, MASS_KILOGRAMS, MASS_POUNDS
from homeassistant.helpers.entity import Entity

from .const import (
ATTR_ACCESS_TOKEN,
ATTR_DECIMAL_VALUES,
ATTR_DEFAULT_NAME,
ATTR_ENTRY_TIMESTAMP,
ATTR_ICON,
ATTR_OPERATIONS,
ATTR_URL,
ATTR_WEIGHT,
DOMAIN,
)

# TODO: add error logging
_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(minutes=5)


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Setup Weight Gurus sensors from a config entry created in the integrations UI."""
config = hass.data[DOMAIN][config_entry.entry_id]

unit_system = hass.config.units.is_metric
email = config[CONF_EMAIL]
password = config[CONF_PASSWORD]

weight_gurus_data = WeightGurusData(email, password)

async_add_entities(
[WeightGurusSensor(unit_system, weight_gurus_data)], update_before_add=True
)


class WeightGurusSensor(Entity):
"""Representation of a Weight Gurus Sensor."""

def __init__(self, unit_system, weight_gurus_data):
"""Initialize the Weight Gurus sensor."""
self._state = None
self._unit_system = unit_system
self._weight_gurus_data = weight_gurus_data
self._data = self._weight_gurus_data._data

@property
def name(self):
"""Return the name of the Weight Gurus sensor."""
return ATTR_DEFAULT_NAME

@property
def state(self):
"""Return the state of the Weight Gurus sensor."""
# TODO: better way to handle significant digits
if self._unit_system:
return round(
self._data.get(ATTR_DECIMAL_VALUES).get(ATTR_WEIGHT) * 0.45359, 1
)
return self._data.get(ATTR_DECIMAL_VALUES).get(ATTR_WEIGHT)

@property
def unit_of_measurement(self):
"""Return the unit of measurement of the Weight Gurus sensor state."""
if self._unit_system:
return MASS_KILOGRAMS
return MASS_POUNDS

@property
def extra_state_attributes(self):
"""Return entity specific state attributes for Weight Gurus."""
self._extra_state_attributes = self._data.get(ATTR_DECIMAL_VALUES)

entry_timestamp = self._data.get(ATTR_ENTRY_TIMESTAMP)
entry_timestamp = dt_util.parse_datetime(entry_timestamp)
localized_timestamp = dt_util.as_local(entry_timestamp)

self._extra_state_attributes[ATTR_ENTRY_TIMESTAMP] = localized_timestamp

self._extra_state_attributes = {
stringcase.snakecase(key): value
for (key, value) in sorted(
self._extra_state_attributes.items(), key=lambda item: item[0]
)
if key != ATTR_WEIGHT
}
return self._extra_state_attributes

@property
def icon(self):
"""Return the icon to use in Weight Gurus frontend."""
return ATTR_ICON

def update(self):
"""Update data from Weight Gurus for the sensor."""
self._weight_gurus_data.update()
self._data = self._weight_gurus_data._data


# TODO: PYPI package
class WeightGurusData:
"""Coordinate retrieving and updating data from Weight Gurus."""

def __init__(self, email, password):
"""Initialize the WeightGurusData object."""
self._data = None
self._email = email
self._password = password

def WeightGurusQuery(self, email, password):
"""Query Weight Gurus for data via hidden API."""
data_account = {CONF_EMAIL: email, CONF_PASSWORD: password}
account_response = requests.post(f"{ATTR_URL}/account/login", data=data_account)
account_json = account_response.json()

account_access_token = account_json[ATTR_ACCESS_TOKEN]

headers_operation = {
"Authorization": f"Bearer {account_access_token}",
"Accept": "application/json, text/plain, */*",
}
operation_response = requests.get(
f"{ATTR_URL}/operation/", headers=headers_operation
)
operation_json = operation_response.json()

# TODO: account for deleted entries
for entry in operation_json[ATTR_OPERATIONS][-1:]:
entry_data = {}

entry_data[ATTR_ENTRY_TIMESTAMP] = entry.get(ATTR_ENTRY_TIMESTAMP)

entry_data[ATTR_DECIMAL_VALUES] = {
key: value / 10
for (key, value) in entry.items()
if isinstance(value, int)
}

return entry_data

def update(self):
"""Update data from Weight Gurus via WeightGurusQuery."""
self._data = self.WeightGurusQuery(self._email, self._password)
13 changes: 13 additions & 0 deletions custom_components/weight_gurus/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"title": "Weight Gurus Login Information",
"data": {
"email": "email",
"password": "password"
}
}
}
}
}
6 changes: 6 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"domains": "sensor",
"iot_class": "Cloud Polling",
"name": "Weight Gurus",
"render_readme": true
}

0 comments on commit 1516458

Please sign in to comment.