Integrate Email (Utilities)

In Singapore, the utilities company sends you an email with your consumption and bill every month. I wanted to integrate this data into Home-Assistant. I achieved using Apps Script and MQTT. (See Code in GitHub).

This same technology can actually be used to create any email-based sensors.

Requirements

  • Using gMail. Apps Script is a Google Drive solution that allows you to run some Google APIs and execute them on a schedule in a very easy way.
  • Having a MQTT component configure.

How It Works

Apps Script

  1. Create a new Standalone Script.
/**
 * Fetches utility consumption details from the Utilities email and sends it to Hassio MQTT.
 *
 * Apps Script on drive.google.com
 */

/* Configuration. */

/** @const {string} URL for MQTT Server API. */
var MQTT_URL = 'https://your-ha-domain:port/api/services/mqtt/publish';

/** @const {string} (Long-Lived) Access token for API. */
var API_TOKEN = 'your-api-token'

/** @const {string} MQTT Topic for Utility Bill. */
var EXPENSE_TOPIC = '/utilities/bill';

/** @const {string} MQTT Topic for Electricity Consumption. */
var ELECTRICITY_TOPIC = '/utilities/electricity';

/** @const {string} MQTT Topic for Water Consumption. */
var WATER_TOPIC = '/utilities/water';

/** @const {string} MQTT Topic for Gas Consumption. */
var GAS_TOPIC = '/utilities/gas';

/** @const {string} Query search to fetch utility emails. */
var UTILITY_QUERY = 'subject:"Utility Bill Summary"';

/** @const {!RegExp} RegExp to find monetary values in SGD. */
var SGD_REGEXP = new RegExp(/S\$[0-9]+\.[0-9]{2}/g);

/** @const {!RegExp} RegExp to find kWh energy values. */
var KWH_REGEXP = new RegExp(/[0-9]+ kWh/g);

/** @const {!RegExp} RegExp to find Cu M water values. */
var CUM_REGEXP = new RegExp(/[0-9]+\.[0-9]+ Cu M/);


/* Main workflow. */

/**
 * Parses content of utilities email and send data to Hassio.
 * Main function. Schedule on a periodic basis (e.g. weekly).
 */
function main() {
  var utilityEmail = fetchLastUtilityEmail();
  if (!utilityEmail) return;

  var utilityContent = getEmailContent(utilityEmail);
  if (!utilityContent) return;

  var billExpense = getBillExpense(utilityContent);
  var electricityConsumption = getElectricityConsumption(utilityContent);
  var waterConsumption = getWaterConsumption(utilityContent);
  var gasConsumption = getGasConsumption(utilityContent);

  sendToMqttServer(EXPENSE_TOPIC, billExpense);
  sendToMqttServer(ELECTRICITY_TOPIC, electricityConsumption);
  sendToMqttServer(WATER_TOPIC, waterConsumption);
  sendToMqttServer(GAS_TOPIC, gasConsumption);
}


/* Url fetching utilities. */

/**
 * Send data to MQTT server.
 * @param {string} topic MQTT topic to which to send data.
 * @param {*} data Data to send to MQTT.
 */
function sendToMqttServer(topic, data) {
  var payload = {
    'topic': topic,
    'payload': data,
    'retain': true,
  };

  var headers = {
    'Authorization': 'Bearer ' + API_TOKEN,
    'Content-Type': 'application/json',
  };

  var fetchOptions = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(payload),
    'escaping': false,
  };

  UrlFetchApp.fetch(MQTT_URL, fetchOptions);
}


/* Email parsing. */

/**
 * Fetches the last utility email.
 * @return {GmailMessage} Message for the utility bill.
 *   Returns null if the thread is not found.
 */
function fetchLastUtilityEmail() {
  var threads = GmailApp.search(UTILITY_QUERY);

  if (threads.length === 0) return null;

  var messages = threads[0].getMessages();
  return messages[0];
}

