<reponame>anewinn/aeroponics
import argparse
import datetime
import json
import os
import sys
import platform
import random
import ssl
from time import sleep

import jwt
import paho.mqtt.client as mqtt
from rfc3339 import rfc3339


def parse_command_line_args():
  """Parse command line arguments."""
  parser = argparse.ArgumentParser(description=(
    'Example Google Cloud IoT Core MQTT device connection code.'))
  parser.add_argument(
    '--project_id',
    default=os.environ.get('GOOGLE_CLOUD_PROJECT'),
    help='GCP cloud project name')
  parser.add_argument(
    '--registry_id',
    default='my-registry',
    help='Cloud IoT Core registry id')
  parser.add_argument(
    '--device_id',
    default='my-device',
    help='Cloud IoT Core device id')
  parser.add_argument(
    '--private_key_file',
    default='rsa_private.pem',
    help='Path to private key file.')
  parser.add_argument(
    '--algorithm',
    choices=('RS256', 'ES256'),
    default='RS256',
    help='Which encryption algorithm to use to generate the JWT.')
  parser.add_argument(
    '--cloud_region',
    default='europe-west1',
    help='GCP cloud region')
  parser.add_argument(
    '--ca_certs',
    default='roots.pem',
    help=('CA root from https://pki.google.com/roots.pem'))
  parser.add_argument(
    '--message_type',
    choices=('event', 'state'),
    default='event',
    help=('Indicates whether the message to be published is a '
          'telemetry event or a device state message.'))
  parser.add_argument(
    '--mqtt_bridge_hostname',
    default='mqtt.googleapis.com',
    help='MQTT bridge hostname.')
  parser.add_argument(
    '--mqtt_bridge_port',
    default=8883,
    type=int,
    help='MQTT bridge port.')
  parser.add_argument(
    '--jwt_expires_minutes',
    default=60,
    type=int,
    help=('Expiration time, in minutes, for JWT tokens.'))
  parser.add_argument(
    '--device_type',
    choices=('sim', 'pi'),
    default='pi',
    help='Type of device: sim|pi.')
  parser.add_argument(
    '--serial_port',
    default='/dev/ttyACM0',
    help='Serial port device connected to the Arduino.')
  return parser.parse_args()


def create_jwt(project_id, private_key_file, algorithm):
  """Creates a JWT (https://jwt.io) to establish an MQTT connection.
      Args:
       project_id: The cloud project ID this device belongs to
       private_key_file: A path to a file containing either an RSA256 or
               ES256 private key.
       algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'
      Returns:
          An MQTT generated from the given project_id and private key, which
          expires in 20 minutes. After 20 minutes, your client will be
          disconnected, and a new JWT will have to be generated.
      Raises:
          ValueError: If the private_key_file does not contain a known key.
      """

  token = {
    'iat': datetime.datetime.utcnow(),
    'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
    'aud': project_id
  }

  with open(private_key_file, 'r') as f:
    private_key = f.read()

  print('Creating JWT using {} from private key file {}'.format(
    algorithm, private_key_file))

  return jwt.encode(token, private_key, algorithm=algorithm)


def get_client(
  project_id, cloud_region, registry_id, device_id, private_key_file,
  algorithm, ca_certs, mqtt_bridge_hostname, mqtt_bridge_port):

  """Create our MQTT client. The client_id is a unique string that identifies
  this device. For Google Cloud IoT Core, it must be in the format below."""

  client = mqtt.Client(
    client_id=('projects/{}/locations/{}/registries/{}/devices/{}'
               .format(
                       project_id,
                       cloud_region,
                       registry_id,
                       device_id)))

  client.username_pw_set(
    username='unused',
    password=create_jwt(
            project_id, private_key_file, algorithm))

  client.tls_set(ca_certs=ca_certs, tls_version=ssl.PROTOCOL_TLSv1_2)

  client.on_connect = on_connect
  client.on_publish = on_publish
  client.on_disconnect = on_disconnect
  client.on_message = on_message
  client.connect(mqtt_bridge_hostname, mqtt_bridge_port)
  
  print('Connecting to gCloud IOT')

  mqtt_config_topic = '/devices/{}/config'.format(device_id)
  client.subscribe(mqtt_config_topic, qos=1)
  client.loop_start()
  return client


