Skip to content

Commit

Permalink
Initial implementation of bhyve integration
Browse files Browse the repository at this point in the history
  • Loading branch information
sebr committed Dec 17, 2019
0 parents commit 49a6a49
Show file tree
Hide file tree
Showing 11 changed files with 538 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
*.pyc

23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# bhyve-home-assistant

BHyve component for [Home Assistant](https://www.home-assistant.io/).

## Supported Features
* Battery sensor for `sprinkler_timer` devices

## Installation

Recommended installation is via HACS
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs)

### Sample Configuration

```yaml
bhyve:
username: !secret bhyve_username
password: !secret bhyve_password

sensor:
- platform: bhyve
```
64 changes: 64 additions & 0 deletions custom_components/bhyve/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging
from datetime import timedelta

import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout

from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_HOST)
from homeassistant.helpers import config_validation as cv

__version__ = '0.0.1'

_LOGGER = logging.getLogger(__name__)

CONF_ATTRIBUTION = "Data provided by api.orbitbhyve.com"
DATA_BHYVE = 'data_bhyve'
DEFAULT_BRAND = 'Orbit BHyve'
DOMAIN = 'bhyve'

NOTIFICATION_ID = 'bhyve_notification'
NOTIFICATION_TITLE = 'Orbit BHyve Component Setup'

DEFAULT_HOST = 'https://api.orbitbhyve.com'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.url
}),
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
"""Set up the BHyve component."""

_LOGGER.info('bhyve loading')

# Read config
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
host = conf.get(CONF_HOST)

_LOGGER.info(host)

try:
from .pybhyve import PyBHyve

bhyve = PyBHyve(username=username, password=password)
if not bhyve.is_connected:
return False

hass.data[DATA_BHYVE] = bhyve

except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Orbit BHyve: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />You will need to restart hass after fixing.'.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False

return True
8 changes: 8 additions & 0 deletions custom_components/bhyve/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"domain": "bhyve",
"name": "Orbit BHyve Integration",
"documentation": "https://github.com/sebr/bhyve-home-assistant/blob/master/README.md",
"dependencies": [],
"codeowners": ["@sebr"],
"requirements": []
}
88 changes: 88 additions & 0 deletions custom_components/bhyve/pybhyve/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import base64
import datetime
import logging
import os
import threading
import time

from .backend import BHyveBackEnd
from .config import BHyveConfig
from .constant import (DEVICES_PATH, API_POLL_PERIOD)
from .device import BHyveDevice

logging.basicConfig(level=logging.DEBUG)
_LOGGER = logging.getLogger('pybhyve')

__version__ = '0.0.1'


class PyBHyve(object):

def __init__(self, **kwargs):

self.info('pybhyve loading')
# Set up the config first.
self._cfg = BHyveConfig(self, **kwargs)

self._be = BHyveBackEnd(self)

self._devices = []

self.info('pybhyve starting')
self._last_poll = 0
self._refresh_devices()

def __repr__(self):
# Representation string of object.
return "<{0}: {1}>".format(self.__class__.__name__, self._cfg.name)

def _refresh_devices(self):
now = time.time()
if (now - self._last_poll < API_POLL_PERIOD):
self.debug('Skipping refresh, not enough time has passed')
return

self._devices = []
devices = self._be.get(DEVICES_PATH + "?t={}".format(time.time()))

for device in devices:
deviceName = device.get('name')
deviceType = device.get('type')
self.info('Created device: {} [{}]'.format(deviceType, deviceName))
self._devices.append(BHyveDevice(deviceName, self, device))

self._last_poll = now

@property
def cfg(self):
return self._cfg

@property
def devices(self):
return self._devices

@property
def is_connected(self):
return self._be.is_connected()

def get_device(self, device_id):
self._refresh_devices()
for device in self._devices:
if device.device_id == device_id:
return device
return None

def update(self):
pass

def error(self, msg):
_LOGGER.error(msg)

def warning(self, msg):
_LOGGER.warning(msg)

def info(self, msg):
_LOGGER.info(msg)

