Repository: opsview/plugnpy

Issue:
Encoding error on README.md
In certain environments, the build will fail with an encoding error on README.md:

```
$ pip install -e .[test]
# ...
    ERROR: Complete output from command python setup.py egg_info:
    ERROR: Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/builds/IMPSS/tpp.lib.plugnpy/setup.py", line 44, in <module>
        long_description=read('README.md'),
      File "/builds/IMPSS/tpp.lib.plugnpy/setup.py", line 13, in read
        return open(join(dirname(__file__), fname)).read()
      File "/root/.pyenv/versions/3.6.7/lib/python3.6/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 3342: ordinal not in range(128)
    ----------------------------------------
```

Because `README.md` is in utf-8, this should be used when reading from it.
```
11:46 $ file README.md
README.md: UTF-8 Unicode text, with very long lines
```

In `setup.py`:
```python
def read(fname):
    return open(join(dirname(__file__), fname), encoding='utf-8').read()
```

Should have `encoding='utf-8'` as above.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 365bd2c..1d24eaa 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,19 @@
 
 ## Installing the Library
 
-For use with versions of Opsview below 6.1, Opsview Python's pip will need to be used to install this library, as they do not ship with it pre-installed.
+For Opsview versions above 6.1. **plugnpy** is preinstalled.
 
-`/opt/opsview/python/bin/pip install <location of plugnpy-version.tar.gz>`
+For Opsview versions below 6.1, Opsview Python's **pip** needs to be used to install the library.
 
-To install the library locally, download the release package and simply install with pip.
+```
+/opt/opsview/python/bin/pip install <location of plugnpy-version.tar.gz>
+```
+
+To install the library locally, download the release package and install with **pip**.
 
-`pip install <location of plugnpy-version.tar.gz>`
+```
+pip install <location of plugnpy-version.tar.gz>
+```
 
 ## Quick Start
 
@@ -31,23 +37,23 @@ To see all available options:
 make help
 ```
 
-To build a Conda development environment:  
+To build a Conda development environment:
 ```
 make conda_dev
 . activate
 ```
 
-To test inside a `conda_dev` environment using setuptools:  
+To test inside a `conda_dev` environment using setuptools:
 ```
 make test
 ```
 
-To build and test the project inside a Conda environment:  
+To build and test the project inside a Conda environment:
 ```
 make build
 ```
 
-The coverage report is available at:  
+The coverage report is available at:
 ```
 env-~#PROJECT#~/conda-bld/coverage/htmlcov/index.html
 ```
@@ -57,84 +63,221 @@ env-~#PROJECT#~/conda-bld/coverage/htmlcov/index.html
 
 Should you wish (or if you've made improvements to the source code and you want to use them), the library can be built with the following command.
 
-`python setup.py sdist`
+```
+python setup.py sdist
+```
 
 or
 
-`make wheel`
+```
+make wheel
+```
 
-This will create a `plugnpy-VERSION.RELEASE.tar.gz`file in the `dist` directory which should be installed the same way as the prepackaged one above.
+This will create a `plugnpy-VERSION.RELEASE.tar.gz` file in the `dist` directory which can be installed in the same way as the prepackaged one above.
 
 
 ## Writing Checks
 
-All plugins written using the Library must first import it. This can be done by simply writing import plugnpy at the top of the script.
+The core of a check written using plugnpy is the **Check** object. A **Check** object must be instantiated before metrics can be defined.
+
+```python
+import plugnpy
+check = plugnpy.Check()
+```
+
+To add metrics to this check, simply use the **add_metric()** method of the **Check** object. This takes in arguments to add a **Metric** object to an internal array.
+
+```python
+check.add_metric('disk_usage', 30.5, '%', display_name="Disk Usage",
+                 display_format='{name} is {value}{unit}')
+```
+
+The **Metric** objects are then used to create the final output when the **final()** method is called.
+
+```python
+check.final()
+```
+
+This would produce the following output:
+
+`METRIC OK - Disk Usage is 30.50% | disk_usage=30.50%;;`
+
+You can also specify the precision of the value in the summary and the performance data that you want to output, by using the **summary_precision** and **perf_data_precision** parameters when adding metrics (default is 2 decimal places):
+
+```python
+check.add_metric('disk_usage', 30.55432, '%', display_name="Disk Usage",
+                 display_format='{name} is {value}{unit}', summary_precision=3, perf_data_precision=4)
+```
+This would produce the following output:
+
+`METRIC OK - Disk Usage is 30.554% | disk_usage=30.5543%;;`
+
+## Checks with thresholds
+
+To apply thresholds to a metric, simply set the threshold values in the **add_metric()** call.
+
+```python
+check.add_metric('cpu_usage', 70.7, '%', warning_threshold='70', critical_threshold='90',
+                 display_name="CPU Usage", display_format="{name} is {value}{unit}")
+```
+
+This would produce the following output:
+
+`METRIC WARNING - CPU Usage is 70.70% | cpu_usage=70.70%;70;90`
+
+The library supports all Nagios threshold definitions as found here: [Nagios Development Guidelines · Nagios Plugins](https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT).
+
+As well as being fully compatible with Nagios thresholds, **plugnpy** allows thresholds to be specified in friendly units.
 
-The core of a check written using plugnpy is the Check object. A Check object must be instantiated before Metrics can be defined.
+```python
+check.add_metric('mem_swap', 100, 'B', '10MB', '20MB', display_name="Memory Swap")
+```
+
+This would produce the following output:
+
+`METRIC OK - Memory Swap is 100.00B | mem_swap=100.00B;10MB;20MB`
+
+## Writing checks with multiple metrics
+
+Writing service checks with multiple metrics is easy. Simply create the **Check** object and add multiple metrics using the **add_metric()** method.
 
 ```python
 check = plugnpy.Check()
+check.add_metric('disk_usage', 30.5, '%', '70', '90', display_name="Disk Usage",
+                 display_format='{name} is {value}{unit}')
+check.add_metric('cpu_usage', 70.7, '%', '70', '90', display_name="CPU Usage",
+                 display_format='{name} is {value}{unit}')
+check.final()
 ```
 
-To add metrics to this check, simply use the add_metric() method of your Check object. This takes in arguments to add a Metric object to an internal array. It is these Metric objects that are called upon to create the final output.
+This would produce the following output:
+
+`METRIC WARNING - Disk Usage is 30.50%, CPU Usage is 70.70% | disk_usage=30.50%;70;90 cpu_usage=70.70%;70;90`
+
+When adding multiple metrics, the separator between metrics can be customised. By default this is set to `', '` but can easily be changed or removed by setting the **sep** field when creating the **Check** object.
 
 ```python