def error_str(rc):
  """Convert a Paho error to a human readable string."""
  return '{}: {}'.format(rc, mqtt.error_string(rc))


def on_connect(unused_client, unused_userdata, unused_flags, rc):
  """Callback for when a device connects."""
  print('gcp_on_connect', error_str(rc))


def on_disconnect(unused_client, unused_userdata, rc):
  """Paho callback for when a device disconnects."""
  print('gcp_on_disconnect', error_str(rc))


def on_publish(unused_client, unused_userdata, unused_mid):
  """Paho callback when a message is sent to the broker."""
  print('gcp_on_publish')


def on_message(unused_client, unused_userdata, message):
  """Callback when the device receives a message on a subscription."""
  payload = str(message.payload)
  print('Received message \'{}\' on topic \'{}\' with Qos {}'.format(
          payload, message.topic, str(message.qos)))


def publish(client, mqtt_topic, device, pipePressure, resTemp, resPH, resEC, sens5):
  """Function to publish sensor data to Cloud IoT Core."""
  payload = {}
  payload['clientid'] = platform.uname()[1]
  payload['pipePressure'] = float('{:.2f}'.format(pipePressure))
  payload['resTemperature'] = float('{:.2f}'.format(resTemp))
  payload['resPH'] = float('{:.2f}'.format(resPH))
  payload['resEC'] = float('{:.2f}'.format(resEC))
  payload['spareSensor'] = float('{:.2f}'.format(sens5))
  payload['timestamp'] = rfc3339(datetime.datetime.now())
  json_payload = json.dumps(payload)
  print('Publishing message: {}'.format(json_payload))
  client.publish(mqtt_topic, json_payload, qos=1)  
  return


def read_sensors(device, ser, pipePressure, resTemp, resPH, resEC, sens5):
  """Read Pi sensors"""
  print('Reading the sensors')
  try:

    if device == 'pi':
      s = {}
      response = serial_send_and_receive(ser, "1\n")
      response = response.rstrip()                          # removes trailing spaces in string
      print('Received from Arduino: {}'.format(response))
      try:
        if response[0] == 'X':
          pairs = response.split(' ')
          for pair in pairs:
            if pair == 'X':
              continue
            else:
              name, value = pair.split(':')
              s[name] = value
        else:
          print('Error getting Arduino sensor values over serial')
          return 0
        
      except:
          print('No readings received')
      
      try:
        pipePressure = float(s['p'])
        resTemp = 0
        resPH = 0
        resEC = 0
        sens5 = 0
      except:
        pipePressure = 0
        resTemp = 0
        resPH = 0
        resEC = 0
        sens5 = 0
    
    else:
      pipePressure = simulate_sensors(pipePressure, 3, -40, 50)
      resTemp = simulate_sensors(resTemp, 6, 850, 1150)
      resPH = simulate_sensors(resPH, 3, 0, 100)
      resEC = simulate_sensors(resEC, 0.1, 0, 3.6)
      sens5 = simulate_sensors(sens5, 0.1, 0, 3.6)

  except IOError:
    print('I/O Error')
    
  return pipePressure, resTemp, resPH, resEC, sens5


def serial_send_and_receive(ser, theinput):
  """Write string to serial connection and return any response."""
  
  ser.write(b"2\n")
  
  try:
    state = ser.readline().decode('utf-8').rstrip()
  except:
    state = "empty"
        
  return state


def simulate_sensors(prev, stdev, min, max):
  """Gaussian distribution for simulated sensor readings."""
  delta = random.gauss(0, stdev)
  new = prev + delta
  if new < min or new > max:
    new = prev - delta
  return new


def init_serial(serial_port):

  import serial
  print('Creating and flushing serial port. Rebooting Arduino..')
  ser = serial.Serial(serial_port)
  with ser:
    ser.setDTR(False)
    sleep(1)
    ser.flushInput()
    ser.setDTR(True)
  ser = serial.Serial(serial_port, 9600, timeout=1)
  ser.flush()
  print('Wait 5s for set-up..')
  sleep(5)
  return ser