/**
 * Get email contents.
 * @param {!GmailMessage} emailMessage Email message from which to get body.
 * @return {string} Contents of the message.
 */
function getEmailContent(emailMessage) {
  return emailMessage.getBody();
}

/**
 * Extracts expense from utility email.
 * @param {string} emailBody Contents of the utility email.
 * @return {?number} Total expense of the month for utility email.
 *   Returns null if not found or unparseable as float.
 */
function getBillExpense(emailBody) {
  return parseContentVariable(emailBody, SGD_REGEXP, 0, 'S$', parseFloatOrNull);
}

/**
 * Extracts electricity consumption from utility email.
 * @param {string} emailBody Contents of the utility email.
 * @return {?number} Electricity consumption of the month for utility email.
 *   Returns null if not found or unparseable as float.
 */
function getElectricityConsumption(emailBody) {
  return parseContentVariable(emailBody, KWH_REGEXP, 0, ' kWh', parseIntOrNull);
}

/**
 * Extracts water consumption from utility email.
 * @param {string} emailBody Contents of the utility email.
 * @return {?number} Water consumption of the month for utility email.
 *   Returns null if not found or unparseable as float.
 */
function getWaterConsumption(emailBody) {
  return parseContentVariable(emailBody, CUM_REGEXP, 0, ' Cu M', parseFloatOrNull);
}

/**
 * Extracts gas usage from utility email.
 * @param {string} emailBody Contents of the utility email.
 * @return {?number} Gas consumption of the month for utility email.
 *   Returns null if not found or unparseable as float.
 */
function getGasConsumption(emailBody) {
  return parseContentVariable(emailBody, KWH_REGEXP, 3, ' kWh', parseIntOrNull);
}


/* Email parsing utilities. */

/**
 * Extracts a clean content from a text.
 * @param {string} content Content from which to extract text.
 * @param {!RegExp} pattern Pattern to extract content.
 * @param {number=} index Index for matched pattern.
 * @param {string=} replacementText Text to remove before proceeding.
 * @param {function(string):(?string|?number)=} modifierFunction Function to manipulate value.
 * @return {?number|?string} Parsed content.
 *   Null if not found, or unparseable.
 */
function parseContentVariable(content, pattern, index, replacementText, modifierFunction) {
  if (index === undefined) index = 0;
  if (replacementText === undefined) replacementText = '';

  var matches = content.match(pattern);
  if (!matches || matches.length < index + 1) return null;

  var positionedMatch = matches[index];
  if (replacementText) positionedMatch = positionedMatch.replace(replacementText, '');

  if (modifierFunction) return modifierFunction(positionedMatch);
  return positionedMatch;
}

/**
 * Parses to int or null.
 * @param {string} string String to parse to int.
 * @return {?number} Parsed int, or null if not parseable.
 */
function parseIntOrNull(string) {
  var int = parseInt(string, 10);
  if (isNaN(int)) return null;
  return int;
}

/**
 * Parses to float or null.
 * @param {string} string String to parse to int.
 * @return {?number} Parsed float, or null if not parseable.
 */
function parseFloatOrNull(string) {
  var float = parseFloat(string, 10);
  if (isNaN(float)) return null;
  return float;
}

This script fetches an email with a given subject. Then, uses RegExp to parse the content (document.querySelector is not available in Apps Script). Finally, it sends the data to MQTT via REST API (HTTP Post request).

If you are also in Singapore, you can just adjust your MQTT_URL and API_TOKEN with your Home-Assistant URL for MQTT and your API Token respectively.

  1. Create a Time-Driven Trigger a daily, weekly or monthly trigger the script and update the sensor data.

Home-Assistant

  1. Create sensors to read the post data.