-check.add_metric('metric_name', metric_value, unit, warning_threshold, critical_threshold,
-                 display_name="Metric Name", msg_fmt="{name} at {value}{unit}",
-                 convert_metric=True)
+check = plugnpy.Check(sep=' + ')
 ```
 
-### Checks with thesholds
+Adding multiple metrics to the **Check** object would then produce the following output:
+
+`METRIC OK - Disk Usage is 30.50% + CPU Usage is 70.70% | disk_usage=30.5%;70;90 cpu_usage=70.70%;70;90`
+
+## Checks with automatic conversions
 
-To create a check with thresholds, simply set the threshold values in the add_metric() call. Ideally, these metrics would come from user passed arguments but they are hardcoded here as an example.
+To create a check with automatic value conversions, simply call the **add_metric()** method with the **convert_metric** field set to **True**.
+The unit passed in should not have any existing prefix - for example, pass your value in **B** rather than **KB** or **MB**.
+
+Setting the **convert_metric** field to **True** will override the unit (displayed in the summary) with the best match for the conversion.
+By default, **convert_metric** is set to **True** only for metrics in **B**, **b**, **Bps** and **bps**.
+
+The supported conversion prefixes are:
+
+|Prefix Name | Prefix Symbol | Base 1000         |
+|:-----------|:--------------|:------------------|
+|exa         |E              | 1000<sup>6</sup>  |
+|peta        |P              | 1000<sup>5</sup>  |
+|tera        |T              | 1000<sup>4</sup>  |
+|giga        |G              | 1000<sup>3</sup>  |
+|mega        |M              | 1000<sup>2</sup>  |
+|kilo        |K              | 1000<sup>1</sup>  |
+|milli       |m              | 1000<sup>-1</sup> |
+|micro       |u              | 1000<sup>-2</sup> |
+|nano        |n              | 1000<sup>-3</sup> |
+|pico        |p              | 1000<sup>-4</sup> |
+
+
+The units supporting these prefixes are as follows:
+
+|Unit          |Supported conversion prefixes   |
+|:-------------|:-------------------------------|
+|s             |p, n, u, m                      |
+|B, b, Bps, bps|K, M, G, T, P, E                |
+|W, Hz         |p, n, u, m, K, M, G, T, P, E    |
+
+
+For example, adding the metric:
 
 ```python
-check.add_metric('cpu_usage', 70.7, '%', 60, 80, display_name="CPU Usage",
-                 msg_fmt="{name} at {value}{unit}")
+check.add_metric('mem_buffer', 1829863424, 'B', '1073741824', '2147483648',
+                 display_name="Memory Buffer", convert_metric=True)
 ```
+Would produce the following output:
 
-This line would create the following output:
+`METRIC WARNING - Memory Buffer is 1.70GB | mem_buffer=1829904384.00B;1073741824;2147483648`
 
-`METRIC WARNING - CPU Usage at 70.7% | cpu_usage=70.7%;60;80`
+And adding the metric below:
 
-The library supports all nagios threshold definitions as found here: [Nagios Development Guidelines · Nagios Plugins](https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT).
+```python
+check.add_metric('latency', 0.0002, 's', '0.0004', '0.0007', display_name="Latency",
+                 convert_metric=True, summary_precision=1, perf_data_precision=4)
+```
+Would produce the following output:
+
+`METRIC WARNING - Latency is 0.2ms | latency=0.0002s;0.0004;0.0007`
+
+All unit conversions are dealt with inside the library (as long as **convert_metric** is set to **True**), allowing values to be entered without having to do any manual conversions.
+
+For metrics with the **unit** related to bytes (**B**, **b**, **Bps** or **bps**), conversions are done based on the International Electrotechnical Commission (IEC) standard, using 1024 as the base multiplier. However the library also supports the International System (SI) standard, which uses 1000 as the base multiplier, this can be changed by calling **add_metric()**  with the **si_bytes_conversion** field set to **True** (**False** by default).
+
+```python
+check.add_metric('mem_buffer', 1000, 'B', '1GB', '2GB', display_name="Memory Buffer",
+                 convert_metric=True, si_bytes_conversion=True, summary_precision=0)
+```
+
+This would produce the following output:
+
+`METRIC OK - Memory Buffer is 1KB | mem_buffer=1000.00B;1GB;2GB`
 
-### Checks with metrics with units
 
-To create a check with thresholds, simply set the unit in the add_metric() call.
+For metrics using any other unit, conversions are done using the SI standard (1000 as the base multiplier).
+
+## Helper methods
+
+The **Metric** class includes two helper methods to make developing service checks easier.
+
+The **evaluate()** method evaluates a metric value against the specified warning and critical thresholds and returns the status code.
 
 ```python
-check.add_metric('mem_buffer', 1829863424, 'B', "1GB", "2GB", display_name="Memory Buffer",
-                 convert_metric=True)
+status_code = metric.evaluate(15, '10', '20')
 ```
 
-This line would create the following output:
+The above example would return the value `1`, since the value is above the warning threshold but not the critical threshold.
 
-`METRIC WARNING - Memory Buffer is 1.70GB | mem_buffer=1829904384B;1073741824;2147483648`
+The **convert_value()** method converts a given value and unit to a more human friendly value and unit.
 
-As you can see, all unit conversion is dealt with inside the library (so long as *convert_metric* is set to true!), allowing users to input their thresholds in friendly units rather than having to calculate the bytes themselves. Having *convert_metric* set to True will ignore the unit passed in and override it with the best match in Bytes.
+```python
+value, unit = metric.convert_value(2048, 'B')
+```
+
+The above example would return `2.00` as the value and `'KB'` as the unit.
+Both methods support the **si_bytes_conversion** field. See [**Checks with automatic conversions**](#checks-with-automatic-conversions) above for more details.
 
 ## Using the Argument Parser
 
-plugnpy comes with its own Argument Parser. This parser is essential argparse.ArgumentParser (a Python built in) but with its own  error() and  exit() to quit with the appropriate exit codes for Opspack UNKNOWNS.
+**plugnpy** comes with its own Argument Parser. This parser inherits from **argparse.ArgumentParser** but will exit with code 3 when called with the -h/--help flag.
+
+The parser also has the ability to print copyright information when -h/--help is called.
 
-The Parser also has the ability to print copyright information when -h/--help is called. This can either be setup when the Parser object is created, or added after.
+This can either be setup when the **Parser** object is created.
 
 ```python
-# Example 1
 parser = plugnpy.Parser(description="Monitors Memory Utilisation",
                         copyright="Example Copyright 2017-2018")