def main(argv):
  args = parse_command_line_args()

  sub_topic = 'events' if args.message_type == 'event' else 'state'
  mqtt_topic = '/devices/{}/{}'.format(args.device_id, sub_topic)

  device = args.device_type
  if device == 'pi':
    ser = init_serial(args.serial_port)
  else:
    ser = None
  
  # Default starting values for sensor readings
  pipePressure = 0
  resTemp = 0
  resPH = 0
  resEC = 0
  sens5 = 0

  jwt_iat = datetime.datetime.utcnow()
  jwt_exp_mins = args.jwt_expires_minutes

  client = get_client(
    args.project_id, args.cloud_region, args.registry_id, args.device_id,
    args.private_key_file, args.algorithm, args.ca_certs,
    args.mqtt_bridge_hostname, args.mqtt_bridge_port)

  while True:
    seconds_since_issue = (datetime.datetime.utcnow() - jwt_iat).seconds
    if seconds_since_issue > 60 * (jwt_exp_mins - 2):
      print('Refreshing token after {}s').format(seconds_since_issue)
      client.loop_stop()
      jwt_iat = datetime.datetime.utcnow()
      client = get_client(
        args.project_id, args.cloud_region,
        args.registry_id, args.device_id, args.private_key_file,
        args.algorithm, args.ca_certs, args.mqtt_bridge_hostname,
        args.mqtt_bridge_port)
    pipePressure, resTemp, resPH, resEC, sens5 = read_sensors(device, ser, pipePressure, resTemp, resPH, resEC, sens5)
    publish(client, mqtt_topic, device, pipePressure, resTemp, resPH, resEC, sens5)
    sleep(10)  # Wait between readings


if __name__ == '__main__':
  main(sys.argv)



import argparse
import datetime
import json
import os
import sys
import platform
import random
import ssl
from time import sleep

import jwt
import paho.mqtt.client as mqtt
from rfc3339 import rfc3339


def parse_command_line_args():
  """Parse command line arguments."""
  parser = argparse.ArgumentParser(description=(
    'Example Google Cloud IoT Core MQTT device connection code.'))
  parser.add_argument(
    '--project_id',
    default=os.environ.get('GOOGLE_CLOUD_PROJECT'),
    help='GCP cloud project name')
  parser.add_argument(
    '--registry_id',
    default='my-registry',
    help='Cloud IoT Core registry id')
  parser.add_argument(
    '--device_id',
    default='my-device',
    help='Cloud IoT Core device id')
  parser.add_argument(
    '--private_key_file',
    default='rsa_private.pem',
    help='Path to private key file.')
  parser.add_argument(
    '--algorithm',
    choices=('RS256', 'ES256'),
    default='RS256',
    help='Which encryption algorithm to use to generate the JWT.')
  parser.add_argument(
    '--cloud_region',
    default='europe-west1',
    help='GCP cloud region')
  parser.add_argument(
    '--ca_certs',
    default='roots.pem',
    help=('CA root from https://pki.google.com/roots.pem'))
  parser.add_argument(
    '--message_type',
    choices=('event', 'state'),
    default='event',
    help=('Indicates whether the message to be published is a '
          'telemetry event or a device state message.'))
  parser.add_argument(
    '--mqtt_bridge_hostname',
    default='mqtt.googleapis.com',
    help='MQTT bridge hostname.')
  parser.add_argument(
    '--mqtt_bridge_port',
    default=8883,
    type=int,
    help='MQTT bridge port.')
  parser.add_argument(
    '--jwt_expires_minutes',
    default=60,
    type=int,
    help=('Expiration time, in minutes, for JWT tokens.'))
  parser.add_argument(
    '--device_type',
    choices=('sim', 'pi'),
    default='pi',
    help='Type of device: sim|pi.')
  parser.add_argument(
    '--serial_port',
    default='/dev/ttyACM0',
    help='Serial port device connected to the Arduino.')
  return parser.parse_args()


