├── .gitignore ├── docs ├── temperature-unit.md ├── mug-name.md ├── mug-color.md ├── current-temp.md ├── target-temp.md ├── liquid-level.md ├── liquid-state.md ├── ota.md ├── time-date-zone.md ├── push-events.md └── battery.md ├── LICENSE ├── README.md └── data-collection.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tooling -------------------------------------------------------------------------------- /docs/temperature-unit.md: -------------------------------------------------------------------------------- 1 | **Temperature Unit** 2 | --- 3 | 4 | Retrieves the current unit of temperature used by the mug. 5 | 6 | * **UUID** 7 | 8 | `fc540004-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `Read` 13 | `Write` 14 | 15 | * **Data Format** 16 | 17 | Size: 1 Byte 18 | Type: Uint8 19 | 20 | `0` for Celcius, `1` for Fahrenheit 21 | -------------------------------------------------------------------------------- /docs/mug-name.md: -------------------------------------------------------------------------------- 1 | **Mug Name** 2 | --- 3 | 4 | The name of the mug. 5 | 6 | * **UUID** 7 | 8 | `fc540001-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | `WRITE` 14 | 15 | * **Data Format** 16 | 17 | Size: Up to 14 Bytes 18 | 19 | Type: String (ASCII Encoding w/o spaces) 20 | 21 | **Example Data** 22 | 23 | ``` 24 | 45 6D 62 65 72 4D 75 68 25 | E m b e r M u g 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/mug-color.md: -------------------------------------------------------------------------------- 1 | **Mug color** 2 | --- 3 | 4 | Retrieves and sets the color of the mug's LED indicator. 5 | 6 | * **UUID** 7 | 8 | `fc540014-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | `WRITE` 14 | 15 | * **Data Format** 16 | 17 | Size: 4 Bytes 18 | Type: RGBA color code 19 | 20 | Byte | Data 21 | --- | --- 22 | 0 | Red 23 | 1 | Blue 24 | 2 | Green 25 | 3 | Alpha 26 | 27 | * **Example Data** 28 | 29 | For setting the mug color to lime-green: `BFFF00FF` -------------------------------------------------------------------------------- /docs/current-temp.md: -------------------------------------------------------------------------------- 1 | **Current temperature** 2 | --- 3 | 4 | Retrieves the current temperature of the mug 5 | 6 | * **UUID** 7 | 8 | `fc540002-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | 14 | * **Data Format** 15 | 16 | Size: 2 Bytes 17 | Type: UINT16 Little Endian 18 | 19 | For calculating the real temperature in °C, multiply the value by 0.01. 20 | 21 | * **Example Data** 22 | 23 | When receiving `5550`, multiply by 0.01 to read the real temperature: 55.00 °C 24 | -------------------------------------------------------------------------------- /docs/target-temp.md: -------------------------------------------------------------------------------- 1 | **Target temperature** 2 | --- 3 | 4 | Retrieves and sets the target temperature of the mug 5 | 6 | * **UUID** 7 | 8 | `fc540003-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | `WRITE` 14 | 15 | * **Data Format** 16 | 17 | Size: 2 Bytes 18 | Type: UINT16 Little Endian 19 | 20 | For calculating the real temperature in °C, multiply the value by 0.01. 21 | 22 | Sending `0000` turns off the heater 23 | 24 | * **Example Data** 25 | 26 | For setting the mug temperature to 55.50 °C, send the UINT `5550`. -------------------------------------------------------------------------------- /docs/liquid-level.md: -------------------------------------------------------------------------------- 1 | **Liquid Level** 2 | --- 3 | 4 | Retrieves the level of liquid present in the cup 5 | 6 | * **UUID** 7 | 8 | `fc540005-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | 14 | * **Data Format** 15 | 16 | Size: 1 Byte 17 | 18 | * **Observation** 19 | 20 | This value only seems to update when the device is charging. 21 | 22 | There may be values other intermediate values present but they have no significance for the app. Most likely because the liquid level sensor is too coarse to tell the difference. 23 | 24 | * 0 -> Empty 25 | * 30 -> Not Empty 26 | -------------------------------------------------------------------------------- /docs/liquid-state.md: -------------------------------------------------------------------------------- 1 | **Liquid State** 2 | --- 3 | 4 | The current state of the mug 5 | 6 | * **UUID** 7 | 8 | `fc540008-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | 14 | * **Data Format** 15 | 16 | Size: 1 Byte 17 | 18 | Observations: The device emits state `3` but it is not registered by the app, though recorded in its analytics. 19 | 20 | This data is read after state change events to facilitate data collection through the app. 21 | 22 | 23 | * **Example Data** 24 | 25 | * 1 -> Empty 26 | * 2 -> Filling 27 | * 3 -> (Unknown) 28 | * 4 -> Cooling 29 | * 5 -> Heating 30 | * 6 -> Stable Temperature 31 | 32 | -------------------------------------------------------------------------------- /docs/ota.md: -------------------------------------------------------------------------------- 1 | **Over The Air** 2 | --- 3 | 4 | Info about the current firmware running on the mug. 5 | 6 | * **UUID** 7 | 8 | `fc54000C-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | 14 | * **Data Format** 15 | 16 | Size: 4-6 bytes 17 | 18 | Byte | Data 19 | --- | --- 20 | 0-1 | Firmware version UINT16 Little Endian 21 | 2-3 | Hardware version UINT16 Little Endian 22 | 4-5? | Bootloader version UINT16 Little Endian. Optional 23 | 24 | * **Example Data** 25 | 26 | `8A 01 0A 00` 27 | 28 | `8A 01` -> firmware version `0x18A` (394) 29 | 30 | `A` -> hardware version 10 31 | 32 | Missing bootloader version defaults to 0 -------------------------------------------------------------------------------- /docs/time-date-zone.md: -------------------------------------------------------------------------------- 1 | **Current date and timezone** 2 | --- 3 | 4 | A write-only sink for the mug to store the current date and timezone 5 | 6 | * **UUID** 7 | 8 | `fc540006-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `WRITE` 13 | 14 | * **Data Format** 15 | 16 | Size: 5 Bytes 17 | 18 | Byte | Value 19 | --- | --- 20 | 0-3 | Unix timestamp recorded by the app. UINT32 Little Endian 21 | 4 | Timezone offset (ex: GMT+03) 22 | 23 | * **Example Data** 24 | 25 | ``` 26 | Date Time TZ 27 | ----------- -- 28 | B9 F0 98 63 03 29 | ``` 30 | 31 | 32 | ```js 33 | // interpreting the data in Javascript 34 | console.log(new Date(0x6398F0B9 * 1000)) 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /docs/push-events.md: -------------------------------------------------------------------------------- 1 | **Push Events** 2 | --- 3 | 4 | Events sent by the mug for the application to register to. 5 | 6 | * **UUID** 7 | 8 | `fc540012-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | `NOTIFICATION` 14 | 15 | * **Data Format** 16 | 17 | Size: 1 Byte 18 | 19 | These are notifications from the mug used to send events and also prompt the app to refresh certain stats by re-reading characteristics and/or sending metrics information. 20 | 21 | States: 22 | 23 | * 0x01 -> Refresh [battery level](./battery.md) 24 | * 0x02 -> Charging 25 | * 0x03 -> Not Charging 26 | * 0x04 -> Refresh [target temperature](./target-temp.md) 27 | * 0x05 -> Refresh [drink temperature](./current-temp.md) 28 | * 0x06 -> _Not implemented_ 29 | * 0x07 -> Refresh [liquid level](./liquid-level.md) 30 | * 0x08 -> Refresh [liquid state](./liquid-state.md) 31 | 32 | -------------------------------------------------------------------------------- /docs/battery.md: -------------------------------------------------------------------------------- 1 | **Battery percentage and other values** 2 | --- 3 | 4 | Retrieves the battery percentage of the mug and other values. 5 | 6 | * **UUID** 7 | 8 | `fc540007-236c-4c94-8fa9-944a3e5353fa` 9 | 10 | * **Methods** 11 | 12 | `READ` 13 | 14 | * **Data Format** 15 | 16 | Size: 5 Bytes 17 | 18 | Byte | Value 19 | --- | --- 20 | 0 | Battery percentage (5 - 100. Not scaled to 0 - 255) 21 | 1 | Charging status. 1 for plugged in, 0 for unplugged 22 | 2-3 | Battery temperature as UINT16 Little Endian, encoded like the [other temperatures](./target-temp.md) 23 | 4 | (Legacy) Most likely battery voltage 24 | 25 | * **Example Data** 26 | 27 | When receiving `4f 00 1c 0c 00`: 28 | 29 | `4f` -> 79% 30 | 31 | `00` -> Not charging 32 | 33 | `1c 0c` -> 3100 (which maybe is 31.00 °C, battery temperature). Discarded by app 34 | 35 | `00` -> Battery Voltage. Not set by device and discarded by app 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Paul Orlob 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Mug Bluetooth Documentation 2 | 3 | ## Introduction 4 | 5 | This repository contains a reverse-engineered documentation for the bluetooth API of [Ember Mugs](https://ember.com/). 6 | 7 | The information provided here was retrieved using the Ember smartphone app on Android using an **Ember Mug 2** and decompiling the APK through [Java decompilers](http://www.javadecompilers.com/apk). 8 | 9 | It may not be applicable to other Ember mugs, but feel free to extend the documentation and open a pull request :). 10 | 11 | ## Privacy 12 | 13 | Data collected by the bluetooth gets sent to: [`https://collector.embertech.com`](https://collector.embertech.com) 14 | 15 | Read [Data Collection & Privacy](./data-collection.md) for more information. 16 | 17 | ## Documentation 18 | 19 | All commands have a service UUID of `fc543622236c4c948fa9944a3e5353fa` 20 | 21 | * [Mug color](./docs/mug-color.md) 22 | * [Target temperature](./docs/target-temp.md) 23 | * [Current temperature](./docs/current-temp.md) 24 | * [Battery percentage](./docs/battery.md) 25 | * [Temperature unit](./docs/temperature-unit.md) 26 | * [Liquid level](./docs/liquid-level.md) 27 | * [Liquid state](./docs/liquid-state.md) 28 | * [Mug name](./docs/mug-name.md) 29 | * [Date & timezone](./docs/time-date-zone.md) 30 | * [Push events](./docs/push-events.md) 31 | * [Firmware & hardware versions](./docs/push-events.md) 32 | 33 | 34 | -------------------------------------------------------------------------------- /data-collection.md: -------------------------------------------------------------------------------- 1 | # Data Collection & Privacy 2 | 3 | The ember mug continuously collects data from your mug and your surroundings and sends it to https://collector.embertech.com for analytics regularly. 4 | 5 | The collected data includes but is not limited to: 6 | - Fine grained geolocation (your exact longitude and latitude) 7 | - The temperature of your mug 8 | - Whether you have liquid in your mug 9 | - Your IP (as an inevitable consequence of data collection) 10 | - How you logged into the app 11 | - Your mug name 12 | - Your preferred temperature unit 13 | - Your mug presets 14 | - The current date including timezone 15 | - Your email 16 | - Your mobile OS and version info 17 | - Basically anything you can imagine collecting, ember app collects 18 | 19 | These analytics events happen: 20 | - Once every day 21 | - When you fill your mug 22 | - When you empty your mug 23 | - When your mug starts charging 24 | - When your mug leaves the charging plate 25 | 26 | 27 | ## Interesting decompiled snippets 28 | 29 | The app at some point wanted to respect GDPR and opt out of data collection for european citizens. 30 | 31 | ```java 32 | @Subscribe 33 | public void onLiquidStateChanged(OnLiquidStateChanged onLiquidStateChanged) { 34 | if (!EmberApp.isEuropeUnionUser()) { 35 | collectAppData(...); 36 | sendNewDrinkEventToFirebase(); 37 | pushDailyAppData(...); 38 | } 39 | } 40 | ``` 41 | 42 | But probably figured it either has no users in Europe or a lot of money to spend on lawsuits. 43 | 44 | ```java 45 | private void checkEuropeUnion() { 46 | EmberApp.setIsEuropeUnionUser(false); 47 | } 48 | ``` 49 | 50 | Or maybe they moved GDPR compliance to 3rd party services by discarding analytics based on IP which kind of defeats the point of GDPR in the first place. 51 | 52 | 53 | 54 | ```java 55 | for (Preset next : appStatisticsData.getPresets()) { 56 | JSONObject jSONObject2 = new JSONObject(); 57 | jSONObject2.put("name", next.getBeverage()); 58 | jSONObject2.put("temp", next.getTemperature()); 59 | jSONArray.put(jSONObject2); 60 | } 61 | jSONObject.put("presets", jSONArray); 62 | jSONObject.put("liquidLevel", Integer.toString(mugService.getLiquidLevel())); 63 | jSONObject.put("hw", mugService.getHardwareVersion()); 64 | jSONObject.put("loginMethod", getAuthLoginMethod(authorizationDataStore)); 65 | jSONObject.put("mobileOSVersion", Build.VERSION.RELEASE); 66 | jSONObject.put("latitude", getLatitude(str)); 67 | jSONObject.put("deviceType", getDeviceType(mugService)); 68 | jSONObject.put("currentTemperature", String.valueOf(Math.round(evaluateUnits(true, mugService)))); 69 | jSONObject.put("fw", mugService.getFirmwareVersion()); 70 | jSONObject.put("mugId", mugService.getId()); 71 | jSONObject.put("date", getCurrentTimeInIso8601()); 72 | jSONObject.put("targetTemperature", String.valueOf(Math.round(evaluateUnits(false, mugService)))); 73 | jSONObject.put("temperatureUnit", getTemperatureUnit(mugService)); 74 | jSONObject.put("longitude", getLongitude(str2)); 75 | jSONObject.put("liquidState", getLiquidState(mugService)); 76 | jSONObject.put(RemoteConfigConstants.RequestFieldKey.APP_VERSION, appUtils.getVersionName()); 77 | jSONObject.put("email", getAuthEmail(authorizationDataStore)); 78 | jSONObject.put("mobilePlatform", MOBILE_PLATFORM); 79 | jSONObject.put("mobileDevice", Build.MANUFACTURER + Build.PRODUCT); 80 | ``` --------------------------------------------------------------------------------