sensor:
  - platform: mqtt
    name: "Utilities Bill"
    unit_of_measurement: "SGD$"
    state_topic: "/utilities/bill"

  - platform: mqtt
    name: "Electricity Consumption"
    unit_of_measurement: "kWh"
    state_topic: "/utilities/electricity"

  - platform: mqtt
    name: "Water Consumption"
    unit_of_measurement: "Cu M"
    state_topic: "/utilities/water"

  - platform: mqtt
    name: "Gas Consumption"
    unit_of_measurement: "kWh"
    state_topic: "/utilities/gas"
  1. (Optional) Add the sensors to your groups or views.

Why are you publishing to MQTT via the HA API? You can just publish direct to the broker without needing an API token.
You could also create a sensor direct from the script if you wanted instead of using an MQTT sensor…
Good idea though.

@DavidFW1960 Thanks for the feedback. This is not a Script within Hassio, the data is fetched on Apps Script which is on Google Drive as it’s reading my email.

With this in mind:

  1. Do you have an example on how to call MQTT from an external server without API token? I would prefer to implement that, yes. All the examples and documentation and found had some sort of authentication (I just preferred token over others).

  2. For your second comment: this is not a Hassio script. What do you mean about creating a sensor directly from the script? Do you mean using the REST API to push a value to the sensor? If yes, I guess it would be about whether MQTT adds any value. I think persistance of data is better (aka: not having “unknown” as this only runs once per week), but not too sure.

1 Like

I just use mosquitto_pub on my coffee roaster controller to publish to a topic on the broker and I use a mqtt sensor as you have done to get that into Home Assistant. I use MQTT anyway but I also could just use a sensor in HA directly. You would need mosquitto-tools installed to get that command.

Yes using the rest api to push to a sensor. I was thinking about this earlier and you are right - with mqtt it will be persistent if you use the retain flag whereas with a sensor it won’t populate until the sensor refreshes. For a script I use with the API, it runs when HA starts and then at a specified interval. Your use case might not make this as practical?

@DavidFW1960 Thanks for the clarifications.

Yes, that was my point on my first message. Since the technology used was Apps Script, this wouldn’t be possible. Apps Script only allows you to run JavaScript and does not allow you to install modules or packages. As such, that “mosquitto_pub” that you suggested wouldn’t be possible. This is why I opted for the REST API as it’s the only way that would integrate like that.

The native alternative of doing this on Home-Assistant would be to integrate Gmail API, with full OAuth workflow, and then pull the email. This would be a lot of hassle, not just for the custom component, but because the OAuth is an overkill just to read one email.

Obviously there are advantage and disadvantages to both methods, but the overall for this approach is this:

Advantages of Apps Script (for this solution)

  1. No OAuth needed, based on Drive credentials where run.
  2. Easy and integrated API access Gmail.
  3. As a result, fast development and low maintenance.

Disadvantages of Apps Script (for this solution)

  1. Code and schedule does not live within Home-Assistant.
  2. It requires a Long Lived Token, which is a point of attack.

In this case, I think the risks and costs are small compared to the fast development and low maintenance.

In any case, I just created for fun and to showcase that different type of integrations are possible within the platform.

@nitobuendia thank you for sharing this.
how would you use this script to get an mqtt message when you receive an email with a specific subject.
the trigger being the email itself.

As far as I know, there’s no onEmail trigger.

Technically, you can use the same code make the trigger to run more frequently (e.g. every minute).

Although, that may require changes. For example, a way to store the last email you processed to avoid duplicates and the code needs to run fast enough to avoid race conditions.

Thank you.
My intention was to use it to report a device state.
After it reports I came across a python script that deletes the email. So no duplicates.
Basically
Trigger, report state, delete email.
I got it working with imap but wandered if mqtt would be faster

Hi, nice project.
I try to use but for now seems to have an error: Exception: Request failed for https://xxx returned code 400. Truncated server response: 400: Bad Request (use muteHttpExceptions option to examine full response)
If my utility suppliers have different ways to entitle the subject, this means i have to make as many scripts as many suppliers i have? or i can say if the subject is “bill1” or “invoice billing” or “right now we issued your invoice” …then continue with extract