+```
 
-# Example 2
+Or added after by calling the **set_copyright()** method.
+```python
 parser = plugnpy.Parser(description="Monitors Memory Utilisation")
 parser.set_copyright("Example Copyright 2017-2018")
 ```
 
-To use the parser, create an object of type plugnpy.Parser and use as you would normally use an ArgumentParser object.
+To use the parser, create an object of type **plugnpy.Parser** and use as you would normally use an **argparse.ArgumentParser** object.
 
 ## Using the Exceptions
 
-plugnpy comes with its own Exception objects. These mirror the Exceptions we've used in recent Opspacks. They have no special implementation beyond their names and can be found in plugnpy.Exceptions. To be consistent, here is the appropriate times to raise each Exception.
+**plugnpy** comes with its own **Exception** objects. They have no special implementation beyond their names and can be found in **plugnpy.Exceptions**. To be consistent, here are the appropriate times to raise each exception:
 
 | Exception              | Usage                                                                                                                                                                |
 |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
diff --git a/VERSION b/VERSION
index 45a1b3f..227cea2 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.1.2
+2.0.0
diff --git a/conda/meta.yaml b/conda/meta.yaml
index cf502c7..58a8c88 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -1,6 +1,6 @@
 package:
   name: plugnpy
-  version: 1.1.1
+  version: 2.0.0
 
 source:
   path: ..
diff --git a/examples/check_cpu.py b/examples/check_cpu.py
index 7da691b..d7bf5a2 100644
--- a/examples/check_cpu.py
+++ b/examples/check_cpu.py
@@ -26,6 +26,6 @@ if __name__ == "__main__":
     check = plugnpy.Check()  # Instantiate Check object
     # Add Metric
     check.add_metric('cpu_usage', get_cpu_usage(), '%', args.warning, args.critical, display_name="CPU Usage",
-                     msg_fmt="{name} at {value}{unit}")
+                     display_format="{name} at {value}{unit}")
     # Run Check (handles printing and exit codes)
     check.final()
diff --git a/examples/check_memory.py b/examples/check_memory.py
index 90b0d96..34cc633 100644
--- a/examples/check_memory.py
+++ b/examples/check_memory.py
@@ -47,7 +47,7 @@ if __name__ == "__main__":
         memory_usage = get_mem_usage()
         # Add Metrics to Check Object
         check.add_metric('mem_utilisation', memory_usage.percent, '%', None, None,
-                         display_name="Memory Utilisation", msg_fmt="{name} at {value}{unit}")
+                         display_name="Memory Utilisation", display_format="{name} at {value}{unit}")
         check.add_metric('mem_buffer', memory_usage.buffers, 'B', args.warning, args.critical,
                          display_name="Memory Buffer", convert_metric=True)
         check.add_metric('mem_cache', memory_usage.cached, 'B', '', '',
@@ -55,7 +55,8 @@ if __name__ == "__main__":
         check.add_metric('mem_free', memory_usage.free, 'B', '', '',
                          convert_metric=True, display_in_summary=False)
         check.add_metric('test', None, '', '', '',
-                         msg_fmt="An example of adding a dummy metric to add another line", display_in_perf=False)
+                         display_format="An example of adding a dummy metric to add another line",
+                         display_in_perf=False)
     elif args.mode == "swap_usage":
         swap_usage = get_swap_usage()
         check.add_metric('swap_utilisation', swap_usage.percent, '%', args.warning, args.critical)
diff --git a/plugnpy/__init__.py b/plugnpy/__init__.py
index f4f18cd..9ead37b 100644
--- a/plugnpy/__init__.py
+++ b/plugnpy/__init__.py
@@ -1,6 +1,6 @@
 """plugnpy - A Simple Python Library for creating Opsview Opspack plugins"""
 
-__version__ = '1.1.1'
+__version__ = '2.0.0'
 __release__ = '1'
 __program_name__ = 'plugnpy'
 
diff --git a/plugnpy/check.py b/plugnpy/check.py
index bd2b3de..a0ff6a9 100644
--- a/plugnpy/check.py
+++ b/plugnpy/check.py
@@ -28,27 +28,34 @@ class Check(object):
                         metric_obj.unit,
                         metric_obj.warning_threshold,
                         metric_obj.critical_threshold,
-                        metric_obj.display_unit_factor_type,
                         metric_obj.display_format,
                         metric_obj.display_in_perf,
                         metric_obj.display_in_summary,
                         metric_obj.display_name,
-                        metric_obj.convert_metric)
+                        metric_obj.convert_metric,
+                        metric_obj.si_bytes_conversion,
+                        metric_obj.summary_precision,
+                        metric_obj.perf_data_precision)
 
     def add_metric(self, name, value, unit='', warning_threshold='', critical_threshold='',
-                   display_unit_factor_type='bytes', msg_fmt='{name} is {value}{unit}', display_in_perf=True,
-                   display_in_summary=True, display_name=None, convert_metric=False):
+                   display_format='{name} is {value}{unit}', display_in_perf=True,
+                   display_in_summary=True, display_name=None, convert_metric=None, si_bytes_conversion=False,
+                   summary_precision=2, perf_data_precision=2):
         """Add a metric to the check's performance data.
 
         Keyword Arguments:
-        name -- the name of the metric
-        value -- the value of the metric
-        unit -- the unit of measurement of the metric
-        warning_threshold -- the warning threshold for the metric - see Monitoring Plugins Development Guidelines
-        critical_threshold -- the critical threshold for the metric - see Monitoring Plugins Development Guidelines
-        msg_fmt -- the format for the output of the metric
-        display_in_perf -- whether or not to add this metric to the performance data
-        display_in_summary -- whether or not to add this metric to the Summary
+        - name -- Name of the Metric
+        - value -- Value of the Metric (note: do not include unit of measure)
+        - unit -- Unit of Measure of the Metric
+        - warning_threshold -- Warning threshold for the Metric (default: '') - see Monitoring Plugins Development Guidelines
+        - critical_threshold -- Critical threshold for the Metric (default: '') - see Monitoring Plugins Development Guidelines
+        - display_format -- Formatting string to print the Metric (default: "{name} is {value} {unit}")
+        - display_name -- Name to be used in friendly output (default: value of name)
+        - display_in_summary -- Whether to print the metric in the summary (default: True)
+        - display_in_perf -- Whether to print the metric in performance data (default: True)
+        - convert_metric -- Whether to convert the metric value to a more human friendly unit (default: False)
+        - si_bytes_conversion -- Whether to convert values using the SI standard, uses IEC by default (default: False)
+        - precision -- The number of decimal places to round the metric value to (default 2)
         """
 
         if "'" in name:
@@ -56,16 +63,21 @@ class Check(object):
         if "=" in name:
             raise InvalidMetricName("Metric names cannot contain \"=\".")
 
-        metric = Metric(name, value, unit, warning_threshold or '', critical_threshold or '', display_unit_factor_type,
-                        display_format=msg_fmt, display_in_perf=display_in_perf, display_in_summary=display_in_summary,
-                        display_name=display_name, convert_metric=convert_metric)
+        metric = Metric(name, value, unit, warning_threshold or '', critical_threshold or '',
+                        display_format=display_format, display_in_perf=display_in_perf,
+                        display_in_summary=display_in_summary, display_name=display_name,
+                        convert_metric=convert_metric, si_bytes_conversion=si_bytes_conversion,
+                        summary_precision=summary_precision, perf_data_precision=perf_data_precision)
         self.metrics.append(metric)
 
     def add_message(self, message):
         """Add a message"""
         self.metrics[-1].message = message
 
-    def _exit(self, code, message):
+    def exit(self, code, message):
+        """Exits with specified message and specified exit status.
+        Note: existing messages and metrics are discarded.
+        """
         print("{0} {1} - {2}".format(self.state_type, Check.STATUS[code], message))
         exit(code)
 
@@ -73,34 +85,30 @@ class Check(object):
         """Exits with specified message and OK exit status.
         Note: existing messages and metrics are discarded.
         """
-        self._exit(0, message)
+        self.exit(0, message)
 
     def exit_warning(self, message):
         """Exits with specified message and WARNING exit status.
         Note: existing messages and metrics are discarded.
         """
-        self._exit(1, message)
+        self.exit(1, message)
 
     def exit_critical(self, message):
         """Exits with specified message and CRITICAL exit status.
         Note: existing messages and metrics are discarded.
         """
-        self._exit(2, message)
+        self.exit(2, message)
 
     def exit_unknown(self, message):
         """Exits with specified message and UNKNOWN exit status.
         Note: existing messages and metrics are discarded.
         """
-        self._exit(3, message)
+        self.exit(3, message)
 
     def final(self):
         """Calculates the final check output and exit status, prints and exits with the appropriate code."""
         human_results = [str(metric) for metric in self.metrics if metric.display_in_summary]
-        perf_results = ["{0}={1}{2}{3}{4}{5}{6}".format(
-            "'{0}'".format(metric.name) if ' ' in metric.name else metric.name, metric.value, metric.unit,
-            ';' if metric.warning_threshold or metric.critical_threshold else '', metric.warning_threshold,
-            ';' if metric.warning_threshold or metric.critical_threshold else '', metric.critical_threshold
-        ) for metric in self.metrics if metric.display_in_perf]
+        perf_results = [metric.perf_data for metric in self.metrics if metric.display_in_perf]
 
         summary = "{0}{1}{2}".format(self.sep.join(human_results),
                                      ' | ' if perf_results else '',
diff --git a/plugnpy/metric.py b/plugnpy/metric.py
index b1410e3..7f739d5 100644
--- a/plugnpy/metric.py
+++ b/plugnpy/metric.py
@@ -1,29 +1,9 @@
 """
 Metric Class.
 """
-import re
 from .exception import InvalidMetricThreshold
 
 
-# Prefixes for units
-FACTOR_BASE_NUMBER = {
-    'decimal': 1000,
-    'bytes': 1024
-}
-DISPLAY_UNIT_FACTORS = {
-    '': {'decimal': 1.0, 'bytes': 1.0},
-    'B': {'decimal': 1.0, 'bytes': 1.0},
-    'p': {'decimal': FACTOR_BASE_NUMBER['decimal']**4, 'bytes': FACTOR_BASE_NUMBER['bytes']**4},  # pico
-    'n': {'decimal': FACTOR_BASE_NUMBER['decimal']**3, 'bytes': FACTOR_BASE_NUMBER['bytes']**3},  # nano
-    'u': {'decimal': FACTOR_BASE_NUMBER['decimal']**2, 'bytes': FACTOR_BASE_NUMBER['bytes']**2},  # micro
-    'm': {'decimal': FACTOR_BASE_NUMBER['decimal'], 'bytes': FACTOR_BASE_NUMBER['bytes']},  # mili
-    'K': {'decimal': 1.0 / FACTOR_BASE_NUMBER['decimal'], 'bytes': 1.0 / FACTOR_BASE_NUMBER['bytes']},  # kilo
-    'M': {'decimal': 1.0 / FACTOR_BASE_NUMBER['decimal']**2, 'bytes': 1.0 / FACTOR_BASE_NUMBER['bytes']**2},  # mega
-    'G': {'decimal': 1.0 / FACTOR_BASE_NUMBER['decimal']**3, 'bytes': 1.0 / FACTOR_BASE_NUMBER['bytes']**3},  # giga
-    'T': {'decimal': 1.0 / FACTOR_BASE_NUMBER['decimal']**4, 'bytes': 1.0 / FACTOR_BASE_NUMBER['bytes']**4},  # tera
-}
-
-
 class Metric(object):
     """Object to represent Metrics added to a Check object.
 
@@ -33,99 +13,220 @@ class Metric(object):
         - unit -- Unit of Measure of the Metric
         - warning_threshold -- Warning threshold for the Metric (default: '')
         - critical_threshold -- Critical threshold for the Metric (default: '')
-        - display_unit_factor_type -- Whether to convert Bytes in to Decimal or Binary formats  (default: 'bytes')
         - display_format -- Formatting string to print the Metric (default: "{name} is {value} {unit}")
-        - convert_metric -- Whether to convert the metric in to a friendly form (default: True)
         - display_name -- Name to be used in friendly output (default: value of name)
-        - display_in_perf -- Whether to print the metric in performance variable (default: True)
+        - display_in_summary -- Whether to print the metric in the summary (default: True)
+        - display_in_perf -- Whether to print the metric in performance data (default: True)
+        - convert_metric -- Whether to convert the metric value to a more human friendly unit (default: False)
+        - si_bytes_conversion -- Whether to convert values using the SI standard, uses IEC by default (default: False)
+        - precision -- The number of decimal places to round the metric value to (default 2)
         """
 
     P_INF = float('inf')
     N_INF = float('-inf')
 
+    STATUS_OK = 0
+    STATUS_WARNING = 1
+    STATUS_CRITICAL = 2
+    STATUS_UNKNOWN = 3
+
+    SI_UNIT_FACTOR = 1000
+    IEC_UNIT_FACTOR = 1024
+
+    UNIT_BITS = 'b'
+    UNIT_BYTES = 'B'
+    UNIT_BITS_PS = 'bps'
+    UNIT_BYTES_PS = 'Bps'
+    UNIT_WATTS = 'W'
+    UNIT_HERTZ = 'Hz'
+    UNIT_SECONDS = 's'
+    UNIT_COUNT = 'c'
+
+    UNIT_PICO = 'p'
+    UNIT_NANO = 'n'
+    UNIT_MICRO = 'u'
+    UNIT_MILLI = 'm'
+    UNIT_KILO = 'K'
+    UNIT_MEGA = 'M'
+    UNIT_GIGA = 'G'
+    UNIT_TERA = 'T'
+    UNIT_PETA = 'P'
+    UNIT_EXA = 'E'
+
+    UNIT_PREFIXES_P = (UNIT_EXA, UNIT_PETA, UNIT_TERA, UNIT_GIGA, UNIT_MEGA, UNIT_KILO)
+    UNIT_PREFIXES_N = (UNIT_MILLI, UNIT_MICRO, UNIT_NANO, UNIT_PICO)
+
+    BYTE_UNITS = (UNIT_BYTES, UNIT_BITS, UNIT_BITS_PS, UNIT_BYTES_PS)
+    CONVERTIBLE_UNITS_P = BYTE_UNITS + (UNIT_HERTZ, UNIT_WATTS)
+    CONVERTIBLE_UNITS_N = (UNIT_SECONDS, UNIT_HERTZ, UNIT_WATTS)
+    CONVERTIBLE_UNITS = CONVERTIBLE_UNITS_P + CONVERTIBLE_UNITS_N
+
+    # Prefixes for units
+    DISPLAY_UNIT_FACTORS = {
+        UNIT_BYTES: lambda factor: 1.0,
+        UNIT_PICO: lambda factor: factor ** 4,
+        UNIT_NANO: lambda factor: factor ** 3,
+        UNIT_MICRO: lambda factor: factor ** 2,
+        UNIT_MILLI: lambda factor: factor,
+        UNIT_KILO: lambda factor: 1.0 / factor,
+        UNIT_MEGA: lambda factor: 1.0 / factor ** 2,
+        UNIT_GIGA: lambda factor: 1.0 / factor ** 3,
+        UNIT_TERA: lambda factor: 1.0 / factor ** 4,
+        UNIT_PETA: lambda factor: 1.0 / factor ** 5,
+        UNIT_EXA: lambda factor: 1.0 / factor ** 6,
+    }
+
     def __init__(
             self, name, value, unit,
             warning_threshold='',
             critical_threshold='',
-            display_unit_factor_type='bytes',
             display_format="{name} is {value}{unit}",
-            convert_metric=True,
             display_name=None,
             display_in_summary=True,
             display_in_perf=True,
+            convert_metric=None,
+            si_bytes_conversion=False,
+            summary_precision=2,
+            perf_data_precision=2
     ):
         self.name = name
-        self.display_in_summary = display_in_summary
-        self.display_in_perf = display_in_perf
-        if not display_name:
-            display_name = self.name
-        self.display_name = display_name
         self.value = value
         self.unit = unit
-
-        self.display_unit_factor_type = display_unit_factor_type
+        self.warning_threshold = warning_threshold
+        self.critical_threshold = critical_threshold
         self.display_format = display_format
+        self.display_name = display_name if display_name else self.name
+        self.display_in_summary = display_in_summary
+        self.display_in_perf = display_in_perf
         self.message = None
-        self.convert_metric = convert_metric
 
-        self.warning_threshold = warning_threshold
-        self.critical_threshold = critical_threshold
+        self.convert_metric = convert_metric or (convert_metric is None and unit in Metric.BYTE_UNITS)
 
-        if self.critical_threshold and self.check(value, *self.parse_threshold(self.critical_threshold)):
-            self.state = 2
-        elif self.warning_threshold and self.check(value, *self.parse_threshold(self.warning_threshold)):
-            self.state = 1
-        else:
-            self.state = 0
+        self.si_bytes_conversion = si_bytes_conversion
+        self.state = Metric.evaluate(value, warning_threshold, critical_threshold, si_bytes_conversion)
 
-    def convert_automatic_value(self, value):
-        """Converts values with the right prefix for display."""
-        for key in ('T', 'G', 'M', 'K'):
-            if 1 / DISPLAY_UNIT_FACTORS[key][self.display_unit_factor_type] <= float(value):
-                return float(value) * DISPLAY_UNIT_FACTORS[key][self.display_unit_factor_type], \
-                       '{0}{1}'.format(key, self.unit)
-        return value, self.unit
+        try:
+            self.summary_precision = int(summary_precision)
+        except (ValueError, TypeError) as ex:
+            raise Exception("Invalid value for summary precision '{0}': {1}".format(summary_precision, ex))
 
-    def convert_threshold(self, value):
-        """Convert threshold value."""
-        split_list = re.split(r"([a-z])", str(value), 1, flags=re.I)
-        value = int(split_list[0])
-        if len(split_list) > 1:
-            try:
-                unit = "".join(split_list[1:])[0]
-                return str(int(value / DISPLAY_UNIT_FACTORS[unit][self.display_unit_factor_type]))
-            except Exception as exp:
-                raise InvalidMetricThreshold(exp)
-        return str(value)
+        try:
+            self.perf_data_precision = int(perf_data_precision)
+        except (ValueError, TypeError) as ex:
+            raise Exception("Invalid value for performance data precision '{0}': {1}".format(perf_data_precision, ex))
+        try:
+            self.value = float(self.value)
+            self.perf_data = Metric.calculate_perf_data(name, value, unit, warning_threshold, critical_threshold,
+                                                        precision=self.perf_data_precision)
+        except (ValueError, TypeError):
+            self.perf_data = ''
 
     def __str__(self):
         if self.message:
             return self.message
-        name = self.display_name
-        value = self.value
         unit = self.unit
+        value = self.value
 
         if self.convert_metric:
-            if self.unit == 'B':
-                value, unit = self.convert_automatic_value(self.value)
-                if unit != 'B':
-                    value = "{0:.2f}".format(value)
-        return self.display_format.format(**{'name': name, 'value': value, 'unit': unit})
-
-    def parse_threshold_limit(self, value, is_start):
-        """Parses a numeric string with a unit prefix e.g. 10 > 10.0, 10m > 0.001, 10M > 1000000.0, ~ > -inf/inf."""
+            value, unit = Metric.convert_value(self.value, self.unit, si_bytes_conversion=self.si_bytes_conversion)
+
+        # try to convert value to precision specified if it's a number
+        try:
+            value = float(value)
+            value = "{0:.{1}f}".format(value, self.summary_precision)
+        except (ValueError, TypeError):
+            pass
+
+        return self.display_format.format(**{'name': self.display_name,
+                                             'value': value,
+                                             'unit': unit})
+
+    @staticmethod
+    def evaluate(value, warning_threshold, critical_threshold, si_bytes_conversion=False):
+        """Returns the status code of a check obtained by evaluating the value against warning and critical thresholds"""
+        status = Metric.STATUS_OK
+        if warning_threshold:
+            if Metric._check_range(value, *Metric._parse_threshold(warning_threshold, si_bytes_conversion)):
+                status = Metric.STATUS_WARNING
+        if critical_threshold:
+            if Metric._check_range(value, *Metric._parse_threshold(critical_threshold, si_bytes_conversion)):
+                status = Metric.STATUS_CRITICAL
+        return status
+
+    @staticmethod
+    def calculate_perf_data(name, value, unit, warning_threshold, critical_threshold, precision=2):
+        """Returns the perf data string for the check"""
+        value = '{0:.{1}f}'.format(value, precision)
+        return "{0}={1}{2}{3}{4}{5}{6}".format(
+            "'{0}'".format(name) if ' ' in name else name, value, unit,
+            ';' if warning_threshold or critical_threshold else '', warning_threshold,
+            ';' if warning_threshold or critical_threshold else '', critical_threshold
+        )
+
+    @staticmethod
+    def convert_value(value, unit, si_bytes_conversion=False):
+        """Converts values with the right prefix for display."""
+        try:
+            value = float(value)
+        except (ValueError, TypeError) as ex:
+            raise Exception("Invalid value for value '{0}': {1}".format(value, ex))
+
+        if unit in Metric.CONVERTIBLE_UNITS:
+            conversion_factor = Metric._get_conversion_factor(unit, si_bytes_conversion)
+
+            keys = []
+            if value < 1 and unit in Metric.CONVERTIBLE_UNITS_N:
+                keys = Metric.UNIT_PREFIXES_N
+            elif value > 1 and unit in Metric.CONVERTIBLE_UNITS_P:
+                keys = Metric.UNIT_PREFIXES_P
+
+            for key in keys:
+                multiplication_factor = Metric.DISPLAY_UNIT_FACTORS[key](conversion_factor)
+                if 1.0 / multiplication_factor <= value:
+                    return value * multiplication_factor, '{0}{1}'.format(key, unit)
+        return value, unit
+
+    @staticmethod
+    def _get_conversion_factor(unit, si_bytes_conversion):
+        if unit in Metric.BYTE_UNITS and not si_bytes_conversion:
+            return Metric.IEC_UNIT_FACTOR
+        return Metric.SI_UNIT_FACTOR
+
+    @staticmethod
+    def _convert_threshold(value, si_bytes_conversion):
+        """Convert threshold value."""
+        value = str(value)
+        unit = value.lstrip('.1234567890')
+        if unit:
+            value = value[:-len(unit)]
+
+        try:
+            value = float(value)
+        except (ValueError, TypeError) as exp:
+            raise InvalidMetricThreshold("Invalid metric threshold: {0}.".format(exp))
+
+        if unit:
+            try:
+                unit_prefix = unit[0]
+                conversion_factor = Metric._get_conversion_factor(unit[-1], si_bytes_conversion)
+                value = value / Metric.DISPLAY_UNIT_FACTORS[unit_prefix](conversion_factor)
+            except KeyError as exp:
+                raise InvalidMetricThreshold("Invalid metric threshold: {0}.".format(exp))
+        return value
+
+    @staticmethod
+    def _parse_threshold_limit(value, is_start, si_bytes_conversion):
+        """Parses a numeric string with a unit prefix e.g. 10 -> 10.0, 10m -> 0.001, 10M -> 1000000.0, ~ -> -inf/inf."""
         if value == '~':  # Infinite values
             if is_start:
-                return self.N_INF
-            return self.P_INF
-        if value[-1].isdigit():  # No unit prefix
+                return Metric.N_INF
+            return Metric.P_INF
+        if value[-1].isdigit():  # No unit suffix
             return float(value)
-        elif value[-1] == 'a':  # Bytes unit prefix
-            return float(value[:-1])
-        # Decimal unit prefix
-        return self.convert_threshold(value)
+        return Metric._convert_threshold(value, si_bytes_conversion)
 
-    def parse_threshold(self, threshold):
+    @staticmethod
+    def _parse_threshold(threshold, si_bytes_conversion):
         """
         Parse threshold and return the range and whether we alert if value is out of range or in the range.
         See: https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT
@@ -137,20 +238,20 @@ class Metric(object):
                 threshold = threshold[1:]
             if ':' not in threshold:
                 start = 0.0
-                end = self.parse_threshold_limit(threshold, False)
+                end = Metric._parse_threshold_limit(threshold, False, si_bytes_conversion)
             elif threshold.endswith(':'):
-                start = self.parse_threshold_limit(threshold[:-1], True)
-                end = self.P_INF
+                start = Metric._parse_threshold_limit(threshold[:-1], True, si_bytes_conversion)
+                end = Metric.P_INF
             else:
                 unparsed_start, unparsed_end = threshold.split(':')
-                start = self.parse_threshold_limit(unparsed_start, True)
-                end = self.parse_threshold_limit(unparsed_end, False)
+                start = Metric._parse_threshold_limit(unparsed_start, True, si_bytes_conversion)
+                end = Metric._parse_threshold_limit(unparsed_end, False, si_bytes_conversion)
             return start, end, check_outside_range
         except Exception as exp:
-            raise InvalidMetricThreshold(str(exp))
+            raise InvalidMetricThreshold("Invalid metric threshold: {0}.".format(exp))
 
     @staticmethod
-    def check(value, start, end, check_outside_range):
+    def _check_range(value, start, end, check_outside_range):
         """Check whether the value is inside/outside the range."""
         value, start, end = float(value), float(start), float(end)
         outside_range = value < start or value > end
diff --git a/plugnpy/parser.py b/plugnpy/parser.py
index 19ea054..5aca3e2 100644
--- a/plugnpy/parser.py
+++ b/plugnpy/parser.py
@@ -2,12 +2,33 @@
 Argument Parser class for use in service checks.
 Based on argparse.ArgumentParser but overwrites exiting calls to exit Unknowns.
 """
-from argparse import ArgumentParser
+import argparse
 from gettext import gettext
 import sys
 
 
-class Parser(ArgumentParser):
+class _HelpAction(argparse.Action):
+    def __init__(self,
+                 option_strings,
+                 dest=argparse.SUPPRESS,
+                 default=argparse.SUPPRESS,
+                 help=None):
+        super(_HelpAction, self).__init__(
+            option_strings=option_strings,
+            dest=dest,
+            default=default,
+            nargs=0,
+            help=help)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        parser.print_help()
+        parser.exit(3)
+
+
+argparse._HelpAction = _HelpAction
+
+
+class Parser(argparse.ArgumentParser):
     """Object for parsing command line strings into Python objects.
     Modified to exit with Opsview specific exits.
 
diff --git a/setup.py b/setup.py
index 2c2fdd5..309bf35 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ from plugnpy import __release__ as RELEASE
 
 
 def read(fname):
-    return open(join(dirname(__file__), fname)).read()
+    return open(join(dirname(__file__), fname), encoding='utf-8').read()
 
 
 class RunTests(Command):


Test patch:
diff --git a/test/README.md b/test/README.md
index 6023251..4f6baf3 100644
--- a/test/README.md
+++ b/test/README.md
@@ -1,6 +1,6 @@
 ## plugnpy tests
 
-### Prequesits
+### Prerequisites
 
 To run these unit tests, [pytest-cov](https://pypi.org/project/pytest-cov/) and [pytest](https://docs.pytest.org/en/latest/contents.html) are required. To quickly install all the required libraries, run the following.
 
diff --git a/test/test_check.py b/test/test_check.py
index 9d525de..d6ace87 100644
--- a/test/test_check.py
+++ b/test/test_check.py
@@ -4,11 +4,12 @@ import pytest
 
 from plugnpy.check import Check
 from plugnpy.metric import Metric
+from plugnpy.exception import InvalidMetricName
 
 
 def test_add_metric():
     check = Check()
-    check.add_metric('Memory', 4500000, 'bytes', '4M:', '6M:')
+    check.add_metric('Memory', 4500000, 'B', '4M:', '6M:')
     check.add_metric('CPU', 7, '%', '20:', '50:')
     assert len(check.metrics) == 2
     for metric in check.metrics:
@@ -17,14 +18,25 @@ def test_add_metric():
 
 def test_add_metric_obj():
     check = Check()
-    check.add_metric_obj(Metric('Memory', 4500000, 'bytes', '4M:', '6M:'))
+    check.add_metric_obj(Metric('Memory', 4500000, 'B', '4M:', '6M:'))
     check.add_metric_obj(Metric('CPU', 7, '%', '20:', '50:'))
     assert len(check.metrics) == 2
     for metric in check.metrics:
         assert isinstance(metric, Metric)
 
 
-@pytest.mark.parametrize('status,expected', [
+@pytest.mark.parametrize('metric_name, expected', [
+    ("invalid 'name'", "Metric names cannot contain \"'\"."),
+    ('invalid=name', "Metric names cannot contain \"=\"."),
+])
+def test_add_metric_invalid_metric_name(metric_name, expected):
+    check = Check()
+    with pytest.raises(InvalidMetricName) as ex:
+        check.add_metric(metric_name, 4500000, 'B', '4M:', '6M:')
+    assert str(ex.value) == expected
+
+
+@pytest.mark.parametrize('status, expected', [
     ('ok', 0),
     ('warning', 1),
     ('critical', 2),
@@ -44,13 +56,27 @@ def test_exit(capsys, status, expected):
 
 def test_final(capsys):
     check = Check()
-    check.add_metric('Memory', 4500000, 'B', '4M:', '6M:', convert_metric=True)
+    check.add_metric('Memory', 4500000, 'B', '4M:', '6M:', convert_metric=True, perf_data_precision=0)
     check.add_metric('CPU', 7, '%', '20:', '50:')
     with pytest.raises(SystemExit) as e:
         check.final()
     assert e.value.code == 2
     assert capsys.readouterr().out == (
-        'METRIC CRITICAL - Memory is 4.29MB, CPU is 7% | Memory=4500000B;4M:;6M: CPU=7%;20:;50:{0}'.format(
+        'METRIC CRITICAL - Memory is 4.29MB, CPU is 7.00% | Memory=4500000B;4M:;6M: CPU=7.00%;20:;50:{0}'.format(
+            os.linesep
+        )
+    )
+
+def test_final_with_message(capsys):
+    check = Check()
+    check.add_metric('Memory', 4500000, 'B', '4M:', '6M:', convert_metric=True, perf_data_precision=3)
+    check.add_metric('CPU', 7, '%', '20:', '50:', perf_data_precision=3)
+    check.add_message('CPU is OK')
+    with pytest.raises(SystemExit) as e:
+        check.final()
+    assert e.value.code == 2
+    assert capsys.readouterr().out == (
+        'METRIC CRITICAL - Memory is 4.29MB, CPU is OK | Memory=4500000.000B;4M:;6M: CPU=7.000%;20:;50:{0}'.format(
             os.linesep
         )
     )
@@ -64,7 +90,7 @@ def test_final_with_obj(capsys):
         check.final()
     assert e.value.code == 2
     assert capsys.readouterr().out == (
-        'METRIC CRITICAL - Memory is 4.29MB, CPU is 7% | Memory=4500000B;4M:;6M: CPU=7%;20:;50:{0}'.format(
+        'METRIC CRITICAL - Memory is 4.29MB, CPU is 7.00% | Memory=4500000.00B;4M:;6M: CPU=7.00%;20:;50:{0}'.format(
             os.linesep
         )
     )
diff --git a/test/test_metric.py b/test/test_metric.py
index 4ce54fb..b34f366 100644
--- a/test/test_metric.py
+++ b/test/test_metric.py
@@ -14,34 +14,35 @@ def raise_or_assert(callback, raises, expected):
         assert callback() == expected
 
 
-@pytest.mark.parametrize('display_unit_factor_type,display_format,raises,expected,convert_metric', [
-    ('bytes', '{name} is {value} {unit}', False, 'Memory is 1024 bytes', False),
-    ('bytes', '{value}{unit}', False, '1024bytes', False),
-    ('bytes', '{bad} {wrong}', KeyError, None, False),
-    ('decimal', '{name} is {value} {unit}', False, 'Memory is 1024 bytes', False),
-    ('bytes', '{name} is {value} {unit}', False, 'Memory is 1024 bytes', True),
-],
-    ids=[
-        'display_unit_factor',
-        'no_display_unit_factor',
+@pytest.mark.parametrize('value, si_bytes_conversion, display_format, raises, expected, convert_metric', [
+    (1024, False, '{name} is {value} {unit}', False, 'Memory is 1024.00 B', False),
+    (1024, False, '{value}{unit}', False, '1024.00B', False),
+    (1024, False, '{bad} {wrong}', KeyError, None, False),
+    (1024, False, '{name} is {value} {unit}', False, 'Memory is 1.00 KB', True),
+    (1000, True, '{name} is {value} {unit}', False, 'Memory is 1.00 KB', True),
+    ('GREEN', False, '{name} is {value}', False, 'Memory is GREEN', False),
+], ids=[
+        'convert_false',
+        'display_format',
         'wrong_display_format',
-        'automatic_value_decimal',
-        'automatic_value_bytes',
+        'convert_si_units',
+        'convert_non_si_units',
+        'str_value',
 ])
-def test_str(display_unit_factor_type, display_format, raises, expected, convert_metric):
+def test_str(value, si_bytes_conversion, display_format, raises, expected, convert_metric):
     raise_or_assert(
         functools.partial(str, Metric(
-            'Memory', 1024, 'bytes',
-            display_unit_factor_type=display_unit_factor_type,
+            'Memory', value, 'B',
             display_format=display_format,
-            convert_metric=convert_metric
+            convert_metric=convert_metric,
+            si_bytes_conversion=si_bytes_conversion
         )),
         raises,
         expected
     )
 
 
-@pytest.mark.parametrize('value,start,end,check_outside_range,expected', [
+@pytest.mark.parametrize('value, start, end, check_outside_range, expected', [
     (1, 1, 100, True, False),
     (0, 1, 100, True, True),
     (1, 1, 100, False, True),
@@ -52,15 +53,72 @@ def test_str(display_unit_factor_type, display_format, raises, expected, convert
     'inside_ok',
     'inside_ko',
 ])
-def test_check(value, start, end, check_outside_range, expected):
-    assert Metric.check(value, start, end, check_outside_range) == expected
+def test_check_range(value, start, end, check_outside_range, expected):
+    assert Metric._check_range(value, start, end, check_outside_range) == expected
+
+
+@pytest.mark.parametrize('value, unit, si_bytes_conversion, raises, expected', [
+    # bytes conversion IEC (International Electrotechnical Commission) units
+    (1, 'B', False, False, (1.0, 'B')),
+    (1024, 'B', False, False, (1.0, 'KB')),
+
+    # bytes conversion SI (International System) units
+    (1, 'B', True, False, (1.0, 'B')),
+    (1e-3, 'B', True, False, (0.001, 'B')),
+    (1e3, 'B', True, False, (1.0, 'KB')),
+    (1e6, 'B', True, False, (1.0, 'MB')),
+    (1e9, 'B', True, False, (1.0, 'GB')),
+    (1e12, 'B', True, False, (1.0, 'TB')),
+    (1e15, 'B', True, False, (1.0, 'PB')),
+    (1e18, 'B', True, False, (1.0, 'EB')),
+
+    # conversion SI units
+    (1, 'W', True, False, (1.0, 'W')),
+    (1e-3, 'Hz', True, False, (1.0, 'mHz')),
+    (1e-6, 'W', True, False, (1.0, 'uW')),
+    (1e-9, 'Hz', True, False, (1.0, 'nHz')),
+    (1e-12, 's', True, False, (1.0, 'ps')),
+    (1e3, 'Hz', True, False, (1.0, 'KHz')),
+    (1e6, 'W', True, False, (1.0, 'MW')),
+    (1e9, 'Hz', True, False, (1.0, 'GHz')),
+    (1e12, 's', True, False, (1000000000000.0, 's')),
+
+    # no conversion
+    (1, '', True, False, (1.0, '')),
+    (1e3, '', True, False, (1000, '')),
+    (1e-3, '', True, False, (0.001, '')),
+
+    # invalid value
+    ('a', '', True, Exception, None),
+])
+def test_convert_value(value, unit, si_bytes_conversion, raises, expected):
+    raise_or_assert(
+        functools.partial(Metric.convert_value, value, unit, si_bytes_conversion), raises, expected
+    )
+
+
+@pytest.mark.parametrize('value, si_bytes_conversion, raises, expected', [
+    ('1', True, False, 1.0),
+    ('1B', True, False, 1.0),
+    ('1KB', True, False, 1000.0),
+    ('1000m', True, False, 1.0),
+    ('1', False, False, 1.0),
+    ('1B', False, False, 1.0),
+    ('1KB', False, False, 1024.0),
+    ('', False, InvalidMetricThreshold, ''),
+    ('KB', False, InvalidMetricThreshold, ''),
+])
+def test_convert_threshold(value, si_bytes_conversion, raises, expected):
+    raise_or_assert(
+        functools.partial(Metric._convert_threshold, value, si_bytes_conversion), raises, expected
+    )
 
 
-@pytest.mark.parametrize('value,is_start,raises,expected', [
+@pytest.mark.parametrize('value, is_start, raises, expected', [
     ('~', False, False, Metric.P_INF),
     ('~', True, False, Metric.N_INF),
     ('10', True, False, 10.0),
-    ('10K', True, False, "10240"),
+    ('10K', True, False, 10000.0),
     ('10S', True, InvalidMetricThreshold, None),
 ], ids=[
     'infinite',
@@ -71,11 +129,11 @@ def test_check(value, start, end, check_outside_range, expected):
 ])
 def test_parse_threshold_limit(value, is_start, raises, expected):
     raise_or_assert(
-        functools.partial(Metric('Something', 10, '%').parse_threshold_limit, value, is_start), raises, expected
+        functools.partial(Metric._parse_threshold_limit, value, is_start, Metric.SI_UNIT_FACTOR), raises, expected
     )
 
 
-@pytest.mark.parametrize('threshold,raises,expected', [
+@pytest.mark.parametrize('threshold, raises, expected', [
     ('@1:3', False, (1.0, 3.0, False)),
     ('10', False, (0.0, 10.0, True)),
     ('3:', False, (3.0, Metric.P_INF, True)),
@@ -89,10 +147,10 @@ def test_parse_threshold_limit(value, is_start, raises, expected):
     'wrong_threshold'
 ])
 def test_parse_threshold(threshold, raises, expected):
-    raise_or_assert(functools.partial(Metric('Something', 10, '%').parse_threshold, threshold), raises, expected)
+    raise_or_assert(functools.partial(Metric._parse_threshold, threshold, Metric.SI_UNIT_FACTOR), raises, expected)
 
 
-@pytest.mark.parametrize('warning_threshold,critical_threshold,expected', [
+@pytest.mark.parametrize('warning_threshold, critical_threshold, expected', [
     ('50:120', '30:150', 0),
     ('110:150', '90:170', 1),
     ('130:150', '110:170', 2),
@@ -103,3 +161,30 @@ def test_parse_threshold(threshold, raises, expected):
 ])
 def test_state(warning_threshold, critical_threshold, expected):
     assert Metric('Memory', 100, 'bytes', warning_threshold, critical_threshold).state == expected
+
+def test_summary_precision():
+    metric = Metric('metric_name', 10.12345, 'B', summary_precision=3, perf_data_precision=2)
+    assert '10.123' in str(metric)
+    assert '10.12' in str(metric.perf_data)
+    assert '10.12345' not in str(metric)
+    assert '10.123' not in str(metric.perf_data)
+
+@pytest.mark.parametrize('value, error_msg',
+                         [(None, "Invalid value for performance data precision 'None': "
+                                 "int() argument must be"),
+                          ('abc', "Invalid value for performance data precision 'abc': "
+                                  "invalid literal for int() with base 10: 'abc'")])
+def test_invalid_perf_data_precision(value, error_msg):
+    with pytest.raises(Exception) as ex:
+        Metric('metric_name', 10.12345, 'B', perf_data_precision=value)
+    assert error_msg in str(ex.value)
+
+@pytest.mark.parametrize('value, error_msg',
+                         [(None, "Invalid value for summary precision 'None': "
+                                 "int() argument must be a string"),
+                          ('abc', "Invalid value for summary precision 'abc': "
+                                  "invalid literal for int() with base 10: 'abc'")])
+def test_invalid_summary_precision(value, error_msg):
+    with pytest.raises(Exception) as ex:
+        Metric('metric_name', 10.12345, 'B', summary_precision=value)
+    assert error_msg in str(ex.value)
diff --git a/test/test_parser.py b/test/test_parser.py
index 248f43f..228ad35 100644
--- a/test/test_parser.py
+++ b/test/test_parser.py
@@ -11,8 +11,20 @@ def test_copyright(capsys):
     parser.print_help(sys.stdout)
     assert 'copyright string' in capsys.readouterr().out
 
+    parser.set_copyright('new copyright string')
+    parser.print_help(sys.stdout)
+    assert 'new copyright string' in capsys.readouterr().out
+
+
 def test_customizations(capsys):
     with pytest.raises(SystemExit) as e:
         Parser().error('something')
     assert e.value.code == 3
     assert capsys.readouterr().out.endswith('something{0}'.format(os.linesep))
+
+
+def test_help_exit(capsys):
+    parser = Parser()
+    with pytest.raises(SystemExit) as ex:
+        parser.parse_args(args=['-h'])
+    assert ex.value.code == 3