def create_jwt(project_id, private_key_file, algorithm):
  """Creates a JWT (https://jwt.io) to establish an MQTT connection.
      Args:
       project_id: The cloud project ID this device belongs to
       private_key_file: A path to a file containing either an RSA256 or
               ES256 private key.
       algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'
      Returns:
          An MQTT generated from the given project_id and private key, which
          expires in 20 minutes. After 20 minutes, your client will be
          disconnected, and a new JWT will have to be generated.
      Raises:
          ValueError: If the private_key_file does not contain a known key.
      """

  token = {
    'iat': datetime.datetime.utcnow(),
    'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
    'aud': project_id
  }

  with open(private_key_file, 'r') as f:
    private_key = f.read()

  print('Creating JWT using {} from private key file {}'.format(
    algorithm, private_key_file))

  return jwt.encode(token, private_key, algorithm=algorithm)


def get_client(
  project_id, cloud_region, registry_id, device_id, private_key_file,
  algorithm, ca_certs, mqtt_bridge_hostname, mqtt_bridge_port):

  """Create our MQTT client. The client_id is a unique string that identifies
  this device. For Google Cloud IoT Core, it must be in the format below."""

  client = mqtt.Client(
    client_id=('projects/{}/locations/{}/registries/{}/devices/{}'
               .format(
                       project_id,
                       cloud_region,
                       registry_id,
                       device_id)))

  client.username_pw_set(
    username='unused',
    password=create_jwt(
            project_id, private_key_file, algorithm))

  client.tls_set(ca_certs=ca_certs, tls_version=ssl.PROTOCOL_TLSv1_2)

  client.on_connect = on_connect
  client.on_publish = on_publish
  client.on_disconnect = on_disconnect
  client.on_message = on_message
  client.connect(mqtt_bridge_hostname, mqtt_bridge_port)
  
  print('Connecting to gCloud IOT')

  mqtt_config_topic = '/devices/{}/config'.format(device_id)
  client.subscribe(mqtt_config_topic, qos=1)
  client.loop_start()
  return client


def error_str(rc):
  """Convert a Paho error to a human readable string."""
  return '{}: {}'.format(rc, mqtt.error_string(rc))


def on_connect(unused_client, unused_userdata, unused_flags, rc):
  """Callback for when a device connects."""
  print('gcp_on_connect', error_str(rc))


def on_disconnect(unused_client, unused_userdata, rc):
  """Paho callback for when a device disconnects."""
  print('gcp_on_disconnect', error_str(rc))


def on_publish(unused_client, unused_userdata, unused_mid):
  """Paho callback when a message is sent to the broker."""
  print('gcp_on_publish')


def on_message(unused_client, unused_userdata, message):
  """Callback when the device receives a message on a subscription."""
  payload = str(message.payload)
  print('Received message \'{}\' on topic \'{}\' with Qos {}'.format(
          payload, message.topic, str(message.qos)))


def publish(client, mqtt_topic, device, pipePressure, resTemp, resPH, resEC, sens5):
  """Function to publish sensor data to Cloud IoT Core."""
  payload = {}
  payload['clientid'] = platform.uname()[1]
  payload['pipePressure'] = float('{:.2f}'.format(pipePressure))
  payload['resTemperature'] = float('{:.2f}'.format(resTemp))
  payload['resPH'] = float('{:.2f}'.format(resPH))
  payload['resEC'] = float('{:.2f}'.format(resEC))
  payload['spareSensor'] = float('{:.2f}'.format(sens5))
  payload['timestamp'] = rfc3339(datetime.datetime.now())
  json_payload = json.dumps(payload)
  print('Publishing message: {}'.format(json_payload))
  client.publish(mqtt_topic, json_payload, qos=1)  
  return


def read_sensors(device, ser, pipePressure, resTemp, resPH, resEC, sens5):
  """Read Pi sensors"""
  print('Reading the sensors')
  try:

    if device == 'pi':
      s = {}
      response = serial_send_and_receive(ser, "1\n")
      response = response.rstrip()                          # removes trailing spaces in string
      print('Received from Arduino: {}'.format(response))
      try:
        if response[0] == 'X':
          pairs = response.split(' ')
          for pair in pairs:
            if pair == 'X':
              continue
            else:
              name, value = pair.split(':')
              s[name] = value
        else:
          print('Error getting Arduino sensor values over serial')
          return 0
        
      except:
          print('No readings received')
      
      try:
        pipePressure = float(s['p'])
        resTemp = 0
        resPH = 0
        resEC = 0
        sens5 = 0
      except:
        pipePressure = 0
        resTemp = 0
        resPH = 0
        resEC = 0
        sens5 = 0
    
    else:
      pipePressure = simulate_sensors(pipePressure, 3, -40, 50)
      resTemp = simulate_sensors(resTemp, 6, 850, 1150)
      resPH = simulate_sensors(resPH, 3, 0, 100)
      resEC = simulate_sensors(resEC, 0.1, 0, 3.6)
      sens5 = simulate_sensors(sens5, 0.1, 0, 3.6)

  except IOError:
    print('I/O Error')
    
  return pipePressure, resTemp, resPH, resEC, sens5