def debug(self, msg):
_LOGGER.debug(msg)
113 changes: 113 additions & 0 deletions custom_components/bhyve/pybhyve/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import json
import pprint
import re
import threading
import time
import uuid

import requests
import requests.adapters

from .constant import (LOGIN_PATH, DEVICES_PATH)

# include token and session details
class BHyveBackEnd(object):

def __init__(self, bhyve):

self._bhyve = bhyve
self._lock = threading.Condition()
self._req_lock = threading.Lock()

self._requests = {}
self._callbacks = {}

self._token = None

# login
self._session = None
self._logged_in = self._login()
if not self._logged_in:
self._bhyve.warning('failed to log in')
return


def _request(self, path, method='GET', params=None, headers=None, stream=False, timeout=None):
if params is None:
params = {}
if headers is None:
headers = {}
if timeout is None:
timeout = self._bhyve.cfg.request_timeout
try:
with self._req_lock:
url = self._bhyve.cfg.host + path
self._bhyve.debug('starting request=' + str(url))
# self._bhyve.debug('starting request=' + str(params))
# self._bhyve.debug('starting request=' + str(headers))
if method == 'GET':
r = self._session.get(url, params=params, headers=headers, stream=stream, timeout=timeout)
if stream is True:
return r
elif method == 'PUT':
r = self._session.put(url, json=params, headers=headers, timeout=timeout)
elif method == 'POST':
r = self._session.post(url, json=params, headers=headers, timeout=timeout)
except Exception as e:
self._bhyve.warning('request-error={}'.format(type(e).__name__))
return None

self._bhyve.debug('finish request=' + str(r.status_code))
if r.status_code != 200:
return None

body = r.json()
# self._bhyve.debug(pprint.pformat(body, indent=2))

return body

# login and set up session
def _login(self):

# attempt login
self._session = requests.Session()
body = self.post(LOGIN_PATH, { 'session': {'email': self._bhyve.cfg.username, 'password': self._bhyve.cfg.password} })
if body is None:
self._bhyve.debug('login failed')
return False

# save new login information
self._token = body['orbit_session_token']
self._user_id = body['user_id']

headers = {
'Accept': 'application/json, text/plain, */*',
'Host': re.sub('https?://', '', self._bhyve.cfg.host),
'Content-Type': 'application/json; charset=utf-8;',
'Referer': self._bhyve.cfg.host,
'Orbit-Session-Token': self._token
}
headers['User-Agent'] = ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/72.0.3626.81 Safari/537.36')

self._session.headers.update(headers)
return True

def is_connected(self):
return self._logged_in

def get(self, path, params=None, headers=None, stream=False, timeout=None):
return self._request(path, 'GET', params, headers, stream, timeout)

def put(self, path, params=None, headers=None, timeout=None):
return self._request(path, 'PUT', params, headers, False, timeout)

def post(self, path, params=None, headers=None, timeout=None):
return self._request(path, 'POST', params, headers, False, timeout)

@property
def session(self):
return self._session

def devices(self):
return self.get(DEVICES_PATH + "?t={}".format(time.time()))
33 changes: 33 additions & 0 deletions custom_components/bhyve/pybhyve/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from .constant import DEFAULT_HOST

class BHyveConfig(object):
def __init__(self, bhyve, **kwargs):
""" The constructor.
Args:
kwargs (kwargs): Configuration options.
"""
self._bhyve = bhyve
self._kw = kwargs

@property
def name(self, default='bhyve'):
return self._kw.get('name', default)

@property
def username(self, default='unknown'):
return self._kw.get('username', default)

@property
def password(self, default='unknown'):
return self._kw.get('password', default)

@property
def host(self, default=DEFAULT_HOST):
return self._kw.get('host', default)

@property
def request_timeout(self, default=60):
return self._kw.get('request_timeout', default)

6 changes: 6 additions & 0 deletions custom_components/bhyve/pybhyve/constant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DEFAULT_HOST = 'https://api.orbitbhyve.com'

LOGIN_PATH = '/v1/session'
DEVICES_PATH = '/v1/devices'

API_POLL_PERIOD = 300
Loading

0 comments on commit 49a6a49

Please sign in to comment.