Rename 'firetv' to 'androidtv' and add Android TV functionality (#21944)

* Working on adding androidtv functionality to firetv component

* 'should_poll' must return True

* Change 'properties' to 'device_properties'

* Also mention 'Android TV' in services.yaml

* Use GitHub for 'androidtv' requirement

* Add 'androidtv==0.0.10' to requirements, remove 'firetv==1.0.9'

* Add 'GET_PROPERTIES' adb command option; use pypi for REQUIREMENTS

* Rename integration from 'firetv' to 'androidtv'

* Change default name to 'Android TV'

* Rename integration from 'firetv' to 'androidtv'

* Change firetv to androidtv in .coveragerc

* Change firetv to androidtv in requirements_all.txt

* Remove 'DEFAULT_APPS'
This commit is contained in:
Jeff Irion 2019-03-13 03:18:59 -07:00 committed by Pascal Vizeli
parent e5da7a0014
commit 007bf2bcb5
7 changed files with 284 additions and 168 deletions

View File

@ -27,6 +27,7 @@ omit =
homeassistant/components/ambient_station/* homeassistant/components/ambient_station/*
homeassistant/components/amcrest/* homeassistant/components/amcrest/*
homeassistant/components/android_ip_webcam/* homeassistant/components/android_ip_webcam/*
homeassistant/components/androidtv/*
homeassistant/components/apcupsd/* homeassistant/components/apcupsd/*
homeassistant/components/apiai/* homeassistant/components/apiai/*
homeassistant/components/apple_tv/* homeassistant/components/apple_tv/*
@ -172,7 +173,6 @@ omit =
homeassistant/components/fan/wemo.py homeassistant/components/fan/wemo.py
homeassistant/components/fastdotcom/* homeassistant/components/fastdotcom/*
homeassistant/components/fibaro/* homeassistant/components/fibaro/*
homeassistant/components/firetv/*
homeassistant/components/folder_watcher/* homeassistant/components/folder_watcher/*
homeassistant/components/foursquare/* homeassistant/components/foursquare/*
homeassistant/components/freebox/* homeassistant/components/freebox/*

View File

@ -0,0 +1,6 @@
"""
Support for functionality to interact with Android TV and Fire TV devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.androidtv/
"""

View File

@ -1,8 +1,8 @@
""" """
Support for functionality to interact with FireTV devices. Support for functionality to interact with Android TV and Fire TV devices.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.firetv/ https://home-assistant.io/components/media_player.androidtv/
""" """
import functools import functools
import logging import logging
@ -12,18 +12,25 @@ from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA) MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP)
from homeassistant.const import ( from homeassistant.const import (
ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME,
STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_STANDBY)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
FIRETV_DOMAIN = 'firetv' ANDROIDTV_DOMAIN = 'androidtv'
REQUIREMENTS = ['firetv==1.0.9'] REQUIREMENTS = ['androidtv==0.0.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_ANDROIDTV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | \
SUPPORT_VOLUME_STEP
SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \ SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP
@ -34,11 +41,15 @@ CONF_ADB_SERVER_PORT = 'adb_server_port'
CONF_APPS = 'apps' CONF_APPS = 'apps'
CONF_GET_SOURCES = 'get_sources' CONF_GET_SOURCES = 'get_sources'
DEFAULT_NAME = 'Amazon Fire TV' DEFAULT_NAME = 'Android TV'
DEFAULT_PORT = 5555 DEFAULT_PORT = 5555
DEFAULT_ADB_SERVER_PORT = 5037 DEFAULT_ADB_SERVER_PORT = 5037
DEFAULT_GET_SOURCES = True DEFAULT_GET_SOURCES = True
DEFAULT_APPS = {} DEFAULT_DEVICE_CLASS = 'auto'
DEVICE_ANDROIDTV = 'androidtv'
DEVICE_FIRETV = 'firetv'
DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
SERVICE_ADB_COMMAND = 'adb_command' SERVICE_ADB_COMMAND = 'adb_command'
@ -58,72 +69,94 @@ def has_adb_files(value):
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS):
vol.In(DEVICE_CLASSES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ADBKEY): has_adb_files, vol.Optional(CONF_ADBKEY): has_adb_files,
vol.Optional(CONF_ADB_SERVER_IP): cv.string, vol.Optional(CONF_ADB_SERVER_IP): cv.string,
vol.Optional( vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT):
CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, cv.port,
vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean,
vol.Optional( vol.Optional(CONF_APPS, default=dict()):
CONF_APPS, default=DEFAULT_APPS): vol.Schema({cv.string: cv.string}) vol.Schema({cv.string: cv.string})
}) })
# Translate from `FireTV` reported state to HA state. # Translate from `AndroidTV` / `FireTV` reported state to HA state.
FIRETV_STATES = {'off': STATE_OFF, ANDROIDTV_STATES = {'off': STATE_OFF,
'idle': STATE_IDLE, 'idle': STATE_IDLE,
'standby': STATE_STANDBY, 'standby': STATE_STANDBY,
'playing': STATE_PLAYING, 'playing': STATE_PLAYING,
'paused': STATE_PAUSED} 'paused': STATE_PAUSED}
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the FireTV platform.""" """Set up the Android TV / Fire TV platform."""
from firetv import FireTV from androidtv import setup
hass.data.setdefault(FIRETV_DOMAIN, {}) hass.data.setdefault(ANDROIDTV_DOMAIN, {})
host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT])
if CONF_ADB_SERVER_IP not in config: if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation) # Use "python-adb" (Python ADB implementation)
if CONF_ADBKEY in config: if CONF_ADBKEY in config:
ftv = FireTV(host, config[CONF_ADBKEY]) aftv = setup(host, config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY])
else: else:
ftv = FireTV(host) aftv = setup(host, device_class=config[CONF_DEVICE_CLASS])
adb_log = "" adb_log = ""
else: else:
# Use "pure-python-adb" (communicate with ADB server) # Use "pure-python-adb" (communicate with ADB server)
ftv = FireTV(host, adb_server_ip=config[CONF_ADB_SERVER_IP], aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT]) adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS])
adb_log = " using ADB server at {0}:{1}".format( adb_log = " using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
if not ftv.available: if not aftv.available:
_LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log) # Determine the name that will be used for the device in the log
if CONF_NAME in config:
device_name = config[CONF_NAME]
elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV:
device_name = 'Android TV device'
elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV:
device_name = 'Fire TV device'
else:
device_name = 'Android TV / Fire TV device'
_LOGGER.warning("Could not connect to %s at %s%s",
device_name, host, adb_log)
return return
name = config[CONF_NAME] if host in hass.data[ANDROIDTV_DOMAIN]:
get_sources = config[CONF_GET_SOURCES]
apps = config[CONF_APPS]
if host in hass.data[FIRETV_DOMAIN]:
_LOGGER.warning("Platform already setup on %s, skipping", host) _LOGGER.warning("Platform already setup on %s, skipping", host)
else: else:
device = FireTVDevice(ftv, name, get_sources, apps) if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV:
add_entities([device]) device = AndroidTVDevice(aftv, config[CONF_NAME],
_LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) config[CONF_APPS])
hass.data[FIRETV_DOMAIN][host] = device device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Android TV'
else:
device = FireTVDevice(aftv, config[CONF_NAME], config[CONF_APPS],
config[CONF_GET_SOURCES])
device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Fire TV'
if hass.services.has_service(FIRETV_DOMAIN, SERVICE_ADB_COMMAND): add_entities([device])
_LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log)
hass.data[ANDROIDTV_DOMAIN][host] = device
if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND):
return return
def service_adb_command(service): def service_adb_command(service):
"""Dispatch service calls to target entities.""" """Dispatch service calls to target entities."""
cmd = service.data.get(ATTR_COMMAND) cmd = service.data.get(ATTR_COMMAND)
entity_id = service.data.get(ATTR_ENTITY_ID) entity_id = service.data.get(ATTR_ENTITY_ID)
target_devices = [dev for dev in hass.data[FIRETV_DOMAIN].values() target_devices = [dev for dev in hass.data[ANDROIDTV_DOMAIN].values()
if dev.entity_id in entity_id] if dev.entity_id in entity_id]
for target_device in target_devices: for target_device in target_devices:
@ -134,7 +167,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.info("Output of command '%s' from '%s': %s", _LOGGER.info("Output of command '%s' from '%s': %s",
cmd, target_device.entity_id, repr(output)) cmd, target_device.entity_id, repr(output))
hass.services.register(FIRETV_DOMAIN, SERVICE_ADB_COMMAND, hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command, service_adb_command,
schema=SERVICE_ADB_COMMAND_SCHEMA) schema=SERVICE_ADB_COMMAND_SCHEMA)
@ -163,24 +196,21 @@ def adb_decorator(override_available=False):
return _adb_decorator return _adb_decorator
class FireTVDevice(MediaPlayerDevice): class ADBDevice(MediaPlayerDevice):
"""Representation of an Amazon Fire TV device on the network.""" """Representation of an Android TV or Fire TV device."""
def __init__(self, ftv, name, get_sources, apps): def __init__(self, aftv, name, apps):
"""Initialize the FireTV device.""" """Initialize the Android TV / Fire TV device."""
from firetv import APPS, KEYS from androidtv.constants import APPS, KEYS
self.apps = APPS
self.keys = KEYS
self.apps.update(apps)
self.firetv = ftv
self.aftv = aftv
self._name = name self._name = name
self._get_sources = get_sources self._apps = APPS
self._apps.update(apps)
self._keys = KEYS
# ADB exceptions to catch # ADB exceptions to catch
if not self.firetv.adb_server_ip: if not self.aftv.adb_server_ip:
# Using "python-adb" (Python ADB implementation) # Using "python-adb" (Python ADB implementation)
from adb.adb_protocol import (InvalidChecksumError, from adb.adb_protocol import (InvalidChecksumError,
InvalidCommandError, InvalidCommandError,
@ -195,10 +225,25 @@ class FireTVDevice(MediaPlayerDevice):
# Using "pure-python-adb" (communicate with ADB server) # Using "pure-python-adb" (communicate with ADB server)
self.exceptions = (ConnectionResetError,) self.exceptions = (ConnectionResetError,)
self._state = None # Property attributes
self._available = self.firetv.available self._available = self.aftv.available
self._current_app = None self._current_app = None
self._running_apps = None self._state = None
@property
def app_id(self):
"""Return the current app."""
return self._current_app
@property
def app_name(self):
"""Return the friendly name of the current app."""
return self._apps.get(self._current_app, self._current_app)
@property
def available(self):
"""Return whether or not the ADB connection is valid."""
return self._available
@property @property
def name(self): def name(self):
@ -210,30 +255,170 @@ class FireTVDevice(MediaPlayerDevice):
"""Device should be polled.""" """Device should be polled."""
return True return True
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_FIRETV
@property @property
def state(self): def state(self):
"""Return the state of the player.""" """Return the state of the player."""
return self._state return self._state
@property @adb_decorator()
def available(self): def media_play(self):
"""Return whether or not the ADB connection is valid.""" """Send play command."""
return self._available self.aftv.media_play()
@adb_decorator()
def media_pause(self):
"""Send pause command."""
self.aftv.media_pause()
@adb_decorator()
def media_play_pause(self):
"""Send play/pause command."""
self.aftv.media_play_pause()
@adb_decorator()
def turn_on(self):
"""Turn on the device."""
self.aftv.turn_on()
@adb_decorator()
def turn_off(self):
"""Turn off the device."""
self.aftv.turn_off()
@adb_decorator()
def media_previous_track(self):
"""Send previous track command (results in rewind)."""
self.aftv.media_previous()
@adb_decorator()
def media_next_track(self):
"""Send next track command (results in fast-forward)."""
self.aftv.media_next()
@adb_decorator()
def adb_command(self, cmd):
"""Send an ADB command to an Android TV / Fire TV device."""
key = self._keys.get(cmd)
if key:
return self.aftv.adb_shell('input keyevent {}'.format(key))
if cmd == 'GET_PROPERTIES':
return self.aftv.get_properties_dict()
return self.aftv.adb_shell(cmd)
class AndroidTVDevice(ADBDevice):
"""Representation of an Android TV device."""
def __init__(self, aftv, name, apps):
"""Initialize the Android TV device."""
super().__init__(aftv, name, apps)
self._device = None
self._muted = None
self._device_properties = self.aftv.device_properties
self._unique_id = 'androidtv-{}-{}'.format(
name, self._device_properties['serialno'])
self._volume = None
@adb_decorator(override_available=True)
def update(self):
"""Update the device state and, if necessary, re-connect."""
# Check if device is disconnected.
if not self._available:
# Try to connect
self._available = self.aftv.connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update.
if not self._available:
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._device, self._muted, self._volume = \
self.aftv.update()
self._state = ANDROIDTV_STATES[state]
@property @property
def app_id(self): def is_volume_muted(self):
"""Return the current app.""" """Boolean if volume is currently muted."""
return self._current_app return self._muted
@property @property
def app_name(self): def source(self):
"""Return the friendly name of the current app.""" """Return the current playback device."""
return self.apps.get(self._current_app, self._current_app) return self._device
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_ANDROIDTV
@property
def unique_id(self):
"""Return the device unique id."""
return self._unique_id
@property
def volume_level(self):
"""Return the volume level."""
return self._volume
@adb_decorator()
def media_stop(self):
"""Send stop command."""
self.aftv.media_stop()
@adb_decorator()
def mute_volume(self, mute):
"""Mute the volume."""
self.aftv.mute_volume()
@adb_decorator()
def volume_down(self):
"""Send volume down command."""
self.aftv.volume_down()
@adb_decorator()
def volume_up(self):
"""Send volume up command."""
self.aftv.volume_up()
class FireTVDevice(ADBDevice):
"""Representation of a Fire TV device."""
def __init__(self, aftv, name, apps, get_sources):
"""Initialize the Fire TV device."""
super().__init__(aftv, name, apps)
self._get_sources = get_sources
self._running_apps = None
@adb_decorator(override_available=True)
def update(self):
"""Update the device state and, if necessary, re-connect."""
# Check if device is disconnected.
if not self._available:
# Try to connect
self._available = self.aftv.connect(always_log_errors=False)
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update.
if not self._available:
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._running_apps = \
self.aftv.update(self._get_sources)
self._state = ANDROIDTV_STATES[state]
@property @property
def source(self): def source(self):
@ -245,76 +430,15 @@ class FireTVDevice(MediaPlayerDevice):
"""Return a list of running apps.""" """Return a list of running apps."""
return self._running_apps return self._running_apps
@adb_decorator(override_available=True) @property
def update(self): def supported_features(self):
"""Update the device state and, if necessary, re-connect.""" """Flag media player features that are supported."""
# Check if device is disconnected. return SUPPORT_FIRETV
if not self._available:
# Try to connect
self._available = self.firetv.connect()
# To be safe, wait until the next update to run ADB commands.
return
# If the ADB connection is not intact, don't update.
if not self._available:
return
# Get the `state`, `current_app`, and `running_apps`.
ftv_state, self._current_app, self._running_apps = \
self.firetv.update(self._get_sources)
self._state = FIRETV_STATES[ftv_state]
@adb_decorator()
def turn_on(self):
"""Turn on the device."""
self.firetv.turn_on()
@adb_decorator()
def turn_off(self):
"""Turn off the device."""
self.firetv.turn_off()
@adb_decorator()
def media_play(self):
"""Send play command."""
self.firetv.media_play()
@adb_decorator()
def media_pause(self):
"""Send pause command."""
self.firetv.media_pause()
@adb_decorator()
def media_play_pause(self):
"""Send play/pause command."""
self.firetv.media_play_pause()
@adb_decorator() @adb_decorator()
def media_stop(self): def media_stop(self):
"""Send stop (back) command.""" """Send stop (back) command."""
self.firetv.back() self.aftv.back()
@adb_decorator()
def volume_up(self):
"""Send volume up command."""
self.firetv.volume_up()
@adb_decorator()
def volume_down(self):
"""Send volume down command."""
self.firetv.volume_down()
@adb_decorator()
def media_previous_track(self):
"""Send previous track command (results in rewind)."""
self.firetv.media_previous()
@adb_decorator()
def media_next_track(self):
"""Send next track command (results in fast-forward)."""
self.firetv.media_next()
@adb_decorator() @adb_decorator()
def select_source(self, source): def select_source(self, source):
@ -325,14 +449,6 @@ class FireTVDevice(MediaPlayerDevice):
""" """
if isinstance(source, str): if isinstance(source, str):
if not source.startswith('!'): if not source.startswith('!'):
self.firetv.launch_app(source) self.aftv.launch_app(source)
else: else:
self.firetv.stop_app(source[1:].lstrip()) self.aftv.stop_app(source[1:].lstrip())
@adb_decorator()
def adb_command(self, cmd):
"""Send an ADB command to a Fire TV device."""
key = self.keys.get(cmd)
if key:
return self.firetv.adb_shell('input keyevent {}'.format(key))
return self.firetv.adb_shell(cmd)

View File

@ -0,0 +1,11 @@
# Describes the format for available Android TV and Fire TV services
adb_command:
description: Send an ADB command to an Android TV / Fire TV device.
fields:
entity_id:
description: Name(s) of Android TV / Fire TV entities.
example: 'media_player.android_tv_living_room'
command:
description: Either a key command or an ADB shell command.
example: 'HOME'

View File

@ -1,6 +0,0 @@
"""
Support for functionality to interact with FireTV devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.firetv/
"""

View File

@ -1,11 +0,0 @@
# Describes the format for available Fire TV services
adb_command:
description: Send an ADB command to a Fire TV device.
fields:
entity_id:
description: Name(s) of Fire TV entities.
example: 'media_player.fire_tv_living_room'
command:
description: Either a key command or an ADB shell command.
example: 'HOME'

View File

@ -157,6 +157,9 @@ alpha_vantage==2.1.0
# homeassistant.components.amcrest # homeassistant.components.amcrest
amcrest==1.2.5 amcrest==1.2.5
# homeassistant.components.androidtv.media_player
androidtv==0.0.10
# homeassistant.components.switch.anel_pwrctrl # homeassistant.components.switch.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2 anel_pwrctrl-homeassistant==0.0.1.dev2
@ -435,9 +438,6 @@ fiblary3==0.1.7
# homeassistant.components.sensor.fints # homeassistant.components.sensor.fints
fints==1.0.1 fints==1.0.1
# homeassistant.components.firetv.media_player
firetv==1.0.9
# homeassistant.components.sensor.fitbit # homeassistant.components.sensor.fitbit
fitbit==0.3.0 fitbit==0.3.0