def setUp(self): self.mock_api = True if self.mock_api: self.creds = NokiaCredentials() else: config = configparser.ConfigParser() config.read('nokia.conf') self.creds = NokiaCredentials( consumer_key=config.get('nokia', 'consumer_key'), consumer_secret=config.get('nokia', 'consumer_secret'), access_token=config.get('nokia', 'access_token'), access_token_secret=config.get('nokia', 'access_token_secret'), user_id=config.get('nokia', 'user_id')) self.api = NokiaApi(self.creds)
def test_attributes(self): """ Make sure the NokiaCredentials objects have the right attributes """ creds = NokiaCredentials(access_token=1, token_expiry=2, token_type=3, refresh_token=4, user_id=5, client_id=6, consumer_secret=7) assert hasattr(creds, 'access_token') self.assertEqual(creds.access_token, 1) assert hasattr(creds, 'token_expiry') self.assertEqual(creds.token_expiry, 2) assert hasattr(creds, 'token_type') self.assertEqual(creds.token_type, 3) assert hasattr(creds, 'refresh_token') self.assertEqual(creds.refresh_token, 4) assert hasattr(creds, 'user_id') self.assertEqual(creds.user_id, 5) assert hasattr(creds, 'client_id') self.assertEqual(creds.client_id, 6) assert hasattr(creds, 'consumer_secret') self.assertEqual(creds.consumer_secret, 7)
def test_attributes(self): """ Make sure the NokiaApi objects have the right attributes """ assert hasattr(NokiaApi, 'URL') creds = NokiaCredentials(user_id='FAKEID') api = NokiaApi(creds) assert hasattr(api, 'credentials') assert hasattr(api, 'oauth') assert hasattr(api, 'client')
def test_attributes(self): """ Make sure the NokiaApi objects have the right attributes """ assert hasattr(NokiaApi, 'URL') creds = NokiaCredentials(user_id='FAKEID', token_expiry='123412341234') api = NokiaApi(creds) assert hasattr(api, 'credentials') assert hasattr(api, 'token') assert hasattr(api, 'client') assert hasattr(api, 'refresh_cb')
def test_attribute_defaults(self): """ Make sure NokiaApi object attributes have the correct defaults """ self.assertEqual(NokiaApi.URL, 'https://api.health.nokia.com') creds = NokiaCredentials(user_id='FAKEID') api = NokiaApi(creds) self.assertEqual(api.credentials, creds) self.assertEqual(api.client.auth, api.oauth) self.assertEqual(api.client.params, {'userid': creds.user_id})
def test_attribute_defaults(self): """ Make sure NokiaCredentials attributes have the proper defaults """ creds = NokiaCredentials() self.assertEqual(creds.access_token, None) self.assertEqual(creds.access_token_secret, None) self.assertEqual(creds.consumer_key, None) self.assertEqual(creds.consumer_secret, None) self.assertEqual(creds.user_id, None)
def test_attribute_defaults(self): """ Make sure NokiaApi object attributes have the correct defaults """ self.assertEqual(NokiaApi.URL, 'https://api.health.nokia.com') creds = NokiaCredentials(user_id='FAKEID', token_expiry='123412341234') api = NokiaApi(creds) self.assertEqual(api.credentials, creds) self.assertEqual(api.client.params, {}) self.assertEqual(api.client.token, api.token) self.assertEqual(api.refresh_cb, None)
def __init__(self, config): """Initialize the data object.""" from nokia import NokiaApi, NokiaCredentials credentials = NokiaCredentials(config.get("access_token"), config.get("access_token_secret"), config.get("consumer_key"), config.get("consumer_secret"), config.get("user_id")) self._api = NokiaApi(credentials) self.data = None self._update()
def nokia_creds(token_dict): ''' :param token_dict: :return: Nokia Credentials Object ''' return NokiaCredentials(client_id=client_id, consumer_secret=client_secret, access_token=token_dict['access_token'], token_expiry=token_dict['token_expiry'], token_type=token_dict['token_type'], user_id=token_dict['user_id'], refresh_token=token_dict['refresh_token'])
def setUp(self): self.mock_api = True creds_attrs = [ 'access_token', 'token_expiry', 'token_type', 'refresh_token', 'user_id', 'client_id', 'consumer_secret', ] if self.mock_api: creds_args = {a: 'fake' + a for a in creds_attrs} creds_args.update({ 'token_expiry': '123412341234', 'token_type': 'Bearer', }) self.creds = NokiaCredentials(**creds_args) else: config = configparser.ConfigParser() config.read('nokia.conf') creds_args = {a: config.get('nokia', a) for a in creds_attrs} self.creds = NokiaCredentials(**creds_args) self.api = NokiaApi(self.creds)
def __init__(self, smarthome, consumer_key, consumer_secret, access_token, access_token_secret, user_id, cycle=300): self.logger = logging.getLogger(__name__) self._sh = smarthome self._consumer_key = consumer_key self._consumer_secret = consumer_secret self._access_token = access_token self._access_token_secret = access_token_secret self._auth = NokiaAuth(self._consumer_key, self._consumer_secret) self._user_id = user_id self._creds = NokiaCredentials(self._access_token, self._access_token_secret, self._consumer_key, self._consumer_secret, self._user_id) self._client = NokiaApi(self._creds) self._cycle = cycle self._items = {} if not self.init_webinterface(): self._init_complete = False
def refresh_weight(cfg_file, engine, db_df): print("REFRESHING WEIGHT...") parser = configparser.ConfigParser() parser.read(cfg_file) client_id = parser.get('nokia', 'client_id') client_secret = parser.get('nokia', 'client_secret') access_token = parser.get('nokia', 'access_token') token_expiry = parser.get('nokia', 'token_expiry') token_type = parser.get('nokia', 'token_type') refresh_token = parser.get('nokia', 'refresh_token') user_id = parser.get('nokia', 'user_id') creds = NokiaCredentials(access_token=access_token, token_expiry=token_expiry, token_type=token_type, refresh_token=refresh_token, user_id=user_id, client_id=client_id, consumer_secret=client_secret) client = NokiaApi( creds, refresh_cb=(lambda x: persist_nokia_refresh_token(x, cfg_file))) [date_start, date_end] = get_target_date_endpoints('weight', db_df) date_query = date_start date_diff = date_end - date_query days = date_diff.days + 2 measures = client.get_measures(meastype=1, limit=days) weight_json = [{ 'weight': (float("{:.1f}".format(x.weight * 2.20462))), 'date': x.date.strftime('%Y-%m-%d') } for x in measures] date_values = [[pd.to_datetime(x['date']), x['weight']] for x in weight_json] date_values_imp = [[pd.to_datetime(x['date']), np.nan] for x in weight_json] updated_df = insert_values(date_values, 'weight', db_df) updated_df = insert_values(date_values_imp, 'weight_imputed', updated_df) with engine.connect() as conn, conn.begin(): updated_df.to_sql('fitness', conn, if_exists='replace') return updated_df
def test_set_token_refresh_cb(self): """ Make sure set_token calls refresh_cb when specified """ timestamp = int((datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)).total_seconds()) creds = NokiaCredentials(token_expiry=timestamp) refresh_cb = MagicMock() api = NokiaApi(creds, refresh_cb=refresh_cb) token = { 'access_token': 'fakeat', 'refresh_token': 'fakert', 'expires_in': 100, } api.set_token(token) self.assertEqual(api.token, token) refresh_cb.assert_called_once_with(token)
def test_attributes(self): """ Make sure the NokiaCredentials objects have the right attributes """ creds = NokiaCredentials(access_token=1, access_token_secret=1, consumer_key=1, consumer_secret=1, user_id=1) assert hasattr(creds, 'access_token') self.assertEqual(creds.access_token, 1) assert hasattr(creds, 'access_token_secret') self.assertEqual(creds.access_token_secret, 1) assert hasattr(creds, 'consumer_key') self.assertEqual(creds.consumer_key, 1) assert hasattr(creds, 'consumer_secret') self.assertEqual(creds.consumer_secret, 1) assert hasattr(creds, 'user_id') self.assertEqual(creds.user_id, 1)
def main(): try: # Let's try to load up the config file... with open(CFN) as cf: c = json.load(cf) creds = NokiaCredentials( access_token=c['access_token'], token_expiry=c['token_expiry'], token_type=c['token_type'], refresh_token=c['refresh_token'], user_id=c['user_id'], client_id=c['client_id'], consumer_secret=c['consumer_secret'], ) except: # Can't get credentials, let's go through the process... creds = nokia_auth() # And now we're ready to get our weights... get_weight(creds)
def test_set_token(self): """ Make sure NokiaApi.set_token makes the expected changes """ timestamp = int((datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)).total_seconds()) creds = NokiaCredentials(token_expiry=timestamp) api = NokiaApi(creds) token = { 'access_token': 'fakeat', 'refresh_token': 'fakert', 'expires_in': 100, } api.set_token(token) self.assertEqual(api.token, token) self.assertEqual(api.get_credentials().access_token, 'fakeat') self.assertEqual(api.get_credentials().refresh_token, 'fakert') # Need to check 100 or 101 in case a second ticked over during testing self.assertTrue( int(api.credentials.token_expiry) == (timestamp + 100) or int(api.credentials.token_expiry) == (timestamp + 101))
def nokia_sync(force=False): config = get_config() n = config['nokia'] creds = NokiaCredentials(n['access_token'], n['token_expiry'], n['token_type'], n['refresh_token'], n['user_id'], n['client_id'], n['consumer_secret']) nokia_client = NokiaApi(creds, refresh_cb=nokia_refresh_cb) measures = nokia_client.get_measures() measure = measures[0] logger.info('Recieved {} measurements'.format(len(measures))) # Now check if we need to update last_update = max([m.date.timestamp for m in measures]) logger.info('Last measurement at {}'.format(last_update)) logger.info('Last update at {}'.format(config['nokia']['last_update'])) if (config['nokia']['last_update'] >= last_update) and not force: logger.info('No new weight updates') return measures msg = '' fit = FitEncoder_Weight() fit.write_file_info() fit.write_file_creator() fit.write_device_info(datetime.timestamp(datetime.now())) for measure in measures: if (config['nokia']['last_update'] < measure.date.timestamp) or force: if measure.weight is not None: bmi = measure.weight / config['nokia']['height']**2 msg += 'New measurement at {} ({})\n'.format( str(measure.date.datetime), measure.date.humanize()) msg += 'New weight = {} kg\n'.format(measure.weight) msg += 'New fat ratio= {} %\n'.format(measure.fat_ratio) msg += 'New hydration = {} %\n'.format(measure.hydration) msg += 'New bone mass = {} kg\n'.format(measure.bone_mass) msg += 'New muscle mass = {} kg\n'.format(measure.muscle_mass) msg += 'Calculated BMI = {} kg.m^-2\n'.format(bmi) for m in msg.splitlines(): logger.info(m) # Sync Garmin logger.info('Syncing weight of {} with GARMIN.'.format( measure.weight)) fit.write_weight_scale(timestamp=measure.date.timestamp, weight=measure.weight, percent_fat=measure.fat_ratio, percent_hydration=measure.hydration, bone_mass=measure.bone_mass, muscle_mass=measure.muscle_mass, bmi=bmi) fit.finish() with GarminClient(config['garmin']['username'], config['garmin']['password']) as client: client.upload_activity(io.BytesIO(fit.getvalue()), 'fit') # Sync Strava measure = measures[0] ts = datetime.timestamp(datetime.now()) ts -= (config['nokia']['weight_int'] * 86400) weight = [m.weight for m in measures if m.date.timestamp >= ts] logger.info("Averaging {} weight measurements".format(len(weight))) weight = mean(weight) if (config['nokia']['last_update'] != measure.date.timestamp) or force: logger.info('Syncing weight of {} with STRAVA.'.format(measure.weight)) strava = Strava(config['strava']) strava_token = strava.connect() config['strava'] = strava_token strava.client.update_athlete(weight=weight) msg += 'Synced weight of {} with Strava\n'.format(measure.weight) config = get_config() config['nokia']['last_update'] = max([m.date.timestamp for m in measures]) write_config(config) send_email('New Weight Sync', msg) return measures
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Authenticate to the Withings API.""" from nokia import NokiaApi, NokiaAuth, NokiaCredentials hass.http.register_view(WithingsAuthCallbackView()) client_id = config.get(CONF_CLIENT_ID) consumer_secret = config.get(CONF_CONSUMER_SECRET) config_path = hass.config.path(WITHINGS_CONFIG_PATH) @asyncio.coroutine def _read_config(): if not os.path.isfile(config_path): return None with open(config_path, 'r') as auth_file: config = json.load(auth_file) if config.get('client_id') == client_id: return config @asyncio.coroutine def _write_config(creds): with open(config_path, 'w') as auth_file: json.dump( { 'client_id': client_id, 'access_token': creds.access_token, 'refresh_token': creds.refresh_token, 'token_type': creds.token_type, 'token_expiry': creds.token_expiry, 'user_id': creds.user_id, }, auth_file) @asyncio.coroutine def _add_device(creds): client = NokiaApi(creds) withings = WithingsSensor(hass, client) yield from withings.async_update() return async_add_devices([withings]) config = yield from _read_config() if config is not None: creds = NokiaCredentials(client_id=client_id, consumer_secret=consumer_secret, access_token=config['access_token'], token_expiry=config['token_expiry'], token_type=config['token_type'], refresh_token=['refresh_token'], user_id=config['user_id']) yield from _add_device(creds) else: callback_uri = '{}{}'.format(hass.config.api.base_url, WithingsAuthCallbackView.url) auth = NokiaAuth(client_id, consumer_secret, callback_uri=callback_uri, scope='user.info,user.metrics,user.activity') authorize_url = auth.get_authorize_url() configurator = hass.components.configurator request_id = configurator.async_request_config( "Withings", description="Authorization required for Withings account.", link_name="Authorize Home Assistant", link_url=authorize_url, entity_picture='/local/images/logo_nokia_health_mate.png') @asyncio.coroutine def initialize_callback(code): """Handle OAuth callback from Withings authorization flow.""" creds = auth.get_credentials(code) yield from _write_config(creds) yield from _add_device(creds) configurator.async_request_done(request_id) hass.data[DATA_CALLBACK] = initialize_callback return True
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Authenticate to the Nokia Health API.""" from nokia import NokiaApi, NokiaAuth, NokiaCredentials hass.http.register_view(NokiaAuthCallbackView()) consumer_key = config.get(CONF_CONSUMER_KEY) consumer_secret = config.get(CONF_CONSUMER_SECRET) config_path = hass.config.path(NOKIA_CONFIG_PATH) @asyncio.coroutine def _read_config(): if not os.path.isfile(config_path): return None with open(config_path, 'r') as auth_file: config = json.load(auth_file) if config['consumer_key'] == consumer_key: return config @asyncio.coroutine def _write_config(creds): with open(config_path, 'w') as auth_file: json.dump( { 'consumer_key': consumer_key, 'access_token': creds.access_token, 'access_token_secret': creds.access_token_secret, 'user_id': creds.user_id, }, auth_file) @asyncio.coroutine def _add_device(creds): client = NokiaApi(creds) nokia = NokiaSensor(hass, client) yield from nokia.async_update() return async_add_devices([nokia]) config = yield from _read_config() if config is not None: creds = NokiaCredentials(config['access_token'], config['access_token_secret'], consumer_key, consumer_secret, config['user_id']) yield from _add_device(creds) else: auth = NokiaAuth(consumer_key, consumer_secret) callback_uri = '{}{}'.format(hass.config.api.base_url, NokiaAuthCallbackView.url) authorize_url = auth.get_authorize_url(callback_uri=callback_uri) configurator = hass.components.configurator request_id = configurator.async_request_config( "Nokia Health", description="Authorization required for Nokia Health account.", link_name="Authorize Home Assistant", link_url=authorize_url, entity_picture='/local/images/logo_nokia_health_mate.png') @asyncio.coroutine def initialize_callback(oauth_verifier): """Handle OAuth callback from Nokia authorization flow.""" creds = auth.get_credentials(oauth_verifier) yield from _write_config(creds) yield from _add_device(creds) configurator.async_request_done(request_id) hass.data[DATA_CALLBACK] = initialize_callback return True
def test_get_credentials(self): """ Make sure NokiaApi returns the credentials as expected """ creds = NokiaCredentials(token_expiry=0) api = NokiaApi(creds)
def _update(self): """ Updates information on diverse items Mappings: ('weight', 1), ('height', 4), ('fat_free_mass', 5), ('fat_ratio', 6), ('fat_mass_weight', 8), ('diastolic_blood_pressure', 9), ('systolic_blood_pressure', 10), ('heart_pulse', 11), ('temperature', 12), ('spo2', 54), ('body_temperature', 71), ('skin_temperature', 72), ('muscle_mass', 76), ('hydration', 77), ('bone_mass', 88), ('pulse_wave_velocity', 91) """ if 'access_token' not in self.get_items( ) or 'token_expiry' not in self.get_items( ) or 'token_type' not in self.get_items( ) or 'refresh_token' not in self.get_items(): self.logger.error( "Plugin '{}': Mandatory Items for OAuth2 Data do not exist. Verify that you have items with withings_type: token_expiry, token_type, refresh_token and access_token in your item tree." .format(self.get_fullname())) return if self._client is None: if self.get_item('access_token')( ) and self.get_item('token_expiry')() > 0 and self.get_item( 'token_type')() and self.get_item('refresh_token')(): if (self.shtime.now() < datetime.datetime.fromtimestamp( self.get_item('token_expiry')(), tz=self.shtime.tzinfo())): self.logger.debug( "Plugin '{}': Token is valid, will expire on {}.". format( self.get_fullname(), datetime.datetime.fromtimestamp( self.get_item('token_expiry')(), tz=self.shtime.tzinfo()).strftime( '%d.%m.%Y %H:%M:%S'))) self.logger.debug( "Plugin '{}': Initializing NokiaCredentials: access_token - {} token_expiry - {} token_type - {} refresh_token - {} user_id - {} client_id - {} consumer_secret - {}" .format(self.get_fullname(), self.get_item('access_token')(), self.get_item('token_expiry')(), self.get_item('token_type')(), self.get_item('refresh_token')(), self._user_id, self._client_id, self._consumer_secret)) self._creds = NokiaCredentials( self.get_item('access_token')(), self.get_item('token_expiry')(), self.get_item('token_type')(), self.get_item('refresh_token')(), self._user_id, self._client_id, self._consumer_secret) self._client = NokiaApi(self._creds, refresh_cb=self._store_tokens) else: self.logger.error( "Plugin '{}': Token is expired, run OAuth2 again from Web Interface (Expiry Date: {})." .format( self.get_fullname(), datetime.datetime.fromtimestamp( self.get_item('token_expiry')(), tz=self.shtime.tzinfo()).strftime( '%d.%m.%Y %H:%M:%S'))) return else: self.logger.error( "Plugin '{}': Items for OAuth2 Data are not set with required values. Please run process via WebGUI of the plugin." .format(self.get_fullname())) return measures = self._client.get_measures() last_measure = measures[0] if last_measure.get_measure( 11) is not None and 'heart_pulse' in self._items: self._items['heart_pulse'](last_measure.get_measure(11)) self.logger.debug("Plugin '{}': heart_pulse - {}".format( self.get_fullname(), last_measure.get_measure(11))) # Bugfix for strange behavior of returning heart_pulse as seperate dataset.. if last_measure.get_measure(1) is None: last_measure = measures[1] if last_measure.get_measure(1) is not None and 'weight' in self._items: self._items['weight'](last_measure.get_measure(1)) self.logger.debug("Plugin '{}': weight - {}".format( self.get_fullname(), last_measure.get_measure(1))) if last_measure.get_measure(4) is not None and 'height' in self._items: self._items['height'](last_measure.get_measure(4)) self.logger.debug("Plugin '{}': height - {}".format( self.get_fullname(), last_measure.get_measure(4))) if last_measure.get_measure( 5) is not None and 'fat_free_mass' in self._items: self._items['fat_free_mass'](last_measure.get_measure(5)) self.logger.debug("Plugin '{}': fat_free_mass - {}".format( self.get_fullname(), last_measure.get_measure(5))) if last_measure.get_measure( 6) is not None and 'fat_ratio' in self._items: self._items['fat_ratio'](last_measure.get_measure(6)) self.logger.debug("Plugin '{}': fat_ratio - {}".format( self.get_fullname(), last_measure.get_measure(6))) if last_measure.get_measure( 8) is not None and 'fat_mass_weight' in self._items: self._items['fat_mass_weight'](last_measure.get_measure(8)) self.logger.debug("Plugin '{}': fat_mass_weight - {}".format( self.get_fullname(), last_measure.get_measure(8))) if last_measure.get_measure( 9) is not None and 'diastolic_blood_pressure' in self._items: self._items['diastolic_blood_pressure']( last_measure.get_measure(9)) self.logger.debug( "Plugin '{}': diastolic_blood_pressure - {}".format( self.get_fullname(), last_measure.get_measure(9))) if last_measure.get_measure( 10) is not None and 'systolic_blood_pressure' in self._items: self._items['systolic_blood_pressure']( last_measure.get_measure(10)) self.logger.debug( "Plugin '{}': systolic_blood_pressure - {}".format( self.get_fullname(), last_measure.get_measure(10))) if last_measure.get_measure( 11) is not None and 'heart_pulse' in self._items: self._items['heart_pulse'](last_measure.get_measure(11)) self.logger.debug("Plugin '{}': heart_pulse - {}".format( self.get_fullname(), last_measure.get_measure(11))) if last_measure.get_measure( 12) is not None and 'temperature' in self._items: self._items['temperature'](last_measure.get_measure(12)) self.logger.debug("Plugin '{}': temperature - {}".format( self.get_fullname(), last_measure.get_measure(12))) if last_measure.get_measure(54) is not None and 'spo2' in self._items: self._items['spo2'](last_measure.get_measure(54)) self.logger.debug("Plugin '{}': spo2 - {}".format( self.get_fullname(), last_measure.get_measure(54))) if last_measure.get_measure( 71) is not None and 'body_temperature' in self._items: self._items['body_temperature'](last_measure.get_measure(71)) self.logger.debug("Plugin '{}': body_temperature - {}".format( self.get_fullname(), last_measure.get_measure(71))) if last_measure.get_measure( 72) is not None and 'skin_temperature' in self._items: self._items['skin_temperature'](last_measure.get_measure(72)) self.logger.debug("Plugin '{}': skin_temperature - {}".format( self.get_fullname(), last_measure.get_measure(72))) if last_measure.get_measure( 76) is not None and 'muscle_mass' in self._items: self._items['muscle_mass'](last_measure.get_measure(76)) self.logger.debug("Plugin '{}': muscle_mass - {}".format( self.get_fullname(), last_measure.get_measure(76))) if last_measure.get_measure( 77) is not None and 'hydration' in self._items: self._items['hydration'](last_measure.get_measure(77)) self.logger.debug("Plugin '{}': hydration - {}".format( self.get_fullname(), last_measure.get_measure(77))) if last_measure.get_measure( 88) is not None and 'bone_mass' in self._items: self._items['bone_mass'](last_measure.get_measure(88)) self.logger.debug("Plugin '{}': bone_mass - {}".format( self.get_fullname(), last_measure.get_measure(88))) if last_measure.get_measure( 91) is not None and 'pulse_wave_velocity' in self._items: self._items['pulse_wave_velocity'](last_measure.get_measure(91)) self.logger.debug("Plugin '{}': pulse_wave_velocity - {}".format( self.get_fullname(), last_measure.get_measure(91))) if 'height' in self._items and ( 'bmi' in self._items or 'bmi_text' in self._items) and last_measure.get_measure(1) is not None: if self._items['height']() > 0: bmi = round( last_measure.get_measure(1) / ((self._items['height']()) * (self._items['height']())), 2) if 'bmi' in self._items: self._items['bmi'](bmi) if 'bmi_text' in self._items: if bmi < 16: self._items['bmi_text']('starkes Untergewicht') elif 16 <= bmi < 17: self._items['bmi_text']('mäßiges Untergewicht ') elif 17 <= bmi < 18.5: self._items['bmi_text']('leichtes Untergewicht ') elif 18.5 <= bmi < 25: self._items['bmi_text']('Normalgewicht') elif 25 <= bmi < 30: self._items['bmi_text']('Präadipositas (Übergewicht)') elif 30 <= bmi < 35: self._items['bmi_text']('Adipositas Grad I') elif 35 <= bmi < 40: self._items['bmi_text']('Adipositas Grad II') elif 40 <= bmi: self._items['bmi_text']('Adipositas Grad III') else: self.logger.error( "Plugin '{}': Cannot calculate BMI: height is 0, please set height (in m) for height item manually." .format(self.get_fullname())) else: self.logger.error( "Plugin '{}': Cannot calculate BMI: height and / or bmi item missing." .format(self.get_fullname()))