def serial_send_and_receive(ser, theinput):
  """Write string to serial connection and return any response."""
  
  ser.write(b"2\n")
  
  try:
    state = ser.readline().decode('utf-8').rstrip()
  except:
    state = "empty"
        
  return state


def simulate_sensors(prev, stdev, min, max):
  """Gaussian distribution for simulated sensor readings."""
  delta = random.gauss(0, stdev)
  new = prev + delta
  if new < min or new > max:
    new = prev - delta
  return new


def init_serial(serial_port):

  import serial
  print('Creating and flushing serial port. Rebooting Arduino..')
  ser = serial.Serial(serial_port)
  with ser:
    ser.setDTR(False)
    sleep(1)
    ser.flushInput()
    ser.setDTR(True)
  ser = serial.Serial(serial_port, 9600, timeout=1)
  ser.flush()
  print('Wait 5s for set-up..')
  sleep(5)
  return ser

def main(argv):
  args = parse_command_line_args()

  sub_topic = 'events' if args.message_type == 'event' else 'state'
  mqtt_topic = '/devices/{}/{}'.format(args.device_id, sub_topic)

  device = args.device_type
  if device == 'pi':
    ser = init_serial(args.serial_port)
  else:
    ser = None
  
  # Default starting values for sensor readings
  pipePressure = 0
  resTemp = 0
  resPH = 0
  resEC = 0
  sens5 = 0

  jwt_iat = datetime.datetime.utcnow()
  jwt_exp_mins = args.jwt_expires_minutes

  client = get_client(
    args.project_id, args.cloud_region, args.registry_id, args.device_id,
    args.private_key_file, args.algorithm, args.ca_certs,
    args.mqtt_bridge_hostname, args.mqtt_bridge_port)

  while True:
    seconds_since_issue = (datetime.datetime.utcnow() - jwt_iat).seconds
    if seconds_since_issue > 60 * (jwt_exp_mins - 2):
      print('Refreshing token after {}s').format(seconds_since_issue)
      client.loop_stop()
      jwt_iat = datetime.datetime.utcnow()
      client = get_client(
        args.project_id, args.cloud_region,
        args.registry_id, args.device_id, args.private_key_file,
        args.algorithm, args.ca_certs, args.mqtt_bridge_hostname,
        args.mqtt_bridge_port)
    pipePressure, resTemp, resPH, resEC, sens5 = read_sensors(device, ser, pipePressure, resTemp, resPH, resEC, sens5)
    publish(client, mqtt_topic, device, pipePressure, resTemp, resPH, resEC, sens5)
    sleep(10)  # Wait between readings


if __name__ == '__main__':
  main(sys.argv)
Parse command line arguments. Creates a JWT (https://jwt.io) to establish an MQTT connection.       Args:        project_id: The cloud project ID this device belongs to        private_key_file: A path to a file containing either an RSA256 or                ES256 private key.        algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'       Returns:           An MQTT generated from the given project_id and private key, which           expires in 20 minutes. After 20 minutes, your client will be           disconnected, and a new JWT will have to be generated.       Raises:           ValueError: If the private_key_file does not contain a known key. Create our MQTT client. The client_id is a unique string that identifies   this device. For Google Cloud IoT Core, it must be in the format below. Convert a Paho error to a human readable string. Callback for when a device connects. Paho callback for when a device disconnects. Paho callback when a message is sent to the broker. Callback when the device receives a message on a subscription. Function to publish sensor data to Cloud IoT Core. Read Pi sensors # removes trailing spaces in string Write string to serial connection and return any response. Gaussian distribution for simulated sensor readings. # Default starting values for sensor readings # Wait between readings